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

step.gno

25.65 Kb · 852 lines
  1package physics
  2
  3import (
  4	"math"
  5	"strings"
  6)
  7
  8// G is gravity, in board units per substep squared: a hill's Vec is G·sin θ.
  9const G = 1.0
 10
 11// The calibration of Rolling, fitted by least squares to the course's
 12// strokes: keep = min((Friction+keepBonus)·Scale, maxKeep), then
 13// Crr = ((1−keep)·RollSpeed + RollDrag/Scale) / G.
 14const (
 15	RollSpeed = 1.8
 16	RollDrag  = 0.08
 17	keepBonus = 0.05
 18	maxKeep   = 0.98
 19)
 20
 21// Rolling is the rolling-resistance coefficient Crr of a green of the given
 22// Friction under a Surface of the given Scale (1 is plain grass). A Scale of
 23// 0 or less is an infinite Crr: the ball stays where it is.
 24func Rolling(friction, scale float64) float64 {
 25	if !(scale > 0) {
 26		return math.Inf(1)
 27	}
 28	keep := (friction + keepBonus) * scale
 29	if keep > maxKeep {
 30		keep = maxKeep
 31	}
 32	return ((1-keep)*RollSpeed + RollDrag/scale) / G
 33}
 34
 35// The contact rules for walls and posts.
 36const (
 37	// WallFriction is the Coulomb µ between the ball and a wall or post:
 38	// the tangential impulse is at most µ times the normal one.
 39	WallFriction = 0.05
 40	// TangentMass caps the tangential impulse, per unit mass and speed:
 41	// 2/7 is what stops a solid ball's contact point from slipping.
 42	TangentMass = 2.0 / 7
 43	// RestSpeed is the speed into a surface, per substep, under which a
 44	// contact is resting: no restitution, no bounce counted. At least what
 45	// the steepest hill adds in a substep, so a ball a hill presses into a
 46	// rail slides along it instead of jittering.
 47	RestSpeed = 0.35
 48	// MaxBounce is the most restitution a passive piece plays: a Bounce
 49	// above it is played at it.
 50	MaxBounce = 0.75
 51	// MaxKick is the most restitution a bumper (a Bounce above 1 on a skin
 52	// that Kicks) plays, and the most course.Decode accepts. The ball stays
 53	// under SpeedCap.
 54	MaxKick = 1.5
 55)
 56
 57// The flight rules: a ball takes off over a hill's crest (its uphill edge)
 58// when v² > G·CrestRadius, v its speed up the hill, flies a ballistic arc and
 59// lands at the height it took off from.
 60const (
 61	// CrestRadius is the radius of a hill's lip, in board units: a ball
 62	// needs sqrt(G·CrestRadius), 0.71 per substep, to take off.
 63	CrestRadius = 0.5
 64	// MinRamp is the gentlest hill (|Vec|) that launches.
 65	MinRamp = 0.12
 66	// GroundBounce is the restitution of the ground when a ball lands.
 67	GroundBounce = 0.4
 68	// LandFriction is the Coulomb µ between a landing ball and the ground.
 69	LandFriction = 0.3
 70	// HopSpeed is the vertical speed, per substep, under which a landing
 71	// ball stays down.
 72	HopSpeed = 0.25
 73	// MaxSin is the steepest grade a hill plays, sin θ (about 72°), so tan θ
 74	// and a flight stay bounded; course.Decode holds |Vec| to G·MaxSin.
 75	MaxSin = 0.95
 76)
 77
 78// MaxRollOn caps the substeps a ball may roll on past its stroke's own while
 79// a slope keeps it moving or it is still in the air.
 80const MaxRollOn = 120
 81
 82// pinCheck and pinMove: a ball rolling on that moves less than pinMove in
 83// pinCheck substeps is pinned, and stops.
 84const (
 85	pinCheck = 8
 86	pinMove  = 0.1
 87)
 88
 89// MaxWork caps what one stroke may cost, whatever the field, in work units
 90// of about a thousand gas each (Shot.Work): a stroke that reaches it ends
 91// where the ball is. A Field's Cap lowers it for one stroke.
 92const MaxWork = 1000000
 93
 94// The work weights, in units, measured on the GnoVM with probes that add one
 95// kind of piece at a time, so that a unit costs at most 0.87K gas.
 96const (
 97	substepWork  = 700
 98	moveWork     = 15
 99	barWork      = 12
100	broadWork    = 18
101	wallTestWork = 130
102	postTestWork = 50
103	zoneWork     = 20
104	inWork       = 25
105	edgeWork     = 14
106	roundWork    = 12
107	pushWork     = 50
108	rootWork     = 200
109	unstickWork  = 5
110)
111
112// MaxWorkStep is the most a stroke's Work passes its limit (MaxWork, or a
113// lower Cap) by on a Decode-limited field: the most between two checks.
114// Golf holds a shot to its limit plus this (shotBound).
115const MaxWorkStep = 225000
116
117// limit is the work the stroke may cost: MaxWork, or the field's Cap under it.
118func (f *Field) limit() int {
119	if f.Cap > 0 && f.Cap < MaxWork {
120		return f.Cap
121	}
122	return MaxWork
123}
124
125// maxHits is the most contacts one move resolves (a corner meets two).
126const maxHits = 4
127
128// Shot is everything a stroke did: where the ball was at every substep, and
129// how many walls and posts it met. A renderer replays Path and never
130// simulates; two consecutive points far apart are a tunnel, not a line to
131// interpolate.
132type Shot struct {
133	Path    []Vec2
134	Bounces int
135	// Air marks the path points where the ball is off the ground (no zone
136	// acts, no cup takes it). Same length as Path.
137	Air []bool
138	// Cause says, per path point, what most acted on the ball in the substep
139	// that ended there: 'b' a bounce, 's' a hill, 'w' wind or a gust, 'i' a
140	// slippery surface, '-' nothing but friction. Same length as Path.
141	Cause []byte
142	why   byte // this substep's cause so far
143	// Work is what the stroke cost, in MaxWork's units.
144	Work int
145	// start is where the stroke was played from, where a hazard sends it.
146	start Vec2
147}
148
149func (s *Shot) add(p Vec2, air bool) {
150	s.Path = append(s.Path, p)
151	s.Air = append(s.Air, air)
152	s.Cause = append(s.Cause, '-')
153}
154
155// mark records c as this substep's cause if it outranks what is there.
156func (s *Shot) mark(c byte) {
157	if strings.IndexByte(causes, c) > strings.IndexByte(causes, s.why) {
158		s.why = c
159	}
160}
161
162// causes is the Cause letters, weakest first.
163const causes = "-iwsb"
164
165// The loop's mouth: a ball heading in within 45° (loopIn, cos 45°) enters,
166// one more than 25° off straight (loopStraight, cos 25°) is set down loopDrop
167// in front, loopFront outside the mouth; one too slow rolls back out at
168// loopBack of its speed.
169const (
170	loopIn       = 0.7071
171	loopStraight = 0.9063
172	loopFront    = 0.01
173	loopDrop     = 1.5
174	loopBack     = 0.5
175)
176
177// Rest is where the ball stopped.
178func (s Shot) Rest() Vec2 {
179	if len(s.Path) == 0 {
180		return Vec2{}
181	}
182	return s.Path[len(s.Path)-1]
183}
184
185// MaxMove is the longest single move inside a substep. Zones are sampled once
186// per move, so a zone must be at least this deep along the way a ball crosses
187// it, or a fast ball can step over it unseen.
188const MaxMove = 1.5
189
190// SpeedCap is the fastest a ball may go, in board units per substep, held at
191// the start of every substep and after every bounce: it bounds a substep to
192// 6 moves, even between two bumpers or down a slope on ice.
193const SpeedCap = 8
194
195// Step rolls a ball through the field until it comes to rest or runs out of
196// substeps (and past them while a slope keeps it rolling or it is still in
197// the air, see MaxRollOn). With a Radius, the ball meets walls and posts with
198// its edge. Cost is O(moves * (walls + posts + zones)), paid on every shot.
199func (f *Field) Step(pos, vel Vec2, substeps int) Shot {
200	out := Shot{Path: make([]Vec2, 0, substeps+2), Air: make([]bool, 0, substeps+2), start: pos}
201	out.add(pos, false)
202	air := 0.0 // substeps left in the air; 0 on the ground
203	vz := 0.0  // the vertical speed the ball took off (or bounced) with
204	hop := 0.0 // a landing's rebound, up again once the zones under it are met
205
206	// wall offsets once per shot (or per hole, Prepare), never per move
207	offs := f.offsets()
208	bars := f.timedBars()
209	slopes := f.groundSlopes()
210
211	extra := 0 // substeps rolled on past the stroke's (MaxRollOn)
212	// the hill being climbed, and how far up it the climb began
213	var ramp *hill
214	rampFrom, from := 0.0, pos
215	// rolling on, a ball a slope pins against a wall only jitters: it stops
216	// once it has not got anywhere in pinCheck substeps
217	base, anchor := substeps, pos
218	// the rolling deceleration the ball meets, per substep: where it starts,
219	// then the last substep's
220	drag := f.resistance(slopes, pos, f.Tick, &out.Work)
221	limit := f.limit()
222	for i := 0; i < substeps; i++ {
223		tick := i + f.Tick
224		if out.Work > limit {
225			break // spent: the ball stops where the last substep left it
226		}
227		if i >= base && (i-base)%pinCheck == 0 {
228			if i > base && air <= 0 && pos.Sub(anchor).Len() < pinMove {
229				break
230			}
231			anchor = pos
232		}
233		if i == substeps-1 && f.rollsOn(extra, air, 0, slopes, pos, &out.Work) { // hop 0: the check at the substep's end sees a pending one
234			substeps++
235			extra++
236		}
237		// A timed bar that comes back (or stands, on the first substep) on
238		// the ball pushes it out (UnstickIn). Every bar is pushed before the
239		// work is checked, so a stroke never ends inside one.
240		out.Work += barWork * len(bars)
241		for _, j := range bars {
242			w := &f.Walls[j]
243			if There(tick, w.Every, w.On, w.Phase) && (i == 0 || !There(tick-1, w.Every, w.On, w.Phase)) {
244				pos = UnstickIn(pos, f.Walls[j:j+4], nil, f.Radius, f.Walls)
245				out.Work += 4 * unstickWork * len(f.Walls)
246			}
247		}
248		spent := out.Work > limit
249		if spent {
250			if pos != out.Rest() {
251				out.add(pos, false)
252			}
253			break
254		}
255		// moves of at most MaxMove, so a fast ball meets every zone and wall
256		// it crosses; the path still gets one point per substep
257		speed := vel.Len()
258		if speed > SpeedCap {
259			vel, speed = vel.Scale(SpeedCap/speed), SpeedCap
260		}
261		// Rolling resistance as two half-kicks around the moves (velocity
262		// Verlet, so a ball rolls v²/2a), the first at the last substep's
263		// deceleration. It never reverses the ball: it stops it.
264		if air <= 0 && hop <= 0 {
265			if dv := drag / 2; speed > dv {
266				vel, speed = vel.Scale((speed-dv)/speed), speed-dv
267			} else {
268				vel, speed = Vec2{}, 0
269				// at rest where no hill can move it, it stays (no creeping)
270				if !f.rolls(slopes, pos, tick, &out.Work) {
271					if pos != out.Rest() {
272						out.add(pos, false) // where a timed bar put it
273					}
274					break
275				}
276			}
277		}
278		n := int(speed/MaxMove) + 1
279		dt := 1 / float64(n)
280		roll := 0.0 // the rolling deceleration, summed over the moves on the ground
281		out.why = '-'
282		for k := 0; k < n; k++ {
283			if out.Work > limit {
284				spent = true // the stroke has cost all it may: it ends here
285				break
286			}
287			out.Work += moveWork
288			// in the air no zone acts: the ball flies over water, sand, a
289			// tunnel's mouth
290			var up *hill
291			if air <= 0 {
292				// grass unless a zone under this move says otherwise
293				surface := 1.0
294				var under *hill
295				if f.zonesAt(&pos, &vel, &surface, &under, slopes, n, tick, &out) {
296					return out
297				}
298				if surface > 1 {
299					out.mark('i')
300				}
301				if hop > 0 {
302					// a landing's rebound, once the ground had its say
303					// (water under it still catches it)
304					vz, air, hop = hop, 2*hop/G, 0
305				} else {
306					c := 1.0
307					if under != nil {
308						c = under.cos
309					}
310					roll += Rolling(f.Friction, surface) * G * c
311				}
312				up = under
313				if up != nil && vel.Dot(up.z.Vec) >= 0 {
314					up = nil // on the hill, but not climbing it
315				}
316				if up != ramp {
317					// a new climb counts from where the ball came onto the hill
318					ramp = up
319					if up != nil {
320						rampFrom = up.along(from)
321					}
322				}
323			}
324			from = pos
325
326			// contact to contact: a hit spends the rest of the move from there
327			hit, left := false, 1.0
328			for h := 0; h < maxHits && left > 0; h++ {
329				if out.Work > limit {
330					break // spent: the ball stops where it is
331				}
332				move := vel.Scale(left * dt)
333				next := pos.Add(move)
334				best, normal, bounce, ok := f.sweep(offs, pos, next, move, tick, &out)
335				if !ok {
336					pos = next
337					break
338				}
339				const skin = 1e-6
340				pos = pos.Add(move.Scale(best)).Add(normal.Scale(skin))
341				var impact bool
342				vel, impact = contact(vel, normal, bounce)
343				out.Work += rootWork // contact's
344				if vel.LenCmp(SpeedCap) > 0 {
345					out.Work += rootWork
346					vel = vel.Scale(SpeedCap / vel.Len()) // two bumpers facing each other
347				}
348				if impact {
349					out.Bounces++
350					out.mark('b')
351				}
352				hit = true
353				left *= 1 - best
354			}
355			if air > 0 {
356				if air -= dt; air <= 0 {
357					// landing: restitution up, Coulomb friction along
358					air = 0
359					j := (1 + GroundBounce) * vz
360					if l := vel.Len(); l > 0 {
361						dv := LandFriction * j
362						if s := TangentMass * l; dv > s {
363							dv = s
364						}
365						vel = vel.Scale((l - dv) / l)
366					}
367					if vz *= GroundBounce; vz > HopSpeed {
368						hop = vz
369					}
370				}
371			} else if up != nil && !hit && !f.climbs(slopes, pos, vel, tick, &out.Work) && up.overTheTop(from, pos, rampFrom, &out.Work) {
372				// over the crest faster than its curve holds: take off
373				if vu := vel.Dot(up.u); vu > 0 && vu*vu > G*CrestRadius {
374					vz = vu * up.tan
375					air = 2 * vz / G
376				}
377			}
378		}
379		out.add(pos, air > 0)
380		out.Cause[len(out.Cause)-1] = out.why
381		if spent {
382			break
383		}
384		// airborne, a landing to bounce, or onto a hill by now: go on too
385		if i == substeps-1 && f.rollsOn(extra, air, hop, slopes, pos, &out.Work) {
386			substeps++
387			extra++
388		}
389		out.Work += substepWork
390		if air > 0 || hop > 0 {
391			drag = 0
392			continue // no rolling resistance off the ground
393		}
394
395		// the second half-kick, at the moves' mean deceleration on the ground
396		drag = roll * dt
397		l := vel.Len()
398		if dv := drag / 2; l > dv {
399			vel = vel.Scale((l - dv) / l)
400			continue
401		}
402		vel = Vec2{}
403		// on a hill steeper than its resistance, it rolls back next substep
404		if f.rolls(slopes, pos, tick+1, &out.Work) {
405			continue
406		}
407		break
408	}
409
410	// a ball at rest is on the ground, whatever was left of its flight
411	out.Air[len(out.Air)-1] = false
412	// a ball at rest in water or a tunnel's mouth is resolved now
413	var still Vec2
414	surface := 1.0
415	var under *hill
416	if f.zonesAt(&pos, &still, &surface, &under, slopes, 1, substeps+f.Tick, &out) {
417		return out
418	}
419	// a ball at rest in a timed hazard or tunnel meets it whatever the tick:
420	// it would be there when it next comes on
421	for j := range f.Zones {
422		z := &f.Zones[j]
423		out.Work += zoneWork
424		if z.Every <= 0 || z.On <= 0 || (z.Kind != Hazard && z.Kind != Tunnel) || !z.in(pos, &out.Work) {
425			continue
426		}
427		if z.Kind == Hazard {
428			out.add(out.start, false)
429			return out
430		}
431		pos = z.Vec
432		out.add(pos, false)
433	}
434	return out
435}
436
437// contact applies the contact impulse to vel against a surface of unit
438// normal n (toward the ball): restitution along n, Coulomb friction along
439// the surface. It reports an impact (not a resting contact).
440func contact(vel, n Vec2, bounce float64) (Vec2, bool) {
441	vn := vel.Dot(n)
442	if !(vn < 0) {
443		return vel, false // not moving into it
444	}
445	e := bounce
446	if e > 1 {
447		if e > MaxKick {
448			e = MaxKick
449		}
450	} else if e > MaxBounce {
451		e = MaxBounce
452	}
453	impact := -vn >= RestSpeed
454	if !impact {
455		e = 0
456	}
457	jn := -(1 + e) * vn // the normal impulse, per unit mass
458	vt := vel.Sub(n.Scale(vn))
459	if lt := vt.Len(); lt > 0 {
460		jt := WallFriction * jn
461		if s := TangentMass * lt; jt > s {
462			jt = s
463		}
464		vt = vt.Scale((lt - jt) / lt)
465	}
466	return vt.Add(n.Scale(-e * vn)), impact
467}
468
469// sweep is the first wall or post the move pos→next meets: how far along it
470// (0 to 1), the surface's normal there (toward the ball), and its bounce.
471func (f *Field) sweep(offs []offset, pos, next, move Vec2, tick int, out *Shot) (float64, Vec2, float64, bool) {
472	const skin = 1e-6
473	out.Work += broadWork * (len(offs) + len(f.Posts))
474	// broad phase: skip a wall or post whose box the move's box misses
475	lo, hi := pos, next
476	if lo.X > hi.X {
477		lo.X, hi.X = hi.X, lo.X
478	}
479	if lo.Y > hi.Y {
480		lo.Y, hi.Y = hi.Y, lo.Y
481	}
482	best, normal, restitution, hit := 1.0, Vec2{}, f.Bounce, false
483	for j := range offs {
484		o := &offs[j]
485		if o.hi.X < lo.X || o.lo.X > hi.X || o.hi.Y < lo.Y || o.lo.Y > hi.Y {
486			continue
487		}
488		if !There(tick, o.every, o.on, o.phase) {
489			continue // a timed wall that is not there this substep
490		}
491		out.Work += wallTestWork
492		side := pos.Sub(o.a).Dot(o.n)
493		seg, sn, facing := o.plus, o.pn, o.n
494		if side < 0 {
495			seg, sn, facing = o.minus, o.mn, o.n.Scale(-1)
496		}
497		// Already within the radius (a corner, a wall that moved onto the
498		// ball): the offset line is behind it, so meet the wall now.
499		if f.Radius > 0 {
500			d := side
501			if d < 0 {
502				d = -d
503			}
504			if d <= f.Radius+skin/2 {
505				along := Vec2{o.n.Y, -o.n.X}
506				u := pos.Sub(o.a).Dot(along) // along the wall, from A toward B
507				if u >= 0 && u <= o.length {
508					if move.Dot(facing) < 0 {
509						best, normal, hit = 0, facing, true
510						restitution = o.bounce
511					}
512					continue
513				}
514				// past an end, the ball meets the end point, not the face
515				// (whose normal points across a corner's next wall):
516				// touching it, push back from it; else sweep it as a cap
517				end := o.a
518				if u > o.length {
519					end = o.a.Add(along.Scale(o.length))
520				}
521				rv := pos.Sub(end)
522				out.Work += rootWork
523				if l := rv.Len(); l > 0 && l <= f.Radius+skin/2 {
524					if nrm := rv.Scale(1 / l); move.Dot(nrm) < 0 {
525						best, normal, hit = 0, nrm, true
526						restitution = o.bounce
527					}
528				} else if t, nn, ok := (Circle{C: end, R: f.Radius}).hit(pos, next, &out.Work); ok && t < best {
529					best, normal, hit = t, nn, true
530					restitution = o.bounce
531				}
532				continue
533			}
534		}
535		if t, nn, ok := seg.hitN(pos, next, sn); ok && t < best {
536			best, normal, hit = t, nn, true
537			restitution = o.bounce
538		}
539		// the offset lines' square ends leave a gap at an acute corner's
540		// tip: sweep both ends as round caps too (inside the square ones)
541		if f.Radius > 0 && o.length > 0 {
542			along := Vec2{o.n.Y, -o.n.X}
543			for _, end := range [2]Vec2{o.a, o.a.Add(along.Scale(o.length))} {
544				if t, nn, ok := (Circle{C: end, R: f.Radius}).hit(pos, next, &out.Work); ok && t < best {
545					best, normal, hit = t, nn, true
546					restitution = o.bounce
547				}
548			}
549		}
550	}
551	for j := range f.Posts {
552		p := &f.Posts[j]
553		c := p.Circle
554		c.R += f.Radius
555		if reach := c.R + boxPad; c.C.X+reach < lo.X || c.C.X-reach > hi.X || c.C.Y+reach < lo.Y || c.C.Y-reach > hi.Y {
556			continue
557		}
558		out.Work += postTestWork
559		if t, nn, ok := c.hit(pos, next, &out.Work); ok && t < best {
560			best, normal, hit = t, nn, true
561			restitution = bounceOf(p.Bounce, f.Bounce, p.Skin)
562		}
563	}
564	return best, normal, restitution, hit
565}
566
567// zonesAt applies the zones the ball is in, and reports whether one ended
568// the shot. The first ground Slope there is the hill (left in *under);
569// moving air pushes on top of it.
570func (f *Field) zonesAt(pos, vel *Vec2, surface *float64, under **hill, slopes []hill, n, tick int, out *Shot) bool {
571	*under = nil
572	hk := 0 // the next hill of slopes, in field order
573	for j := range f.Zones {
574		z := &f.Zones[j]
575		ground := z.Kind == Slope && !z.Air
576		var h *hill
577		if ground {
578			if hk < len(slopes) {
579				h = &slopes[hk]
580			}
581			hk++
582		}
583		out.Work += zoneWork
584		if !There(tick, z.Every, z.On, z.Phase) || !z.in(*pos, &out.Work) {
585			continue
586		}
587		switch z.Kind {
588		case Surface:
589			*surface = z.Scale // the surface under this move (the last listed wins)
590		case Slope:
591			if ground {
592				if *under != nil || h == nil {
593					continue // already on a hill: the first one is the ground
594				}
595				*under = h
596				out.mark('s')
597				out.Work += pushWork
598				*vel = vel.Add(z.Vec.Scale(1 / float64(n)))
599				continue
600			}
601			out.mark('w')
602			out.Work += pushWork
603			wind := z.Capped
604			v2 := 0.0
605			if wind {
606				v2 = vel.Dot(*vel)
607			}
608			*vel = vel.Add(z.Vec.Scale(1 / float64(n)))
609			// capped wind never speeds the ball up (on ice it would keep a
610			// ball drifting); squared first, rooted only when it did
611			if l2 := vel.Dot(*vel); wind && l2 > v2 {
612				out.Work += 2 * rootWork // two roots
613				if v0, l := math.Sqrt(v2), math.Sqrt(l2); l > v0 {
614					if v0 == 0 {
615						*vel = Vec2{}
616					} else {
617						*vel = vel.Scale(v0 / l)
618					}
619				}
620			}
621		case Tunnel:
622			*pos = z.Vec
623			out.add(*pos, false)
624		case Hazard:
625			out.add(out.start, false) // back where the stroke was played from
626			return true
627		case Loop:
628			// decided at once: the ball is put out of the mouth, so it never
629			// has to remember being inside
630			axis, sign := Vec2{1, 0}, 1.0
631			mid := z.Min.Add(z.Max).Scale(0.5)
632			if math.Abs(z.Vec.Y-mid.Y) > math.Abs(z.Vec.X-mid.X) {
633				axis = Vec2{0, 1}
634			}
635			if z.Vec.Dot(axis) < mid.Dot(axis) {
636				sign = -1
637			}
638			in := axis.Scale(sign) // the way into the loop
639			out.Work += rootWork
640			sp := vel.Len()
641			if vel.Dot(in) <= sp*loopIn {
642				continue // not heading into the mouth: it rolls across
643			}
644			// more than 25° off straight
645			slanted := vel.Dot(in) <= sp*loopStraight
646			front := z.Min.Dot(axis) - loopFront
647			if sign < 0 {
648				front = z.Max.Dot(axis) + loopFront
649			}
650			side := pos.Sub(axis.Scale(pos.Dot(axis)))
651			switch {
652			case slanted:
653				// off the side of the track: down in front of the mouth, still
654				*pos = mid.Sub(axis.Scale(mid.Dot(axis))).Add(axis.Scale(front - sign*loopDrop))
655				out.add(*pos, false)
656				return true
657			case sp >= z.Scale:
658				*pos = z.Vec
659				*vel = in.Scale(sp * LoopKeep)
660			default:
661				*pos = side.Add(axis.Scale(front))
662				*vel = vel.Scale(-loopBack)
663			}
664			out.add(*pos, false)
665		}
666	}
667	return false
668}
669
670// bounceOf is the Bounce a piece plays: its own, or the field's (def), and
671// above 1 only for a bumper (Kicks); any other plays MaxBounce.
672func bounceOf(b, def float64, skin string) float64 {
673	if b = orDefault(b, def); b > 1 && !Kicks(skin) {
674		return MaxBounce
675	}
676	return b
677}
678
679// Kicks reports whether a piece of this skin may kick the ball back faster
680// than it came (a Bounce above 1). Every other piece is passive.
681func Kicks(skin string) bool {
682	return strings.Contains(skin, "bumper") || strings.Contains(skin, "pinball") || strings.Contains(skin, "mushroom")
683}
684
685func orDefault(v, def float64) float64 {
686	if v == 0 {
687		return def
688	}
689	return v
690}
691
692// hill is a ground slope: its steepness g = |Vec| (G·sin θ), cos θ, tan θ,
693// u the unit uphill, and its crest, the zone's edge u mostly points at.
694type hill struct {
695	z        *Zone
696	g        float64
697	cos, tan float64
698	u        Vec2
699	onX      bool    // the crest is an edge across X (u mostly along X)
700	crest    float64 // where it is, on that axis
701	foot     float64 // the opposite edge, where a climb starts
702	depth    float64 // foot to crest
703	upward   float64 // +1 when uphill runs toward +X (or +Y), -1 the other way
704}
705
706func newHill(z *Zone) hill {
707	h := hill{z: z, g: z.Vec.Len(), cos: 1}
708	if h.g == 0 {
709		return h // decor: never climbed
710	}
711	sin := h.g / G
712	if sin > MaxSin {
713		sin = MaxSin
714	}
715	h.cos = math.Sqrt(1 - sin*sin)
716	h.tan = sin / h.cos
717	h.u = z.Vec.Scale(-1 / h.g)
718	h.onX = math.Abs(h.u.X) >= math.Abs(h.u.Y)
719	lo, hi, dir := z.Min.Y, z.Max.Y, h.u.Y
720	if h.onX {
721		lo, hi, dir = z.Min.X, z.Max.X, h.u.X
722	}
723	h.crest, h.foot, h.upward = hi, lo, 1
724	if dir < 0 {
725		h.crest, h.foot, h.upward = lo, hi, -1
726	}
727	h.depth = hi - lo
728	return h
729}
730
731// along is how far up the hill p is from its foot (0 at the foot or below).
732func (h *hill) along(p Vec2) float64 {
733	c := p.Y
734	if h.onX {
735		c = p.X
736	}
737	if d := (c - h.foot) * h.upward; d > 0 {
738		return d
739	}
740	return 0
741}
742
743// JumpRun is the share of a hill's depth a ball must have climbed to take off
744// at its top: one that only clipped the hill near its crest has not ridden it.
745const JumpRun = 0.5
746
747// overTheTop reports whether the move a→b left the hill through its crest
748// (not a side or its foot) after climbing at least JumpRun of it from
749// rampFrom. A hill no steeper than MinRamp launches nothing.
750func (h *hill) overTheTop(a, b Vec2, rampFrom float64, w *int) bool {
751	if h.g <= MinRamp || h.z.in(b, w) {
752		return false
753	}
754	ac, bc, oa, ob := a.Y, b.Y, a.X, b.X
755	lo, hi := h.z.Min.X, h.z.Max.X
756	if h.onX {
757		ac, bc, oa, ob = a.X, b.X, a.Y, b.Y
758		lo, hi = h.z.Min.Y, h.z.Max.Y
759	}
760	if (bc-h.crest)*h.upward < 0 || ac == bc {
761		return false // it did not get past the crest's line
762	}
763	// where the move crosses the crest's line must be on the crest
764	if o := oa + (h.crest-ac)/(bc-ac)*(ob-oa); o < lo || o >= hi {
765		return false
766	}
767	return h.depth-rampFrom >= JumpRun*h.depth
768}
769
770// groundSlopes is the field's hills, in field order, measured once per shot.
771func (f *Field) groundSlopes() []hill {
772	var out []hill
773	for j := range f.Zones {
774		// air is no hill; a timed hill counts while it is there
775		if z := &f.Zones[j]; z.Kind == Slope && !z.Air {
776			out = append(out, newHill(z))
777		}
778	}
779	return out
780}
781
782// ground is the hill under the ball at tick, as zonesAt has it; nil off
783// every hill. A tick below 0 asks for the untimed hills only.
784func (f *Field) ground(slopes []hill, pos Vec2, tick int, w *int) *hill {
785	for j := range slopes {
786		s := &slopes[j]
787		*w += zoneWork
788		if (tick < 0 && s.z.Every > 0) || (tick >= 0 && !There(tick, s.z.Every, s.z.On, s.z.Phase)) {
789			continue
790		}
791		if s.z.in(pos, w) {
792			return s
793		}
794	}
795	return nil
796}
797
798// climbs reports whether the ball is moving up the hill under it.
799func (f *Field) climbs(slopes []hill, pos, vel Vec2, tick int, w *int) bool {
800	h := f.ground(slopes, pos, tick, w)
801	return h != nil && vel.Dot(h.z.Vec) < 0
802}
803
804// resistance is the rolling deceleration Crr·G·cos θ at pos at tick. A tick
805// below 0 asks for the untimed zones only.
806func (f *Field) resistance(slopes []hill, pos Vec2, tick int, w *int) float64 {
807	t := tick
808	if t < 0 {
809		t = 0
810	}
811	surface := 1.0
812	for j := range f.Zones {
813		*w += zoneWork
814		if z := &f.Zones[j]; z.Kind == Surface && (tick >= 0 || z.Every <= 0) && There(t, z.Every, z.On, z.Phase) && z.in(pos, w) {
815			surface = z.Scale
816		}
817	}
818	c := 1.0
819	if h := f.ground(slopes, pos, tick, w); h != nil {
820		c = h.cos
821	}
822	return Rolling(f.Friction, surface) * G * c
823}
824
825// rollsOn says whether a stroke on its last substep goes on for one more:
826// in the air, a landing to bounce (hop), or on a hill that rolls it.
827func (f *Field) rollsOn(extra int, air, hop float64, slopes []hill, pos Vec2, w *int) bool {
828	return extra < MaxRollOn && (air > 0 || hop > 0 || f.rolls(slopes, pos, -1, w))
829}
830
831// rolls reports whether a ball at rest at pos rolls away at tick (G·sin θ
832// above Crr·G·cos θ). A tick below 0 asks for the untimed hills only: a
833// rocking plank would roll a ball on for all MaxRollOn substeps.
834func (f *Field) rolls(slopes []hill, pos Vec2, tick int, w *int) bool {
835	h := f.ground(slopes, pos, tick, w)
836	return h != nil && h.g > f.resistance(slopes, pos, tick, w)
837}
838
839// in is contains, adding what the test costs to *w (zoneWork is the
840// caller's); every polygon edge is charged as if it straddled p.
841func (z *Zone) in(p Vec2, w *int) bool {
842	if p.X < z.Min.X || p.X >= z.Max.X || p.Y < z.Min.Y || p.Y >= z.Max.Y {
843		return false
844	}
845	*w += inWork
846	if len(z.Poly) >= 3 {
847		*w += edgeWork * len(z.Poly)
848	} else if z.Round {
849		*w += roundWork
850	}
851	return z.contains(p)
852}