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}