Porting catalogue - every built-in drill and game against the Lua API
Status: survey of fw 0.8.508 (2026-09-14) and the v0.1 API. This is the
examination the API was designed from: for each of the seven drills and two
games, what it consumes, how its round works, what it scores, draws, says and
persists, and which API calls a Lua port needs. Section 11 is the common
functionality the survey found; section 12 lists the API decisions the
survey forced. File and line references are into
foodoublebassesp32/src/ at 0.8.508.
Legend for "port": every bullet names an API call from api-reference.md.
"Gap" means something the v0.1 API had to add for this port.
1. Face angle (FaceAngleReduxGame, gameMode 22)
| Input | faceAngleBall only; every putt counts (no left-handed rule). Five fields cached for the widget. |
| Round | Fixed 10 putts (not shots-per-drill). Resumable in memory (HOME keeps the picture; Play restarts). |
| Score | FaceAngleReduxScore: quantise to 0.1 deg, factor exp(-sqrt(q / 2)) (1.0 at zero), score = 10 x mean over the putts so far. 10.0 all straight, 8.0 at 0.1, 4.9 at 1.0, 1.1 at 10. Trend key redux, recorded at the results page. |
| Settings | None. No Settings row. |
| Picture | Full-screen canvas, black. A circle edge whose sagitta grows with the envelope min(lo,0)..max(hi,0) (60 arc-degrees per degree), history of earlier edges as fading arcs (step 12 px, opacity 70), radial construction lines (dashed for the zero line), per-putt dots (newest orange 0xFF8C00), a blue 0x44AAFF rolling 3-putt average polyline. Labels: title 18 pt top-left; two 32 pt extreme values whose y tracks the edge ends; a third 32 pt "0" at the zero line; centre "Score" / value / "7/10". 600 ms ease-out animation per putt at 30 ms frames. Combined widget bottom-right, rebuilt per putt. |
| Audio | Owns audio (shot_speech = none); speaks the face angle after the redraw; no stimp in the opener; game-over tone at the end. |
| Keys | HOME/OK exit to the menu; UP/DOWN/BACK nothing; on results any key restarts. |
| Results | "Face angle" / "Drill over", rows Score 7.5/10, Range L1.2 to R0.4, 10 s restart. |
Port
- foogolf.drill{ speaks_stimp = false, shot_speech = "none", trend = {label = "Face angle"} }
- canvas.begin(0), canvas.arc, canvas.dashed_line, canvas.line, canvas.disc, canvas.invalidate
- hud.title, hud.label x 6, hud.move (the value labels follow the edge)
- hud.widget(shot)
- audio.say_face_angle(shot.face_deg) from the tick after the redraw; audio.tone("game_over")
- on_tick for the 600 ms ease (util.ease_out_cubic)
- trends.record(score), ui.results{...}
- Gap: hud.move (labels that track a picture), canvas.arc with sub-pixel width and opacity.
The scoring formula itself is ten lines of Lua; the pure C++ class has a
native test (test/test_face_angle_redux) whose table doubles as the
script's test vector.
2. Face rotation consistency (FaceRotationGame, gameMode 19)
| Input | faceRotationRateDegPerInch (from the last shot's saved rate). A rate above +0.20 deg/in (opening) is ignored: spoken "negative", no dot, no count. |
| Round | Fixed 10 counted putts. Resumable in memory. |
| Score | ConsistencyScore (shared with Arc strength): start 10.0; from putt 2, excess = abs(rate - mean of prior) - 0.15; deduct excess / 0.15 capped at 2.0; floor 0. Trend key frot. |
| Settings | None. |
| Picture | Same circle-edge renderer as Face angle with 120 arc-degrees per deg/in, pinned to the raw extremes (no zero line), pink 0xFF4FA0 rolling average. Widget bottom-right, rebuilt even on an ignored putt. |
| Audio | Owns audio, no stimp; speaks the rate ("point three five") or "negative"; game-over tone. |
| Keys / Results | As Face angle; rows Score, Range (-.08 to .35). |
Port
- Identical shape to Face angle: the same 200-line drawing module shared by both scripts (open-source scripts can share code by copy; there is no require).
- shot.rotation; audio.say_rotation(rate); audio.say_cue("negative").
- Gap: none beyond Face angle's.
3. Arc strength consistency (ArcStrengthGame, gameMode 10)
| Input | The same rotation rate and the same "ignore above +0.20" rule (an ignored putt does not update the widget here - a difference from Face rotation). |
| Round | Fixed 10 (the shots-per-drill row was dropped because the drill never read it). Resumable in memory. |
| Score | The same ConsistencyScore. Trend key arc. |
| Settings | One: Appearance arcLook = Classic / Stripes / Clean (default Stripes), NVS + guest cfg, live rebuild of the page. |
| Picture | Stripes/Clean: header green 0x1B4D1F, four stripe bands from the SBST / Slight / Medium boundaries (0.20 / 0.70 / 1.25 deg/in, 90 px per deg/in), a mirrored lighter negative band, band-name labels 14 pt, dashed guide lines and left-edge tangent arrows (Stripes only), the putter clubhead image at zoom 18 beside a 12 px ball with a two-disc halo, arcs through the apex (newest yellow 0xFFE400 5 px, older 0xB39F00 2 px). Classic: light blue 0x44AAFF background, black ruler with ticks, top-right text stack. No per-shot easing (accumulating arcs). Widget bottom-right in the Stripes family only. |
| Audio | Owns audio; stimp IS spoken (inherits the default); rate or "negative"; game-over tone. |
| Keys / Results | As above; rows Score, Average +0.12. |
Port
- settings.choice{ key = "look", options = {"Classic","Stripes","Clean"}, default = 2 }, on_setting rebuilds.
- canvas.rect bands, canvas.dashed_line, canvas.polygon arrowheads, canvas.image("putter_clubhead", x, y, 18, 0), canvas.arc for the arcs (the C++ draws them column by column; canvas.arc with a large radius is the same curve).
- hud.widget(shot) only when settings.look ~= 1.
- Gap: canvas.image with zoom (the putter asset), canvas.polygon.
4. Swing path and lie (RailGunGame, gameMode 3)
| Input | leftSensorsInnerProportion, rightSensorsInnerProportion only; speed and face are shown, never judged. |
| Round | App::shotsPerDrill() shots (3..20, default 10). Not resumable. |
| Score | +1 when both arrow ends lie inside the chevron band, -1 otherwise (can go negative). Trend key path. |
| Settings | Skill level (sets the arrow sensitivity 2/5/8/12), Shots per drill - both device-wide rows. |
| Picture | Grass texture full screen, eleven white chevrons on a raised band 0x2E8B12 with a shadow, an 8 px arrow (green 0x00FF00 hit / red miss) whose ends are 85 + (prop - 0.5) * 80 * sensitivity/2, speed 48 pt (mph or ft), face angle 48 pt, score 48 pt, the tick/cross row. Result held 600 ms. No widget: this drill IS the widget at full size. |
| Audio | Device shot speech stays ON (the only drill besides Close-in); hit / miss tones; game-over tone. |
| Keys / Results | HOME/OK exit; rows Score, Difficulty (skill name). |
Port
- foogolf.drill{ shot_speech = "device" }, settings.shared("skill_level", "shots_per_drill"), device.shots_per_drill(), device.path_lie_sensitivity(), device.skill_name().
- canvas.grass(0, 170, ...) or canvas.scenery, canvas.polygon chevrons, canvas.line arrow, hud.value, hud.indicators, hud.widget_text(shot) for the speed/face strings.
- audio.tone("hit"|"miss").
- Gap: canvas.grass (the Grass settings texture), hud.widget_text.
5. Lag putting (ContinuousSwingGame, gameMode 9) and Metrics only (gameMode 0)
| Input | Speed + face for physics; the five widget fields. |
| Round | shots_per_drill stations, up to 3 putts each; a station ends on a good putt (+1) or after three. One random distance per station in [csMin, csMax] (4..120, default 4..100), re-rolled at station end unless UP/DOWN froze it ("manual"). Metrics only: no stations, no score, the target re-rolls every three putts forever. |
| Physics | Rollout by the shared model; resting position closed-form on the straight line; holed by wouldBallBeHoled; in real-time mode the rolling page's own verdict wins. Good = holed or inside 3 ft. |
| Animation | Feedback real time: the perspective green rolls with the cup at the target. Instant: the settled view at once. Settled view = the green with a faint flag and no cup, a sight reticle over the ball (green when good), no history; a new distance zooms in over 850 ms. |
| Score | Stations won. Trend key lag. Metrics only records nothing. |
| Settings | Skill level, Min distance, Max distance, Stimp, Impact ratio. Guest cfg csMin csMax manual target. |
| Picture | Green 0x1A6004, title 18 pt, distance 48 pt top-right, tick/cross row, widget bottom-right. |
| Audio | Owns audio; opener speaks the name then the distance; per putt the dominant-miss word (good / left / right / short / long) then the next distance when it changed; UP/DOWN re-speaks the distance after 900 ms quiet; game-over tone. |
| Keys | UP/DOWN +-1 ft (manual mode); HOME/OK exit; results any key restarts. |
| Results | Rows Score 7/10, Putts 23. |
Port
- settings.number x 2, settings.shared("skill_level","stimp","impact_ratio"), device.shots_per_drill().
- green.show{ flag = "faint", halo = false }, green.roll{ instant = device.instant_feedback() } (the default), green.reticle, hud.widget, hud.indicators, hud.value.
- putt.dominant_miss(t) + audio.say_cue, audio.say_distance, timer.after(900, ...) for the settle speech, settings.set("target", ...) (not needed: manual target is session state, kept in a local).
- A metrics_only flag is a second manifest in the same file? No: Metrics only is a separate script sharing the code by copy, trend = nil is not allowed for a drill - so Metrics only is declared as foogolf.drill{ trend = false } (gap: a drill with no trend, allowed only for a metrics view; open whether the built-in Metrics only stays C++).
- Gap: the 850 ms distance zoom (green.show re-render animates the camera when hole_ft changes: added as green.show{ animate_ms = 850 }).
6. Close-in putting (StrokePlayGame Variant::CloseIn, gameMode 15)
| Input | Speed + face; the seat's impact ratio (the player's own, else the device's). |
| Round | 10 one-putt holes on the round engine, roster = current player. Tees dealt as a deck at hole 1 from [ciMin, ciMax] (2..120, default 2..15) so a guest plays the same distances; UP/DOWN retargets the current hole in place and freezes the deck. Never an online round. Resumable. |
| Physics | The rolling page's verdict (holed, final position). Make pattern overlay computed per hole. Spoken tiers: good; short (rollout < distance - cup radius); left/right when a straight putt at this pace would drop; else left/right if abs(x) >= dy, otherwise long. |
| Animation | Perspective green; instant or real time per the Feedback cell; flash gold when holed, green when not; make pattern drawn on the idle view and during the roll; pulsing halo. |
| Score | Net = good - miss (+-1 per hole). Trend key close. |
| Settings | Skill level, Min, Max, Stimp, Impact ratio. Cfg ciMin ciMax ciDist ciD0..2. |
| Picture | "Putt 3/10" 18 pt, distance 48 pt top-right, tick/cross row, widget bottom-right; no seat cards. |
| Audio | Device shot speech stays ON, the verdict word chains behind it; opener speaks the name then the distance; the fanfare at the end (the only drill that does). |
| Keys | UP/DOWN retarget; HOME/OK exit; results any key restarts. |
| Results | "Game over" page: Name +3, Good: 6 of 10, 10 s restart. |
Port
- foogolf.drill{ shot_speech = "device" }; settings.number x 2 with min = 2.
- A local deck: deck = {} of ten math.random(min, max); when the same script is played as a game in Online play the deck would go through round.new{ deck = ... } - but Close-in is a drill, so it is simply local.
- green.show{ hole_ft = deck[h], pattern = putt.make_pattern(deck[h], device.stimp()) }, green.roll{ flash = ..., pattern = ... }, t.cue + audio.say_cue, audio.fanfare().
- hud.title(("Putt %d/%d"):format(h, 10)), hud.value, hud.indicators, hud.widget.
- Gap: putt.make_pattern, the pattern option, putt.cue tiers exposed as data.
7. Make distance (MakeDistanceGame, gameMode 18)
| Input | Speed + face; widget fields. |
| Round | Fixed 10 putts, no target. Not resumable. |
| Physics | Verdict = maxMakeDistanceFt (0 = "Missed"); its own constant-decel roll for the animation, on a straight line; the ball sinks only when it stays inside the strip. A live odometer of feet rolled latches to "Missed" the instant the ball leaves the strip. |
| Animation | Own first-person strip picture (eye height 5.79 ft, anamorphic 15x, horizon at y 85), the camera trailing the ball by 4 ft and tracking it sideways; a rolling frame is memcpy of a static backdrop (scenery above the horizon, the Grass base colour below) plus strip, ruler and ball; roll capped at 5.5 s (long rolls play faster), a 2.6 s result hold, then walk-back after a miss. Instant feedback: the ball drawn at its end, the verdict at once. |
| Score | Round total feet of made putts (a miss banks 0); "Longest" on the results. Trend key make. |
| Settings | Skill level, Hole style (Yellow coat / Cup lines, mdHoleYel), Stimp, Impact ratio. |
| Picture | Title with counter, verdict 48 pt top-centre, running total 18 pt in brackets, ground-painted seven-segment ruler numbers, amber 0xFF8C00 cup rings, widget bottom-right. |
| Audio | Owns audio; speaks the make distance or "missed"; game-over tone after the speech drains. |
| Keys | HOME/BACK exit; OK fast-forwards; results any key restarts. |
| Results | Rows Total 312 ft, Longest 62 ft. |
Port
- putt.make_distance_ft(shot) is the verdict; putt.simulate(shot, {hole_ft = 1e6}) gives the straight roll and t:at(s) drives the ball (the strip has no cup interaction until the end).
- canvas.scenery() + canvas.grass(85, 170) once, canvas.save(); per frame canvas.restore() then strip lines, ruler, cup, ball, canvas.invalidate (full width of the strip only).
- settings.toggle{ key = "coat", on = "Yellow", off = "Cup lines" }, hud.title, hud.label for the verdict/total, hud.widget.
- audio.say_distance(math.floor(d + 0.5)) / audio.say_cue("missed"), audio.tone("game_over").
- Gap: canvas.scenery, canvas.grass, canvas.save/restore, canvas.text with a size list (the seven-segment ruler numerals become canvas.text(..., 8) or stay as line segments).
8. Golf course (StrokePlayGame, gameMode 11; stroke / match / skins)
| Round | 1-4 seats from the active players (+ guests when hosting, four total); 9 holes default (1..18, mpHoles); par 2; tees from the USNDP 18-distance deck shuffled once (default) or random in [putMin, putMax]; honour dealt by OnlineRound::dealHonour (random, alternating devices), later holes fewest-strokes-first, the away ball plays next; no pick-up limit; a leave becomes the next stroke's distance; resumable; a finished round shows the all-players scorecard for 10 s with the fanfare and restarts. |
| Physics | Perspective green show(speed, face, dist, stimp, seat ratio), verdict from the page: holed = gold flash + holed tone; inside 3 ft = green flash + hit tone; else blue 0x44AAFF flash + miss tone. Games always roll. |
| Scoring | Stroke: to-par over completed holes (sat-out holes skipped). Match: pure MatchPlay_* (1 point per hole won). Skins: pure Skins_* (carry, void, leader mask). Solo plays a Par seat. Colours: leading red 0xC0392B, trailing blue 0x2C6FBB. Handicaps: PuttStats_Record per local seat's putt - the only feed on the device. |
| Online | OnlineRound::State JSON <= 1600 B; the host publishes before and after each verdict; guests overwrite their engine and replay other seats' putts; "Putt ignored, waiting for X on Y"; " |
| Picture | Green canvas; "Hole 3/9" (+ "This hole: 2 skins"); distance 32 pt top-right; bottom-left player card (queue above in reverse order, the current seat's one 14 pt line: dot, white hole box, name, coloured score box, "Shot N"); widget bottom-right; classic scorecard page (holes as rows, Feet yellow, Par, SI red, A-D, birdie circles, bogey boxes, Tot/Pts/Skins, Hcp/Rnd) with up to five archived cards. |
| Settings | Skill level, Distances (USNDP / random), Min, Max, Number of holes, Players; Scorecard row; the Games row Scoring cell. |
| Audio | Opener name + stimp, then "hole N . colour . 12 feet" (hole only when changed); tones per verdict; fanfare at the end. |
| Keys | HOME/OK exit; any key on the finished card restarts. |
Port
- foogolf.game{ online = true, scoring = "device", players = "active" }.
- settings.number Min/Max, settings.number holes (1..18), settings.choice Distances, settings.shared("skill_level").
- round.new{ holes = settings.holes, par = 2, tees = settings.dist == 1 and "usndp" or "random" }.
- green.show{ hole_ft = r:remaining_ft(r.turn) }, hud.hole_line(r), hud.value, hud.player_card(r), hud.widget.
- green.roll{ instant = false, flash = ..., on_done = ... } then r:record{...}, audio.tone(...), r:announce_turn(), r:skins_halved() + ui.flash("Skins carry over ...").
- ui.scorecard(r) (Scorecard menu row via settings.action), ui.results_card(r) + audio.fanfare().
- Not portable: the handicap feed (deliberately, section 10.5 of the reference).
- Gap: the whole round module and the four standard cards; settings.action for a Scorecard row.
9. Mini golf (MiniGolfGame, gameMode 23)
| Round | Same engine shape with a course: seed (mgSeed) -> deterministic holes, par 1..3 from a Monte Carlo playability model, up to 18 holes (mgHoles default 9), pick-up at 6 strokes, honour and away-player by route distance; scoring by the Scoring cell (solo plays a Par seat built from each hole's own par); resumable. |
| Physics | Launch angle = explicit aim + face; the shared roll model and cup predicate; walls with restitution 0.65 / 0.92 tangential, 45-degree chamfers, 0.4 ft thick; 2 ms steps, 15 s cap; bounces tick the key sound. |
| Animation | Own canvas: 22 x 9 cells of 2 ft (44 x 18 ft), hole-fit camera (at most 70 px/ft), no zoom during the roll (a user decision), only the ball's patch redrawn per frame; blueprint background (yard grid labelled in feet from the cup) or green; 3-leg aim path with bounce dots; brick walls, mortar, cup, tee ring. |
| Online | The round state extended with g = 23, seed, ball bx/by, per-hole par, shot ang; any tethered device may aim (absolute angle + sequence, host stamps the sender; guests skip their own echo); OK menu (Cancel / Repeat aim / Mulligan) from any device. |
| Picture | HUD strip top ("Hole 3/9 Par 2", turn dot, "Name Shot 2"), score strip at y 150 (dot + score per seat, red when leading), "N ft to cup / wall" bottom-right, big notices in a translucent box ("Holed!", "Hole in one!", "Picked up (6)"). |
| Settings | Number of holes, Regenerate course, Background (Blueprint / Green), Stimp, Players. |
| Audio | Opener, "hole N . colour" (no distance: the player aims first), holed tone, key tick on bounces, game-over tone. |
| Keys | BACK/RIGHT aim 1 deg, UP/DOWN 0.25 deg, OK popup (or fast-forward while rolling), HOME exit. |
Port (v1.1)
- foogolf.game{ online = true }, settings.number holes, settings.action Regenerate (course.seed() re-mint is a runtime call (gap: course.new_seed())), settings.choice Background.
- course.generate(seed, h), course.fit_view, course.draw(hole, view, {background = ...}), course.aim_path, putt.simulate(shot, {course = hole, from = ball, aim_deg = aim}), t:at(s) + canvas.disc + canvas.invalidate for the ball patch, audio.tone("key") per bounce.
- round.new{ holes, par = function(h) return holes[h].par end, tees = nil, pick_up = 6, deck = {seed} }, r:record{ seat, holed, x, y }, r:undo() for mulligan, hud.score_strip(r, 150), ui.popup, ui.box.
- g.state() = { aim = ..., ball = {...} }, g.on_input("aim", t, from), net.send("aim", { deg = a, seq = n }).
- Gap: the whole course module and course.draw; r:undo; hud.score_strip; ui.box; ui.popup; sequenced net.send with sender stamping.
Done. The port shipped in stages and was completed on 2026-09-18: course.*
+ the segment physics (fw 0.8.514), the canvas (0.8.514), round.* with the
score strip, results card and r:undo (0.8.530), ui.popup / ui.box
(0.8.528/0.8.530), the scorecard browser (0.8.533), the device-offered
mulligan (0.8.565) and finally settings.choice for Background, cross-
experience Resume and the shared online round (0.8.570: net.*, g.state() /
g.apply_state(t), g.on_input, shot.remote / seat / aim). The script
is foogolf-ota-cloud/extension-store/extensions/mini_golf.lua, published as
CFZW5. course.draw was never needed - the script draws the course
itself, which is the point of the API. Still not ported from the native game:
nothing.
10. Sim integrations (GSPro Direct, GSPro port 8888, Sim sharing, Garmin R10)
Out of scope for scripting: they forward shots to external software over the network and USB HID, hold sockets and pairing state, and the user has said the USB host transport stays as it is. They are not drills or games in the sense of this API and no script facility is planned for them.
11. Common functionality the survey found
What every experience (or most) does the same way - these became the runtime's shared parts rather than script code.
Lifecycle. All nine implement the same Game contract: activate /
deactivate, forward(msg) (cache the widget fields), onShot(speed, face)
with a null shot meaning "take the screen" (Play, Resume, boot), tick
returning true while busy, forceStop for HOME. Every drill defers a screen
swap out of a key handler with lv_async_call. => hooks in section 2 of the
reference; deferral is the runtime's.
The shot. Every experience reads at most seven numbers of the message:
club speed, face angle at the ball, rotation rate, attack angle (games, for
the wire), the two inner proportions, the impact ratio. Everything else in
SensorMessage::calculations (heights, pivot, turf contact, timing) is
unused by any experience. => the shot table.
The combined widget. Seven of nine build PathLieMini_Build(screen,
BOTTOM_RIGHT, -6, 0, ...) with the same five inputs and the device's speed
display, stimp, impact ratio and skill sensitivity, rebuilt per putt. =>
hud.widget(shot).
Physics. One roll model (computeEsp32DistanceFt), one cup predicate
(isCapturedAtRim), one rim rebound (25 %), used by the perspective page,
the side page, wouldBallBeHoled, maxMakeDistanceFt, MakePattern and
the mini golf stepper. => putt.*.
The perspective green. Lag putting, Close-in and Golf course share
PerspectiveShotPage + PerspectiveGreenScene (frame, drawScene, overlay
hook, halo, flash, reticle, make pattern) and the instant / real-time
switch. => green.*.
Own canvas. Face angle, Face rotation, Arc strength, Make distance and
Mini golf each allocate one 320 x 170 PSRAM canvas and draw with float
anti-aliasing (two of them with a verbatim copy of the same renderer). =>
canvas.* with the AA primitives; one canvas per experience.
Drill scaffolding. DrillCommon: the focus proxy, the results screen
(name + "Drill over" + two rows), the tick/cross indicator row, the 10 s
restart countdown, and Drill_RecordTrend (append + flush). => ui.results,
hud.indicators, trends.record.
Menus. Every experience is one GameMenuSpec row: Play or Resume + New,
then Settings / Trends / About; settings rows are numeric pages, list pages
or in-place toggles whose labels carry the live value; every About page is
the five-section content page. => the manifest, settings.*, about.
Audio. Every experience uses the fixed clip pack through the same
helpers (name + stimp opener, distance, face angle, rotation, the verdict
words, the turn announcement) and the same six tones; the universal rule is
draw, lv_refr_now, then speak from the next tick, and hold busy until the
speech drains (2.5 s cap). => audio.* and the busy rule.
Settings and guest cfg. Every own setting is an NVS key in foogolf,
a GuestMode::noteLocalSettingChange() on a hand change, and an
exportConfig / importConfig pair in the 16-slot integer cfg. =>
settings.* with the same three effects.
Round engine and online. The two games share seats, holes, honour,
away-player, lock-step, the three scorers, the player card, the results
card, the scorecard and the OnlineRound::State relay. => round.*,
hud.player_card, ui.scorecard, ui.results_card, section 13.
Keys. During play HOME and OK leave to the menu in every drill; on every
results page any key but HOME restarts; UP/DOWN are raw only when the page
asks for editing mode. => on_key, keys_editing.
12. API decisions the survey forced
shot_speech = "device" | "none"because two drills keep the device cue and five own their audio; the chaining order ("left two point five" then "good") is the runtime's.speaks_stimpbecause two drills omit it and Arc strength keeps it.trendis mandatory on a drill except for a metrics view (Metrics only), which is whytrend = falseis allowed and documented as a special case.traj.cuecarries the Close-in tiers andputt.dominant_missthe Lag putting rule, because both are policy the built-in drills expose to players and a port must reproduce them exactly.green.show{ pattern = ... }andoverlaybecause Close-in draws the make pattern under the ball and over the roll.hud.movebecause Face angle's value labels track the picture's ends.canvas.save/restorebecause Make distance's frame is a backdrop memcpy plus a few strokes, and Mini golf's patch redraw is what keeps a cast cheap;canvas.invalidatefor the same reason.canvas.image("putter_clubhead", ...)because Arc strength draws the clubhead asset;canvas.scenery/canvas.grassbecause Make distance and Swing path paint the device's scenery and grass.round.new{ deck = ... }because the Close-in deck and the mini golf seed are "host deals, guests adopt" values.r:undo(),ui.popup,ui.box,net.sendwith sequencing because Mini golf's mulligan, OK menu, notices and any-device aiming need them.settings.actionbecause Regenerate course and Scorecard are menu rows that are not values.pick_up,one_puttandpar = functionbecause Mini golf, Close-in and Mini golf again differ from Golf course only in those.- No handicap feed: a user rule, kept as an open decision rather than an API.
13. Facility matrix
| Face angle | Face rot. | Arc str. | Swing path | Lag | Close-in | Make dist. | Golf course | Mini golf | |
|---|---|---|---|---|---|---|---|---|---|
shot.face_deg |
x | shown | x | x | x | x | x | ||
shot.rotation |
x | x | |||||||
shot.path_* |
x | ||||||||
shot.speed_mps |
shown | x | x | x | x | x | |||
putt.simulate / green.roll |
x | x | own strip | x | course | ||||
putt.make_distance_ft |
x | ||||||||
putt.make_pattern |
x | ||||||||
green.* |
x | x | x | ||||||
canvas.* |
x | x | x | x | x | x | |||
hud.widget |
x | x | Stripes | x | x | x | x | ||
hud.indicators |
x | x | x | ||||||
settings.* own |
1 | 2 | 2 | 1 | 4 | 3 | |||
trends.record |
x | x | x | x | x | x | x | ||
round.* |
x | x | |||||||
| online | x | x | |||||||
audio.say* |
face | rate | rate | cue+dist | cue+dist | dist | turn | turn | |
| device shot speech | x | x | |||||||
| fanfare at end | x | x | |||||||
| instant/real time | n/a | n/a | n/a | n/a | x | x | x | always roll | always roll |