@inventure71/paddockjs 1.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +219 -12
  4. package/docs/architecture.md +126 -45
  5. package/docs/concepts.md +9 -5
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +317 -22
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +10 -0
  10. package/docs/learnings.md +22 -0
  11. package/docs/rules.md +71 -14
  12. package/docs/sense_contract.md +59 -0
  13. package/docs/system_specs.md +75 -13
  14. package/docs/training.md +280 -32
  15. package/examples/python/README.md +62 -0
  16. package/examples/python/base_policy_server.py +191 -0
  17. package/examples/python/requirements.txt +2 -0
  18. package/package.json +12 -4
  19. package/src/api/PaddockSimulatorController.js +6 -33
  20. package/src/app/BrowserExpertAdapter.js +76 -9
  21. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  22. package/src/app/F1SimulatorApp.js +291 -1630
  23. package/src/app/banners/bannerFormatters.js +55 -0
  24. package/src/app/banners/raceDataBannerController.js +141 -0
  25. package/src/app/banners/stewardMessageController.js +99 -0
  26. package/src/app/camera/cameraConstants.js +21 -0
  27. package/src/app/camera/cameraController.js +265 -0
  28. package/src/app/camera/cameraGeometry.js +176 -0
  29. package/src/app/domBindings.js +4 -0
  30. package/src/app/readouts/carOverviewRenderer.js +109 -0
  31. package/src/app/readouts/controlStateRenderer.js +37 -0
  32. package/src/app/readouts/raceStatusRenderer.js +133 -0
  33. package/src/app/readouts/readoutFormatters.js +58 -0
  34. package/src/app/readouts/telemetryRenderer.js +113 -0
  35. package/src/app/readouts/timingTowerRenderer.js +177 -0
  36. package/src/app/rendering/appAssets.js +38 -0
  37. package/src/app/rendering/carRenderer.js +286 -0
  38. package/src/app/rendering/displayUtils.js +22 -0
  39. package/src/app/rendering/drsTrailRenderer.js +90 -0
  40. package/src/app/rendering/expertSensorRenderer.js +126 -0
  41. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  42. package/src/app/rendering/replayGhostRenderer.js +124 -0
  43. package/src/app/rendering/trackRenderer.js +25 -0
  44. package/src/app/runtime/frameLoop.js +66 -0
  45. package/src/app/runtime/runtimeTiming.js +34 -0
  46. package/src/app/runtime/runtimeVisibility.js +45 -0
  47. package/src/config/defaultOptions.js +20 -0
  48. package/src/config/restartOptions.js +34 -0
  49. package/src/environment/actions.js +13 -5
  50. package/src/environment/controllerLoop.js +261 -0
  51. package/src/environment/episode.js +98 -2
  52. package/src/environment/evaluation.js +54 -32
  53. package/src/environment/index.d.ts +376 -10
  54. package/src/environment/index.js +1 -0
  55. package/src/environment/metrics.js +159 -0
  56. package/src/environment/observationOptions.js +22 -1
  57. package/src/environment/observationVector.js +200 -0
  58. package/src/environment/observations.js +199 -141
  59. package/src/environment/options.js +37 -5
  60. package/src/environment/recorder.js +13 -6
  61. package/src/environment/rewards.js +13 -2
  62. package/src/environment/runtime.js +272 -23
  63. package/src/environment/scenarios.js +57 -9
  64. package/src/environment/sensors/bodySenses.js +23 -0
  65. package/src/environment/sensors/boundarySenses.js +17 -0
  66. package/src/environment/sensors/carRays.js +75 -0
  67. package/src/environment/sensors/contactSenses.js +32 -0
  68. package/src/environment/sensors/index.js +157 -0
  69. package/src/environment/sensors/indexedRayBands.js +103 -0
  70. package/src/environment/sensors/nearbyCars.js +52 -0
  71. package/src/environment/sensors/opponentRadar.js +75 -0
  72. package/src/environment/sensors/rayConfig.js +110 -0
  73. package/src/environment/sensors/rayDefaults.js +6 -0
  74. package/src/environment/sensors/rayGeometry.js +60 -0
  75. package/src/environment/sensors/rayGuards.js +19 -0
  76. package/src/environment/sensors/sensorTargets.js +66 -0
  77. package/src/environment/sensors/surfaceRays.js +360 -0
  78. package/src/environment/sensors/trackRays.js +424 -0
  79. package/src/environment/sensors.js +1 -367
  80. package/src/environment/specs.js +159 -26
  81. package/src/environment/workerProtocol.js +13 -1
  82. package/src/index.d.ts +171 -6
  83. package/src/index.js +6 -32
  84. package/src/rendering/proceduralTrackAsset.js +20 -823
  85. package/src/rendering/renderSnapshot.js +38 -12
  86. package/src/rendering/track/finishLineRenderer.js +34 -0
  87. package/src/rendering/track/gridRenderer.js +47 -0
  88. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  89. package/src/rendering/track/pitLaneRenderer.js +248 -0
  90. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  91. package/src/rendering/track/trackRenderConstants.js +38 -0
  92. package/src/rendering/track/trackRenderGeometry.js +94 -0
  93. package/src/simulation/collisionGeometry.js +1 -1
  94. package/src/simulation/driver/driverControlConstants.js +28 -0
  95. package/src/simulation/driver/driverController.js +47 -0
  96. package/src/simulation/driver/driverInput.js +28 -0
  97. package/src/simulation/driver/driverMath.js +6 -0
  98. package/src/simulation/driver/driverPersonality.js +25 -0
  99. package/src/simulation/driver/edgeRecovery.js +48 -0
  100. package/src/simulation/driver/racingControls.js +399 -0
  101. package/src/simulation/driver/racingLinePlan.js +166 -0
  102. package/src/simulation/driver/recoveryDynamics.js +38 -0
  103. package/src/simulation/driver/rejoinControls.js +213 -0
  104. package/src/simulation/driver/safetyCarControls.js +35 -0
  105. package/src/simulation/driver/trafficScan.js +51 -0
  106. package/src/simulation/driverController.js +4 -619
  107. package/src/simulation/participants/participantInteractions.js +120 -0
  108. package/src/simulation/pit/pitFlow.js +239 -0
  109. package/src/simulation/pit/pitIntent.js +92 -0
  110. package/src/simulation/pit/pitKinematics.js +24 -0
  111. package/src/simulation/pit/pitOccupancy.js +47 -0
  112. package/src/simulation/pit/pitPenaltyService.js +71 -0
  113. package/src/simulation/pit/pitQueue.js +60 -0
  114. package/src/simulation/pit/pitRouteMovement.js +167 -0
  115. package/src/simulation/pit/pitRouting.js +319 -0
  116. package/src/simulation/pit/pitService.js +43 -0
  117. package/src/simulation/pit/pitServiceConstants.js +21 -0
  118. package/src/simulation/pit/pitServiceProfile.js +45 -0
  119. package/src/simulation/pit/pitSnapshots.js +61 -0
  120. package/src/simulation/pit/pitState.js +113 -0
  121. package/src/simulation/pit/pitTireService.js +94 -0
  122. package/src/simulation/race/classification.js +104 -0
  123. package/src/simulation/race/gridPenalties.js +37 -0
  124. package/src/simulation/race/raceControlState.js +38 -0
  125. package/src/simulation/race/raceDistance.js +32 -0
  126. package/src/simulation/race/raceFinish.js +65 -0
  127. package/src/simulation/race/raceLifecycle.js +67 -0
  128. package/src/simulation/race/raceOrder.js +113 -0
  129. package/src/simulation/race/raceProgress.js +129 -0
  130. package/src/simulation/race/raceSetup.js +93 -0
  131. package/src/simulation/race/raceStep.js +80 -0
  132. package/src/simulation/race/redFlag.js +15 -0
  133. package/src/simulation/race/retirements.js +44 -0
  134. package/src/simulation/race/safetyCar.js +92 -0
  135. package/src/simulation/race/stalledDnf.js +77 -0
  136. package/src/simulation/raceSimulation.js +231 -2817
  137. package/src/simulation/replay/replayGhosts.js +128 -0
  138. package/src/simulation/rules/moduleConfig.js +72 -0
  139. package/src/simulation/rules/penaltyConfig.js +99 -0
  140. package/src/simulation/rules/penaltyEvents.js +18 -0
  141. package/src/simulation/rules/penaltyStats.js +26 -0
  142. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  143. package/src/simulation/rules/ruleDefaults.js +85 -0
  144. package/src/simulation/rules/rulesReview.js +144 -0
  145. package/src/simulation/rules/rulesetPresets.js +47 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  147. package/src/simulation/rulesConfig.js +5 -323
  148. package/src/simulation/snapshots/raceSnapshots.js +193 -0
  149. package/src/simulation/timing/drsTiming.js +87 -0
  150. package/src/simulation/timing/gapEstimation.js +38 -0
  151. package/src/simulation/timing/lapTelemetry.js +182 -0
  152. package/src/simulation/timing/raceTiming.js +15 -0
  153. package/src/simulation/timing/sectorPerformance.js +60 -0
  154. package/src/simulation/timing/timingConstants.js +5 -0
  155. package/src/simulation/timing/timingHistory.js +46 -0
  156. package/src/simulation/timing/timingLines.js +79 -0
  157. package/src/simulation/track/centerlineSpline.js +59 -0
  158. package/src/simulation/track/drsZones.js +67 -0
  159. package/src/simulation/track/pitLaneAccess.js +103 -0
  160. package/src/simulation/track/pitLaneLayout.js +395 -0
  161. package/src/simulation/track/pitLaneState.js +160 -0
  162. package/src/simulation/track/proceduralCenterline.js +237 -0
  163. package/src/simulation/track/sampleGeometry.js +49 -0
  164. package/src/simulation/track/spatialQueries.js +154 -0
  165. package/src/simulation/track/startStraight.js +141 -0
  166. package/src/simulation/track/trackConstants.js +109 -0
  167. package/src/simulation/track/trackGenerationOptions.js +283 -0
  168. package/src/simulation/track/trackMath.js +241 -0
  169. package/src/simulation/track/trackModel.js +240 -0
  170. package/src/simulation/track/trackQueryGrid.js +94 -0
  171. package/src/simulation/track/trackQueryIndex.js +659 -0
  172. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  173. package/src/simulation/track/trackQueryScratch.js +46 -0
  174. package/src/simulation/track/trackQueryStats.js +28 -0
  175. package/src/simulation/track/trackSectors.js +19 -0
  176. package/src/simulation/track/trackStatePolicy.js +24 -0
  177. package/src/simulation/track/trackValidation.js +98 -0
  178. package/src/simulation/trackModel.js +1 -1706
  179. package/src/simulation/vehicle/contactResolution.js +181 -0
  180. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  181. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  182. package/src/simulation/vehicle/runoffResponse.js +87 -0
  183. package/src/simulation/vehicle/surfacePriority.js +28 -0
  184. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  185. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  186. package/src/simulation/vehicle/vehiclePhysics.js +364 -0
  187. package/src/simulation/vehicle/vehicleSnapshots.js +327 -0
  188. package/src/simulation/vehicle/vehicleState.js +220 -0
  189. package/src/simulation/vehicle/wheelSurface.js +152 -0
  190. package/src/simulation/vehicleGeometry.js +1 -247
  191. package/src/simulation/vehiclePhysics.js +1 -136
  192. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  193. package/src/simulation/wheelSurface.js +1 -338
  194. package/src/styles.css +42 -0
  195. package/src/ui/cameraControlsTemplate.js +20 -0
  196. package/src/ui/carOverviewTemplate.js +33 -0
  197. package/src/ui/componentTemplates.js +14 -360
  198. package/src/ui/raceCanvasTemplate.js +69 -0
  199. package/src/ui/raceControlsTemplate.js +36 -0
  200. package/src/ui/shellTemplate.js +5 -4
  201. package/src/ui/telemetryDrawerTemplate.js +55 -0
  202. package/src/ui/telemetryTemplates.js +136 -0
  203. package/src/ui/timingTowerTemplate.js +34 -0
@@ -68,6 +68,8 @@ training script
68
68
  -> createPaddockEnvironment(options)
69
69
  -> resolveEnvironmentOptions(options)
70
70
  -> createRaceSimulation(...)
71
+ -> attach participant interaction profiles
72
+ -> normalize replay ghosts
71
73
  -> applyEnvironmentScenario(...) for reset-only placements
72
74
  -> env.reset()
73
75
  -> env.step(actions)
@@ -101,20 +103,23 @@ Responsibilities:
101
103
  `src/environment/` owns the browser-free expert environment:
102
104
 
103
105
  - `index.js` and `index.d.ts` expose the public subpath API.
104
- - `runtime.js` owns environment-loop `reset()`, `step(actions)`, `getObservation()`, and `getState()` around an injected race-simulation host.
106
+ - `runtime.js` owns environment-loop `reset()`, `step(actions)`, `resetDrivers(placements)`, `getObservation()`, and `getState()` around an injected race-simulation host. It also owns partial reset option merging, preserving omitted nested plain-object groups while replacing arrays and explicit scenario placement maps. It owns per-driver episode bookkeeping for batched environments and formats result state as `full`, `minimal`, or `none` without changing simulator stepping. `resetDrivers()` reapplies normal runoff/barrier classification to the selected cars after placement and before result construction, clears only selected driver episode terminal state, and does not rewind global `info.step`. Reset-only observation scope affects the returned observation and metrics maps, while full `info.drivers` remains scoped to each controlled driver's own episode state. Reward callbacks are transition-only and run from `step(actions)`, not reset/read result construction.
107
+ - `race/stalledDnf.js` owns the optional stalled off-track retirement rule. It consumes current wheel/track surface state after vehicle physics, ignores pre-start/red-flag/finished/pit-controlled states, tracks per-car stopped time below the configured speed threshold, and converts eligible stuck cars into the existing DNF path with `reason: 'stalled-off-track'`. Other modules should consume the resulting `outOfRace`/`dnf` state instead of reimplementing stalled detection.
105
108
  - `options.js` validates `controlledDrivers`, scenario participants, reset placement config, frame skip, action policy, sensor config, and episode limits.
106
- - `actions.js` maps normalized public controls onto simulator steering/throttle/brake controls plus pit intent and optional target compound requests.
109
+ - `actions.js` maps normalized public controls onto simulator steering/throttle/brake controls plus pit intent and optional target compound requests. It treats steering, throttle, and brake as required vehicle-control fields so broken model output is reported instead of silently becoming zeroed controls.
107
110
  - `scenarios.js` owns environment reset placement presets, absolute placements, and relative traffic layouts. It applies them through simulator state APIs during environment creation/reset only; it must not create policy-time teleport or assisted-control paths.
108
- - `observations.js`, `sensors.js`, and `events.js` build sensor-style observations, versioned fixed-schema vectors, track lookahead/curvature, ray/nearby-car readings, and global/per-driver events.
109
- - `recorder.js` owns neutral rollout transition export. It records environment-loop data and does not interpret rewards or policies.
110
- - `evaluation.js` owns deterministic evaluation cases and metrics. It can run a supplied policy against fixed seeds/scenarios, but it must not update models or define reward objectives.
111
- - `workerProtocol.js` owns the JSON-serializable wrapper used by external bridges. It forwards reset/step/spec/state messages to an environment instance and does not choose Python, Gymnasium, PettingZoo, or storage infrastructure.
111
+ - `observations.js`, `observationVector.js`, `sensors.js`, and `events.js` build sensor-style observations, versioned fixed-schema vectors, track lookahead/curvature, ray/nearby-car readings, and global/per-driver events. `observations.js` owns object observation assembly and direct compact-source preparation, while `observationVector.js` is the single owner of model-facing vector order, schema entries, and typed-array formatting for both object-backed and direct vector paths. Observation output formatting is separate from object/vector construction so compact training loops can request vector-only results while `getObservationSpec()` remains the schema source. Vector-only mode uses a direct vector path and can return `Float32Array` buffers without building the full public object observation. When a loop requests vector-only, schema-free, no-state output, `runtime.js` uses the internal `snapshotTraining()` path instead of the heavier public observation snapshot; returned state still uses public snapshot shapes. Environment-owned simulations default to the internal track query index unless `trackQueryIndex: false` is set, because repeated track/surface/ray fallback queries otherwise dominate step cost. `sensors.js` is a compatibility barrel; nearby-car scans, ray defaults, ray config normalization, track rays, surface rays, indexed ray-band intersections, car rays, body senses, contact-patch senses, boundary senses, opponent radar, and ray geometry live under `src/environment/sensors/`. Expensive surface-ray channels are opt-in and appear in the vector schema whenever requested. Replay-ghost sensor type flags are also part of compact ray/nearby-car vectors so vector-only consumers can distinguish real cars from opt-in references. Driver rays use a precision-aware cached origin state and shared per-observation ray target context. Normal driver `precision: 'driver'` road-edge, kerb, and illegal-surface channels keep the sampled values that policies receive with or without the track query index. The `batch-training` participant profile keeps the indexed recovery-ray path as its high-throughput training contract, and other indexed ray-boundary shortcuts are reserved for non-driver/debug precision paths. Destroyed/out-of-race cars keep the active ray schema but short-circuit to miss-valued rays before creating track ray contexts, which prevents terminal cars in batch training from repeatedly querying far-out geometry. Pit-connector and unusual cases still fall back to sampled checks. Ray precision defaults to the sampled model-facing `driver` contract; `debug` precision is diagnostic-only and must not be silently substituted into policy inputs or model-sense visualization.
112
+ - `metrics.js` owns reward-neutral per-driver facts such as progress delta, legal progress delta, off-track/severe-cut state, destroyed state, low-speed state, spin/backwards/instability state, lap completion, lap time, and contact count. Contact metrics dedupe the identifiers on each normalized physical contact event so one event increments each involved driver once. It must not encode or choose rewards.
113
+ - `recorder.js` owns neutral rollout transition export. It snapshots observations, actions, rewards, next observations, terminal flags, and info at record time and does not interpret rewards or policies.
114
+ - `controllerLoop.js` owns browser/headless controller orchestration around an injected runtime. It may lazily reset a fresh runtime before scheduled playback, but public `reset()` remains an explicit playback stop. Controller contexts report current `actions` separately from `previousActions`, where previous means the last action map actually applied before the current physics step. It maintains an observation cache so reset-only `resetDrivers()` results can stay small without handing null observations to the next batch decision for non-reset controlled drivers. Scheduled playback catches controller/runtime failures, stops the loop, and exposes the thrown value through loop stats.
115
+ - `evaluation.js` owns deterministic evaluation cases and metrics. It can run a supplied policy against fixed seeds/scenarios, internally requests the minimal snapshot needed when compact no-state base options are supplied, and reuses environment metric semantics for legal surfaces and contacts. It must not update models or define reward objectives.
116
+ - `workerProtocol.js` owns the JSON-serializable wrapper used by external bridges. It forwards reset/step/spec/state messages to an environment instance, including `getState` output selection, and does not choose Python, Gymnasium, PettingZoo, or storage infrastructure.
112
117
 
113
118
  The environment subpath must not import `src/index.js`, package CSS, PixiJS, `F1SimulatorApp`, or DOM-specific code.
114
119
 
115
- `src/simulation/rulesConfig.js` owns ruleset normalization and modular race-rule defaults. It keeps old flat rule keys compatible while adding preset/module config for pit stops, tire strategy, penalties, weather, reliability, and fuel load. Race modules should be added there first, then consumed by simulation logic through the normalized `race.rules` object.
120
+ `src/simulation/rulesConfig.js` is the public rules normalization facade. Defaults live in `src/simulation/rules/ruleDefaults.js`, ruleset presets in `rulesetPresets.js`, merge/value helpers in `ruleConfigMerge.js`, module normalization in `moduleConfig.js`, and penalty consequence normalization in `penaltyConfig.js`. Race modules should be added through those focused files first, then consumed by simulation logic through the normalized `race.rules` object.
116
121
 
117
- `src/simulation/trackModel.js` owns procedural track geometry, including start/finish normalization, sectors, DRS zones, and the deterministic pit-lane model. The model explicitly straightens the start/finish window so the starting grid, pit-entry approach area, and immediate post-line exit area are geometrically straight instead of merely choosing the least-curved generated segment. The pit lane is generated next to that start straight for every built track and exposes entry/exit connectors, explicit procedural road centerlines that overlap the track surface, connect on the lane-facing side of the main track, are tangent to the track at merge points and to the main pit lane at pit-lane endpoints, a straight main fast lane sized from the team/box run plus bounded entry/exit buffers, a parallel working lane, one shared service area per team, queue geometry behind each service area, and two garage boxes per team. The model also stores pit-lane world bounds so `nearestTrackState()` can skip pit road and service-box geometry checks for normal main-track points outside the pit area. `nearestTrackState()` classifies `pit-entry`, `pit-lane`, `pit-exit`, and `pit-box` as legal drivable states while preserving main-track `crossTrackError` for existing consumers. `src/rendering/proceduralTrackAsset.js` renders the same model with team-colored service areas, queue slots, and paired garage boxes, but draws main-circuit asphalt, kerbs, and borders above pit-lane asphalt so visual crossings are owned by the race track; it also renders an oversized grass background beyond the simulated world bounds, while the transparent race canvas host uses the same grass color as a fallback so deep zoom-out views do not expose host-page background. `F1SimulatorApp` overlays the dynamic pit-lane status light because open/closed/red-flag state belongs to race control, not the static track texture. `src/simulation/raceSimulation.js` owns the current pit-stop state machine: team/service-area assignment, bounded pit-train scheduling, active pit-lane capacity/gap checks, pit-lane open/closed and red-flag gating, same-team staged service queueing, target-compound selection, pit-crew service-time variability, forward approach-route generation, route segment speed-limiter metadata, fast-lane routing, steering-based route following, snap-limited queue capture, physical service-area occupancy checks, movement-based queue-to-service release, service timing, tire change, and exit routing all consume `track.pitLane` instead of recreating a separate visual-only path.
122
+ `src/simulation/track/trackModel.js` is the canonical track-model facade. Procedural centerline generation, validation, sample rebuilding, start-straight normalization, sectors, DRS zones, pit-lane layout/access/state, spatial query indexing, and spatial queries live in focused `src/simulation/track/*` modules. `src/simulation/trackModel.js` remains a compatibility re-export for tests or older internal imports, but new production code should import the canonical track module. `buildTrackModel()` attaches a non-enumerable internal `track.queryIndex` after samples, DRS, sectors, and pit-lane geometry are finalized, and cached built models freeze their static geometry so consumers cannot mutate shared pit-lane data between builds. Race simulations clone the built track to own mutable timing/pit state and default to attaching a fresh runtime index fork unless `trackQueryIndex: false` is explicitly requested. `trackQueryIndex.js` owns the public indexed-query orchestration over typed centerline segment data, arc-length buckets, surface band thresholds, and deterministic nearest/ray query selection. Generic uniform grid operations live in `trackQueryGrid.js`, scratch buffer/epoch reuse lives in `trackQueryScratch.js`, query diagnostics live in `trackQueryStats.js`, and pit-lane road/box grid construction plus pit candidate lookups live in `trackQueryPitIndex.js`. Public callers still use `nearestTrackState()` and the same snapshot shapes; the index is internal infrastructure and must not leak into JSON snapshots or host contracts. Indexed nearest lookup resolves equal-distance centerline segment ties deterministically inside the index; legacy nearest-sample fallback is reserved for missing indexes, invalid positions, or points outside the indexed world bounds. Ray-boundary queries walk the tight segment grid along the ray corridor instead of querying the expanded recovery grid, which keeps compact training-track profiles from producing thousands of false candidate segments per ray. The model explicitly straightens the start/finish window so the starting grid, pit-entry approach area, and immediate post-line exit area are geometrically straight instead of merely choosing the least-curved generated segment. The pit lane is generated next to that start straight for every built track and exposes entry/exit connectors, explicit procedural road centerlines that overlap the track surface, connect on the lane-facing side of the main track, are tangent to the track at merge points and to the main pit lane at pit-lane endpoints, a straight main fast lane sized from the team/box run plus bounded entry/exit buffers, a parallel working lane, one shared service area per team, queue geometry behind each service area, and two garage boxes per team. `src/rendering/proceduralTrackAsset.js` is now only the Pixi container lifecycle wrapper; main-circuit drawing, pit-lane drawing, grid rendering, finish-line rendering, material constants, render geometry, and offset-stroke safety live under `src/rendering/track/`. `F1SimulatorApp` overlays the dynamic pit-lane status light because open/closed/red-flag state belongs to race control, not the static track texture.
118
123
 
119
124
  `src/simulation/rules/` owns focused rule calculation helpers. Each stewarded rule should have its own function and, once non-trivial, its own file. `raceSimulation.js` should orchestrate timing/state and call those helpers; rule modules should return events or penalty payloads rather than mutating the whole simulation directly.
120
125
 
@@ -130,69 +135,143 @@ Responsibilities:
130
135
 
131
136
  ## Runtime App
132
137
 
133
- `src/app/F1SimulatorApp.js` owns browser runtime behavior.
138
+ `src/app/F1SimulatorApp.js` owns browser runtime orchestration.
134
139
 
135
140
  Responsibilities:
136
141
 
137
142
  - PixiJS application setup.
138
- - Asset loading.
139
- - Sprite creation.
140
143
  - Control event binding.
141
- - Fixed-step simulation pacing.
142
- - Viewport/document visibility throttling for the PixiJS ticker.
143
- - Camera modes.
144
- - Overlay-safe camera framing for the left timing-tower preset.
145
- - Timing tower rendering.
146
- - Telemetry rendering.
147
- - Race data panel rendering.
144
+ - Race simulation creation and restart orchestration.
145
+ - Shared app state containers and public package-control methods.
146
+ - Coordination between simulation snapshots, PixiJS renderers, DOM readouts, banners, and host callbacks.
148
147
  - Safety car button.
149
148
  - Multiple package-rendered safety-car buttons bound to one simulation state.
150
149
  - Restart behavior for race data, seed, and track changes. Asset URL changes and browser expert mode changes are intentionally outside restart because texture loading and ticker ownership are part of initialization.
151
150
  - Lifecycle cleanup, including partial-init failure cleanup and destruction of replaced PixiJS display children without destroying shared textures.
152
151
 
153
- When `options.expert.enabled` is true, `F1SimulatorApp` creates a narrow browser expert adapter around its existing `this.sim` race simulation. The adapter uses the same shared environment runtime as the headless API, but it must not create a second race simulation for the visual mount. Expert browser mounts disable automatic ticker-driven simulation advancement; the canvas updates only after `simulator.expert.reset()` or `simulator.expert.step(actions)` renders the new snapshot. The shared runtime owns controlled-driver pit intent defaults: controlled cars start with `pitIntent` set to `0`, tire-threshold automatic pit calls are disabled for them, and expert actions may request `0`, `1`, or `2` plus optional `pitCompound` through the simulation API instead of steering into the pit lane directly. Expert mode is a mount-time boundary; runtime restart rejects `expert` changes instead of attempting to rewire ticker ownership in place.
152
+ `src/app/camera/` owns camera state, camera constants, camera-safe-area measurement, track/pit-lane bounds fitting, zoom limits, mode availability, and target/frame calculation for overview, leader, selected, show-all, and pit cameras.
154
153
 
155
- Expert sensor visualization belongs to the browser app layer, not the headless environment. The environment result owns the observation contract; `BrowserExpertAdapter` passes that observation into `F1SimulatorApp.renderExpertFrame()`, and `F1SimulatorApp` draws opt-in sensor rays in a Pixi world layer so they share the same camera transform as the track and cars.
154
+ `src/app/rendering/` owns PixiJS rendering surfaces:
156
155
 
157
- This file is still large. When changing it substantially, prefer extracting cohesive modules rather than adding unrelated responsibilities.
156
+ - `appAssets.js` loads package-owned textures and applies fallback textures.
157
+ - `carRenderer.js` owns car sprites, hit areas, service countdown labels, and safety-car sprite rendering.
158
+ - `trackRenderer.js` renders dynamic DRS zone overlays around the procedural track asset.
159
+ - `drsTrailRenderer.js` owns DRS trail histories and drawing.
160
+ - `pitLaneStatusRenderer.js` owns pit-lane open/closed light rendering and redraw keys.
161
+ - `expertSensorRenderer.js` owns opt-in expert sensor-ray drawing.
162
+ - `replayGhostRenderer.js` owns translucent replay ghost overlays. Replay ghosts are not interactive car hit areas and must not feed timing rows or selected-car behavior.
163
+ - `displayUtils.js` owns shared Pixi display cleanup and color/angle helpers.
164
+
165
+ `src/app/readouts/` owns generated-DOM readout rendering:
166
+
167
+ - `timingTowerRenderer.js` owns timing tower rows, gap modes, waved-flag labels, and timing penalty badges.
168
+ - `telemetryRenderer.js` owns selected-car telemetry and lap/sector readouts.
169
+ - `carOverviewRenderer.js` owns driver/vehicle overview field selection and rendering.
170
+ - `raceStatusRenderer.js` owns race mode/lap/DRS/contact/start-light/finish readouts.
171
+ - `controlStateRenderer.js` owns camera, overview, banner mute, and safety-car button active states.
172
+ - `readoutFormatters.js` owns shared readout formatting.
173
+
174
+ `src/app/banners/` owns banner state and rendering helpers for race-data lower-thirds, project-radio scheduling/rendering, and steward warning/penalty messages. The app keeps the public state fields that tests and host-facing methods use, while the banner modules own the rendering and scheduling mechanics.
175
+
176
+ `src/app/runtime/` owns frame pacing, DOM/timing update intervals, ticker visibility throttling, and fixed-step render-loop execution. `F1SimulatorApp` delegates ticker work to this runtime layer.
177
+ Warmup ownership is split intentionally: `src/simulation/warmup/runtimeWarmup.js` owns warmup policy normalization, configuration fingerprint caching, and guarded execution; browser/environment/simulation call sites decide which disposable pre-step work to run for their surface.
178
+
179
+ When `options.expert.enabled` is true, `F1SimulatorApp` creates a narrow browser expert adapter around its existing `this.sim` race simulation. The adapter uses the same shared environment runtime as the headless API, but it must not create a second race simulation for the visual mount. Expert browser mounts disable automatic ticker-driven simulation advancement; the canvas updates only after `simulator.expert.reset()` or `simulator.expert.step(actions)` renders the new snapshot. External renderer mode may temporarily redraw the shared track asset from authoritative external snapshots; detach restores the local simulation track before local expert stepping resumes. The shared runtime owns controlled-driver pit intent defaults: controlled cars start with `pitIntent` set to `0`, tire-threshold automatic pit calls are disabled for them, and expert actions may request `0`, `1`, or `2` plus optional `pitCompound` through the simulation API instead of steering into the pit lane directly. Expert mode is a mount-time boundary; runtime restart rejects `expert` changes instead of attempting to rewire ticker ownership in place.
180
+
181
+ `src/app/BrowserExpertExternalRenderer.js` owns the external-renderer bridge used by browser expert mode: source subscription lifecycle, external-to-local driver-id mapping, external frame observation/event/penalty id rewriting, external track-surface redraws, local track restoration on detach, and external-renderer state reporting. `BrowserExpertAdapter.js` remains the local visual expert runtime adapter and should not grow those external-frame responsibilities again.
182
+
183
+ Expert sensor visualization belongs to the browser app layer, not the headless environment. The environment result owns the observation contract; `BrowserExpertAdapter` passes that observation into `F1SimulatorApp.renderExpertFrame()`, and `src/app/rendering/expertSensorRenderer.js` draws opt-in sensor rays in a Pixi world layer so they share the same camera transform as the track and cars. Multi-driver expert mounts visualize the selected controlled car by default, with an explicit `drivers: 'all'` opt-in for heavier all-controlled-car overlays. Ray visualization draws every detected model-facing channel as a separate marker rather than choosing only the nearest hit, which keeps road-edge, kerb, illegal-surface, and car detection debuggable. Barrier walls are track geometry and destruction boundaries, not model-facing ray hits; active ray objects, vectors, schemas, and visualizations must not expose a `barrier` ray channel. The Policy Runner senses panel follows the same boundary for all senses: it reads the active observation object/vector and may show separate diagnostics only when those are labeled as diagnostics, not as model input.
184
+
185
+ Track material rendering must use the same surface-band thresholds as simulation. `src/rendering/track/trackMaterialRenderer.js` exposes `getTrackMaterialBands(track)` as the rendering-side band contract derived from `track.width`, `track.kerbWidth`, `track.gravelWidth`, `track.runoffWidth`, and `track.barrierWidth`; gravel, runoff, visible barrier walls, ray surface hits, wheel surfaces, and barrier destruction must not invent independent offsets or hardcoded widths. Barrier contact uses the visible vehicle footprint against the rendered wall's inner face so visual contact, destruction, and the environment's terminal state agree.
186
+
187
+ Environment sensor target selection belongs to `src/environment/sensors/sensorTargets.js`. It adapts real cars and opt-in replay ghosts into a common sensor target shape while preserving the invariant that replay ghosts stay out of `snapshot.cars` and race systems. Radar enrichment keys targets by entity type plus id, so duplicate ids between real cars and replay ghosts do not cross-contaminate velocity-derived nearby-car facts.
188
+
189
+ `F1SimulatorApp.js` should stay a facade/orchestrator. New camera, rendering, readout, banner, or runtime behavior should usually land in the matching `src/app/*/` ownership module rather than expanding the app class.
158
190
 
159
191
  `src/app/domBindings.js` owns DOM selector lookup and null-safe readout text writes for package-generated UI surfaces.
160
192
 
161
193
  ## Simulation Core
162
194
 
163
- `src/simulation/raceSimulation.js` owns race state and rules.
195
+ `src/simulation/raceSimulation.js` owns race state orchestration and the public `F1RaceSimulation` facade.
164
196
 
165
197
  Responsibilities:
166
198
 
167
- - Race-control mode.
199
+ - Facade construction through `src/simulation/race/raceSetup.js`.
168
200
  - Normalized ruleset/module state received from `rulesConfig.js`.
169
- - Start sequence.
170
- - Safety car.
171
- - Car creation.
172
- - Race ordering.
173
- - Lap calculation.
174
- - Per-car lap and sector telemetry.
175
- - Timing history.
176
- - DRS eligibility.
177
- - Steward penalty ledger orchestration using focused rule helpers.
178
- - Collision response.
179
- - Snapshot creation.
201
+ - Fixed-step execution order through `src/simulation/race/raceStep.js`.
202
+ - Public facade methods used by hosts, tests, environment runtime, and browser runtime.
203
+ - Event buffer lifecycle.
204
+ - Delegation to feature-owned simulation modules.
205
+
206
+ `src/simulation/race/raceControlState.js` owns initial race-control and safety-car state construction.
207
+
208
+ `src/simulation/race/raceLifecycle.js` owns start-light updates, grid hold/release, and pit-lane open/closed transitions.
209
+
210
+ `src/simulation/race/safetyCar.js` owns safety-car and red-flag state transitions plus safety-car movement.
211
+
212
+ `src/simulation/race/redFlag.js` owns applying red-flag hold behavior to cars during the fixed-step loop.
213
+
214
+ `src/simulation/race/raceDistance.js` owns pure race-distance utilities including total-lap normalization, progress wrapping, lap calculation, and finish-distance calculation.
215
+
216
+ `src/simulation/race/raceOrder.js` owns ordered-car context, driver race context, aggression calculation, and DRS reference lookup.
217
+
218
+ `src/simulation/participants/participantInteractions.js` owns the resolved per-car interaction contract. It normalizes profiles and overrides, attaches `car.interaction` during race setup, and provides helpers for collision, sensor visibility, pit-lane blocking, and race-order membership. It does not own vehicle motion or policy behavior.
219
+
220
+ `src/simulation/replay/replayGhosts.js` owns trajectory-driven replay/reference entities. It normalizes ghost input, advances ghost state from race time, interpolates trajectory samples, and serializes `snapshot.replayGhosts`. Replay ghosts are not physics participants and must remain outside car collisions, pit systems, timing/classification, stewarding, and car hit areas.
221
+
222
+ `src/simulation/race/raceProgress.js` owns per-step race-state recalculation: progress, lap telemetry, sector timing, gaps, rank fields, attack state, and DRS latch coordination.
223
+
224
+ `src/simulation/race/raceFinish.js` owns finish detection and race-finish transitions while classification ranking remains in `classification.js`.
225
+
226
+ `src/simulation/race/classification.js` owns final classification ranking and classification consequences.
227
+
228
+ `src/simulation/race/gridPenalties.js` owns pre-start grid-drop repositioning.
229
+
230
+ `src/simulation/rules/penaltyEvents.js` owns penalty lifecycle event shapes emitted by the simulation facade.
231
+
232
+ `src/simulation/pit/pitState.js` owns pit team assignment, pit-crew normalization, and pit-stop initialization.
233
+
234
+ `src/simulation/pit/pitIntent.js` owns manual and automatic pit-intent normalization and mutation.
235
+
236
+ `src/simulation/pit/pitFlow.js` owns pit-stop flow orchestration: scheduling, rearming, entry eligibility, stop start, queue/service/exit progression, and delegation to pit routing/service helpers.
237
+
238
+ `src/simulation/pit/pitRouting.js` owns pit-route construction, pit-lane route sampling, limiter-segment lookup, and render-pose shifts used by route/contact movement.
239
+
240
+ `src/simulation/pit/pitService.js` is the public pit-service export surface and owns only the service-entry handoff. Occupancy, queue behavior, service profile timing, penalty service, tire service, pit-exit release, and pit-route movement are split across focused `src/simulation/pit/pit*.js` modules.
241
+
242
+ `src/simulation/pit/pitSnapshots.js` owns pit-lane status snapshots and public/render/observation pit-stop serializers.
243
+
244
+ `src/simulation/timing/raceTiming.js` is the timing compatibility export surface. Lap telemetry, sector performance, timing history, timing-line crossings, and gap estimation live in `lapTelemetry.js`, `sectorPerformance.js`, `timingHistory.js`, `timingLines.js`, and `gapEstimation.js`.
245
+
246
+ `src/simulation/rules/rulesReview.js` owns dispatch from simulation state to steward modules, while individual steward modules continue to own penalty decisions.
247
+
248
+ `src/simulation/rules/penaltyStats.js` owns penalty-stat aggregation and lookup helpers.
249
+
250
+ `src/simulation/vehicle/contactResolution.js` owns physical collision resolution and contact velocity response. Stewarding remains separate from physical response. In simulator mode, every collision branch that changes speed also keeps `velocityX/Y` synchronized, including contacts where one car is fixed by pit entry, queue, service, or exit routing.
251
+
252
+ `src/simulation/vehicle/runoffResponse.js` owns non-penalty off-track vehicle correction.
253
+
254
+ `src/simulation/snapshots/raceSnapshots.js` owns public, render, and observation snapshot assembly.
255
+
256
+ Weather effects, reliability failures, and fuel-load performance effects are not active `2.0.x` behavior. Their future ownership should stay separate from the race orchestrator: weather belongs in a future `src/simulation/weather/` module that returns track/session condition modifiers, reliability belongs in a future `src/simulation/reliability/` module that emits deterministic failure/degradation events, and fuel-load effects belong in a future vehicle-adjacent performance module that adjusts mass/pace through explicit inputs rather than hidden mutation.
180
257
 
181
258
  `snapshot()` is the full public data contract and must remain stable for host callbacks, environment state, and external callers. Internal browser/rendering paths use lean read models such as `snapshotRender()` so the 60 FPS loop does not serialize setup, wheels, penalties, and other non-render fields every frame. Environment and sensor code may use similarly narrow internal views, cache shared per-observation ray origin state, and broadphase car-ray checks before exact footprint intersection, but those must not change the public `RaceSnapshot` shape.
182
259
 
183
- `src/simulation/driverController.js` owns AI control decisions. It does not move cars directly; it samples the current track/race context, plans green-flag, rejoin, and safety-car targets, and returns steering/throttle/brake controls for `vehiclePhysics.js` to integrate. Green-flag steering combines target-point pursuit, capped upcoming-heading feed-forward, lateral correction, edge-pressure recovery, and a corner-apex offset so default cars can use the track width and carry speed through generated corners without bypassing steering-rate, grip, or yaw-rate limits. Its traffic planner keeps attack, defense, side-overlap, and lane-change thresholds in calibrated meter units so changing rendered scale or simulator-unit density does not silently weaken racecraft decisions. Safety-car queueing is also meter-calibrated: cars pursue compact frozen-order slots, add centerline correction, and cap corner speed so resized tracks do not create runaway safety-car gaps or off-track trains. Attack commitment and rejoin recovery are stored as deterministic per-car controller state; they affect only the next steering/throttle/brake request, not car pose or race order. The controller also treats braking and throttle as mutually exclusive during high steering or edge-recovery demand so it does not request power while trying to save a sharp corner.
260
+ `src/simulation/driverController.js` is a compatibility export for AI control decisions; canonical driver policy code lives under `src/simulation/driver/`. The dispatcher selects green-flag, rejoin, and safety-car controls, while separate modules own input building, personality generation, traffic scanning, edge recovery, edge-motion analysis, rejoin behavior, safety-car queueing, and racing-line attack/defense planning. It does not move cars directly; it returns steering/throttle/brake controls for `vehiclePhysics.js` to integrate.
184
261
 
185
- `src/simulation/vehicleGeometry.js` owns deterministic car footprint math. It exposes the rendered-scale body hull, four wheel/contact-patch oriented rectangles, current/previous pose generation, AABB helpers, and a per-car `geometryState` cache keyed by current and previous pose. Collision detection, wheel-surface sampling, snapshots, and debug previews consume this module instead of each recreating car dimensions.
262
+ `src/simulation/vehicle/vehicleGeometry.js` owns deterministic car footprint math. It exposes the rendered-scale body hull, four wheel/contact-patch oriented rectangles, current/previous pose generation, AABB helpers, and a per-car `geometryState` cache keyed by current and previous pose. Collision detection, wheel-surface sampling, snapshots, and debug previews consume this module instead of each recreating car dimensions. Root `src/simulation/vehicleGeometry.js`, `vehiclePhysics.js`, and `wheelSurface.js` are compatibility re-exports.
186
263
 
187
264
  `src/simulation/collisionGeometry.js` owns shape-confirmed vehicle contact. It first prunes pairs by nearby track progress with a circular-track window, then uses swept body AABBs, SAT body/body overlap checks, and conservative continuous checks between previous and current poses. Wheel shapes are intentionally excluded from car-vs-car collision for cost and stability; wheel geometry is still used by the surface and track-limit pipeline. `raceSimulation.js` only orchestrates response/cooldown/steward review after this module returns contact metadata, and collision stewarding derives rear-contact fault from physical track order so lapped cars are handled consistently.
188
265
 
189
- `src/simulation/wheelSurface.js` owns per-wheel track sampling and the effective car surface. On normal main-track running it uses the cached wheel geometry and the car center's track normal to analytically classify each wheel patch from signed offsets. Callers that already computed the center track state pass it in so wheel-surface classification does not repeat the same nearest-track scan. It falls back to full patch sampling near pit connectors and inside pit-lane states. It derives whether the full car is outside the white line and returns the worst wheel surface for physics. `vehiclePhysics.js` consumes that effective surface through the car track state, and separately compares left-side and right-side wheel resistance to add a small capped yaw bias toward the slower side. It does not classify surfaces from the car center.
266
+ `src/simulation/vehicle/wheelSurface.js` owns per-wheel track sampling and the effective car surface. Surface priority, analytic main-track classification, and pit-lane/connector classification are split into `surfacePriority.js`, `mainTrackWheelSurface.js`, and `pitWheelSurface.js`. On normal main-track running it uses the cached wheel geometry and the car center's track normal to analytically classify each wheel patch from signed offsets. It falls back to full patch sampling near pit connectors and inside pit-lane states. `vehiclePhysics.js` consumes that effective surface through the car track state, and separately compares left-side and right-side wheel resistance to add a small capped yaw bias toward the slower side.
190
267
 
191
- `src/simulation/vehiclePhysics.js` owns vehicle integration using the already-resolved effective surface and wheel-state resistance imbalance.
268
+ `src/simulation/vehicle/vehiclePhysics.js` owns vehicle integration using the already-resolved wheel surfaces and wheel-state resistance imbalance. It exposes the normalized `physicsMode` contract and routes between the compatibility arcade integrator and the opt-in simulator integrator. The simulator path owns 2D velocity integration, yaw response, traction-budget calculation, speed-sensitive steering, steering scrub, aero drag/downforce balance, per-wheel surface averaging, derived slip-angle telemetry, simulator surface coefficients, and telemetry fields; steering scrub opposes signed forward motion so reverse slides do not gain energy from steering loss. Driver AI and environment policies still provide only steering/throttle/brake/pit intent.
269
+
270
+ `src/simulation/vehicle/vehicleState.js` owns external state mutation from simulator and environment reset APIs. It rejects non-finite numeric partial state and resynchronizes simulator velocity from speed/heading when callers do not provide an explicit velocity vector, so scenario placement and `resetDrivers()` cannot leave stale physics velocity behind.
192
271
 
193
272
  `src/simulation/rules/pitLaneSpeedingSteward.js` owns pit-speed-limit review. `raceSimulation.js` calls it after authoritative race-state recalculation so it reads the same pit-lane part classification as snapshots and track-limit stewarding. The steward only checks speed-limited pit-lane parts (`fast-lane`, `working-lane`, service areas, and garage boxes), leaving pit-entry and pit-exit connector roads legal but not speed-limited.
194
273
 
195
- `src/simulation/trackModel.js` owns track construction, procedural track generation, automatic three-sector metadata, DRS zones, and nearest-track queries. Browser mounts generate a fresh procedural seed when `trackSeed` is omitted; explicit procedural track seeds are cached so repeated mounts with the same seed do not regenerate or rebuild the same model. Procedural generation starts from package-owned spline-like templates with deterministic jitter, validates shape, clearance, local turn accumulation, and per-sample heading jumps, and falls back to another template-derived control set instead of an oval control ring when attempts fail.
274
+ Browser mounts generate a fresh procedural seed when `trackSeed` is omitted; explicit procedural track seeds are cached by seed plus resolved generation options so repeated mounts with the same procedural contract do not regenerate or rebuild the same model. The canonical generator owns both the default `race` profile and the smaller training profiles. Profile resolution lives beside the track model, merges `race` defaults, selected profile defaults, and explicit overrides, then feeds the same centerline, validation, start-straight, spatial-query, surface, and physics pipeline. Procedural generation starts from seeded connected coarse region masks, traces the outer boundary, smooths and warps the boundary into centerline controls, validates shape, clearance, local turn accumulation, length, and per-sample heading jumps, and retries with another region-derived control set instead of immediately falling back to an oval control ring when attempts fail.
196
275
 
197
276
  `src/simulation/units.js` owns conversion between simulator units and public meter/km/h display values. Physics stays in simulator units; snapshots expose calibrated display fields such as `speedKph`, `distanceMeters`, and `gapMeters`.
198
277
 
@@ -214,7 +293,7 @@ The app runtime pauses its PixiJS ticker when the race canvas is outside the vie
214
293
 
215
294
  ## UI Shell
216
295
 
217
- `src/ui/componentTemplates.js` owns the generated markup and composition for non-banner UI surfaces:
296
+ `src/ui/componentTemplates.js` is the compatibility barrel for generated markup. Individual UI surfaces own their markup in focused template files:
218
297
 
219
298
  - Race controls.
220
299
  - Camera controls.
@@ -235,17 +314,19 @@ The app runtime pauses its PixiJS ticker when the race canvas is outside the vie
235
314
 
236
315
  `src/ui/templateUtils.js` owns shared template helpers such as escaping and loading-state markup. Banner templates and non-banner component templates should share helpers through this module instead of importing from each other.
237
316
 
238
- `src/ui/shellTemplate.js` composes those component templates into the default all-in-one simulator DOM and package-owned layout presets such as `left-tower-overlay`. Telemetry graph/table surfaces are independent package-owned components controlled by `ui.telemetryModules` when used through stack/drawer templates; the app renders them from `car.lapTelemetry` without requiring host-owned DOM. The telemetry stack can embed the car/driver overview, owns vertical scrolling when the host constrains its height, and is also embedded by the hide/show race telemetry drawer. The overview is also a separately mountable component. The project and radio lower-thirds share the same race-data panel markup; runtime state only swaps content and mode classes. The optional project telemetry detail lives inside that shared lower-third, while the broadcast sector banner remains a separate lower-third-style sector graph that can be mounted standalone or embedded inside the race canvas only when a host explicitly asks for that independent popup. `F1SimulatorApp` binds the selected car name/code/color to it through the same readout path as the other telemetry surfaces. The race telemetry drawer template composes an external top control row, race canvas with embedded timing tower, project/radio lower-third, top steward message, safety-car control, and the telemetry stack in a right drawer. Its open/close state is owned by `F1SimulatorApp`; the safety-car control, project/radio banner mute toggle, and telemetry toggle stay in the top row while telemetry is open. The drawer reserves final race-view space with a stable margin while the sidebar itself animates with a compositor transform, avoiding grid-template reflow during the slide. The drawer race area uses the configured workbench height so host embeds do not leave unused black space below the simulation. The left-tower overlay preset is responsible for internal component placement and package-owned proportions; the project/radio lower-third remains owned by the race canvas so it can either use the race space beside the timing tower in `auto` sizing mode or overlap the tower when space is constrained. Composable hosts can also ask the race-canvas template to embed the timing tower directly with `includeTimingTower`, using the same expand-vs-scroll vertical fit contract. `F1SimulatorApp` measures the resulting timing-tower gutter when framing the PixiJS camera, whether the tower comes from the prebuilt shell or from an embedded composable race canvas. It also owns camera mode availability, wheel/button zoom for every camera mode, temporary project/radio banner muting, hiding the pit camera control when the active track has no pit-lane geometry, and fitting the pit camera from operational `track.pitLane` lane, box, service, and queue bounds instead of host-provided coordinates or long access-road endpoints. On narrow hosts, CSS stacks timing boards full-width and the camera safe-area measurement treats those boards as stacked content instead of side gutters. Hosts should not provide the internal simulator markup or tune preset internals with raw sizing options.
317
+ `src/ui/shellTemplate.js` composes those component templates into the default all-in-one simulator DOM and package-owned layout presets such as `left-tower-overlay`. Telemetry graph/table surfaces are independent package-owned components controlled by `ui.telemetryModules` when used through stack/drawer templates; app readout modules render them from `car.lapTelemetry` without requiring host-owned DOM. The telemetry stack can embed the car/driver overview, owns vertical scrolling when the host constrains its height, and is also embedded by the hide/show race telemetry drawer. The overview is also a separately mountable component. The project and radio lower-thirds share the same race-data panel markup; runtime state only swaps content and mode classes. The optional project telemetry detail lives inside that shared lower-third, while the broadcast sector banner remains a separate lower-third-style sector graph that can be mounted standalone or embedded inside the race canvas only when a host explicitly asks for that independent popup. Readout modules bind the selected car name/code/color to it through the same readout path as the other telemetry surfaces. The race telemetry drawer template composes an external top control row, race canvas with embedded timing tower, project/radio lower-third, top steward message, safety-car control, and the telemetry stack in a right drawer. Its open/close state is owned by `F1SimulatorApp`; the safety-car control, project/radio banner mute toggle, and telemetry toggle stay in the top row while telemetry is open. The drawer reserves final race-view space with a stable margin while the sidebar itself animates with a compositor transform, avoiding grid-template reflow during the slide. The drawer race area uses the configured workbench height so host embeds do not leave unused black space below the simulation. The left-tower overlay preset is responsible for internal component placement and package-owned proportions; the project/radio lower-third remains owned by the race canvas so it can either use the race space beside the timing tower in `auto` sizing mode or overlap the tower when space is constrained. Composable hosts can also ask the race-canvas template to embed the timing tower directly with `includeTimingTower`, using the same expand-vs-scroll vertical fit contract. `src/app/camera/` measures the resulting timing-tower gutter when framing the PixiJS camera, whether the tower comes from the prebuilt shell or from an embedded composable race canvas. It also owns camera mode availability, wheel/button zoom for every camera mode, hiding the pit camera control when the active track has no pit-lane geometry, and fitting the pit camera from operational `track.pitLane` lane, box, service, and queue bounds instead of host-provided coordinates or long access-road endpoints. On narrow hosts, CSS stacks timing boards full-width and the camera safe-area measurement treats those boards as stacked content instead of side gutters. Hosts should not provide the internal simulator markup or tune preset internals with raw sizing options.
318
+
319
+ Penalty UI is display-only. `src/app/banners/stewardMessageController.js` may show track-limit warning events and `snapshot.penalties` in the top steward message, and `src/app/readouts/timingTowerRenderer.js` may show timing-row penalty badges when the host enables those UI options, but the UI must not infer, modify, or recalculate steward decisions.
239
320
 
240
- Penalty UI is display-only. `F1SimulatorApp` may show track-limit warning events and `snapshot.penalties` in the top steward message, and may show timing-row penalty badges when the host enables those UI options, but the UI must not infer, modify, or recalculate steward decisions.
321
+ `src/config/defaultOptions.js` owns preset resolution, telemetry module normalization, the public theme contract, and browser warmup option normalization defaults. Presets are merged before host options; theme fields are applied as package CSS variables by the all-in-one app and composable controller. This keeps sizing/color customization explicit without turning internal layout ratios into public API.
241
322
 
242
- `src/config/defaultOptions.js` owns preset resolution, telemetry module normalization, and the public theme contract. Presets are merged before host options; theme fields are applied as package CSS variables by the all-in-one app and composable controller. This keeps sizing/color customization explicit without turning internal layout ratios into public API.
323
+ `src/config/restartOptions.js` owns shared restart-option merging for both `mountF1Simulator().restart()` and `createPaddockSimulator().restart()`. It preserves omitted driver/entry data, merges nested UI banner and asset texture groups, and resets prior UI/theme state when a restart supplies a new preset so the two browser mount surfaces cannot drift.
243
324
 
244
- Race completion is owned by `src/simulation/raceSimulation.js`. The simulation records provisional `finishOrder` / `finishRank` as each car crosses the line and uses that order for snapshots until final classification exists, so app code never has to re-sort finished cars by live distance. The app layer reads `raceControl.finished`, `winner`, `classification`, and per-car `raceStatus` from snapshots, renders the timing/winner states, and emits lifecycle callbacks. Penalty lifecycle, pit-stop penalty service, service conversion, time adjustment, position drops, grid drops, and disqualification effects are applied inside the simulation, not in the UI. UI code may display countdowns from `pitStop.phase`, `serviceRemainingSeconds`, and `penaltyServiceRemainingSeconds`, but it must not infer, serve, or recalculate steward outcomes from lap text, timing rows, or car position.
325
+ Race completion is owned by `src/simulation/raceSimulation.js`. The simulation records provisional `finishOrder` / `finishRank` as each car crosses the line and uses that order for snapshots until final classification exists, so app code never has to re-sort finished cars by live distance. Race retirement state is owned by `src/simulation/race/retirements.js`; it defines DNF detection and ordering while vehicle code only marks the physical destruction event. The app layer reads `raceControl.finished`, `winner`, `classification`, and per-car `raceStatus`/DNF fields from snapshots, renders the timing/winner states, and emits lifecycle callbacks. Penalty lifecycle, pit-stop penalty service, service conversion, time adjustment, position drops, grid drops, disqualification effects, and DNF classification placement are applied inside the simulation, not in the UI. UI code may display countdowns from `pitStop.phase`, `serviceRemainingSeconds`, and `penaltyServiceRemainingSeconds`, but it must not infer, serve, or recalculate steward outcomes from lap text, timing rows, or car position.
245
326
 
246
327
  Composable hosts may choose where each package-owned component root is placed, but they still receive package-generated markup through the public mount functions. The controller marks each mounted root as an `f1-sim-component` styling scope so standalone pieces receive the same package variables as the all-in-one shell.
247
328
 
248
- `src/styles.css` styles the generated shell, imports package fonts, caps timing-tower width for readability, and owns the fixed-height timing-list scroll behavior. Timing rows are fixed grid rows stacked from the top, so rank positions do not stretch or redistribute when the number of entries changes. The timing tower owns its runtime interval-vs-leader-gap switch, while `F1SimulatorApp` reads both seconds and whole-lap gap values from the race snapshot and formats positive lap deficits as `+N` before seconds. Fixed hidden timing-line creation, crossing timestamps, and seconds-gap calculation stay inside `src/simulation/raceSimulation.js`. Sector-map surfaces read `lapTelemetry.sectorProgress` for bar fill and `lapTelemetry.liveSectors` for live active-sector time; the simulation owns those values so UI components do not infer active split state from DOM state. Future sectors are cleared by the simulation and ignored defensively by the renderer, so banner/sidebar sector nodes cannot keep stale timing text or yellow/purple/green performance classes after the active sector changes. Component templates include lightweight package-owned loading overlays; `F1SimulatorApp` removes them after startup initialization has completed.
329
+ `src/styles.css` styles the generated shell, imports package fonts, caps timing-tower width for readability, and owns the fixed-height timing-list scroll behavior. Timing rows are fixed grid rows stacked from the top, so rank positions do not stretch or redistribute when the number of entries changes. The timing tower owns its runtime interval-vs-leader-gap switch, while `F1SimulatorApp` reads both seconds and whole-lap gap values from the race snapshot and formats positive lap deficits as `+N` before seconds. Fixed hidden timing-line creation, crossing timestamps, and seconds-gap calculation stay inside `src/simulation/timing/`. Sector-map surfaces read `lapTelemetry.sectorProgress` for bar fill and `lapTelemetry.liveSectors` for live active-sector time; the simulation owns those values so UI components do not infer active split state from DOM state. Future sectors are cleared by the simulation and ignored defensively by the renderer, so banner/sidebar sector nodes cannot keep stale timing text or yellow/purple/green performance classes after the active sector changes. Component templates include lightweight package-owned loading overlays; `F1SimulatorApp` removes them after startup initialization has completed.
249
330
 
250
331
  The host page should only provide:
251
332
 
package/docs/concepts.md CHANGED
@@ -55,7 +55,7 @@ The composable controller has a setup phase:
55
55
 
56
56
  After startup, both controller styles expose runtime methods such as `restart()`, `selectDriver()`, `setSafetyCarDeployed()`, `getSnapshot()`, and `destroy()`.
57
57
 
58
- Pit-stop hosts can also call `setPitIntent(driverId, 0 | 1 | 2)`. `0` means no pending pit request, `1` means take the next pit entry only if it is free, and `2` means keep the request active until the automatic pit-stop sequence completes.
58
+ Pit-stop hosts can also call `setPitIntent(driverId, 0 | 1 | 2, targetCompound?)` after enabling `rules.modules.pitStops`. `0` clears a pending request, `1` keeps trying until a free-enough pit-entry window is available, and `2` commits to entering at the next pit-entry window even when opportunistic capacity or gap checks would block mode `1`. `targetCompound` must be one of `rules.modules.tireStrategy.compounds`; `tireStrategy` controls tire choices and stewarding, but does not enable pit stops by itself.
59
59
 
60
60
  ## Driver
61
61
 
@@ -87,7 +87,7 @@ Entry responsibilities:
87
87
 
88
88
  ## Team
89
89
 
90
- A team is entry-level metadata used for race identity and future pit-lane behavior. The timing tower uses the team icon in its team column. Team color defaults to the car color when omitted.
90
+ A team is entry-level metadata used for race identity and current pit-lane behavior. The timing tower uses the team icon in its team column. Team color defaults to the car color when omitted. When pit stops are enabled, team color marks the shared service area, queue point, and garage boxes, and optional `team.pitCrew` stats can affect service-time variability.
91
91
 
92
92
  ## Driver Ratings
93
93
 
@@ -125,9 +125,9 @@ The package supports:
125
125
  - A default named track for low-level simulation callers that provide neither a track nor a track seed.
126
126
  - Procedural browser tracks from generated or explicit `trackSeed` values.
127
127
 
128
- Browser mounts that omit `trackSeed` create a fresh procedural track for that mount. Passing `trackSeed` makes the generated circuit deterministic; repeated procedural seeds are cached within the page runtime. Generated circuits use validated template-based spline controls rather than a pure oval fallback, so failed candidates retry into another shaped layout instead of degrading into a circular track.
128
+ Browser mounts that omit `trackSeed` create a fresh procedural track for that mount. Passing `trackSeed` makes the generated circuit deterministic; repeated procedural seeds are cached within the page runtime. Generated circuits start from seeded connected coarse regions, trace the region boundary, then smooth and warp that boundary into the sampled centerline. This lets tracks move inward and outward through concave sections and chicane-like bends instead of degrading into circular or oval fallback layouts. Failed candidates retry with another deterministic region-derived shape.
129
129
 
130
- Every built track also exposes a deterministic `pitLane` near the start/finish straight. The pit lane has an entry before the start line, an exit after it, explicit lane-aligned entry/exit road centerlines, a straight main fast lane, a parallel working lane, 10 shared team service areas, and 20 unused garage boxes arranged as 10 team pairs. Pit-lane asphalt, service areas, and garage boxes are legal drivable surfaces for sensors, runoff handling, and track-limit stewarding. When the pit-stop module is enabled, cars automatically form bounded pit trains when there is enough rolling gap, brake to the limiter by the main lane start, follow the fast lane, pass through their assigned colored team queue spot, roll into the team service area, change tire compound, and return through the exit. Team-mates share one service area; every car passes through the queue spot first, but it only waits there if the active service area is occupied. A second team car waits without blocking the fast lane before following a short queue-release route into the active service area after the previous car has physically cleared it. Tire condition can request a stop automatically: below the configured request threshold the car asks to pit if free, and below the commit threshold it keeps retrying until served. The speed limiter is active on the straight main pit lane/working lane, not on the entry and exit connector roads.
130
+ Tracks whose resolved generation options enable pit lanes expose a deterministic `pitLane` near the start/finish straight. The `race` profile includes pit-lane geometry; training profiles are pitless by default unless the host explicitly enables a pit lane. An enabled pit lane has an entry before the start line, an exit after it, explicit lane-aligned entry/exit road centerlines, a straight main fast lane, a parallel working lane, 10 shared team service areas, and 20 unused garage boxes arranged as 10 team pairs. Pit-lane asphalt, service areas, and garage boxes are legal drivable surfaces for sensors, runoff handling, track-limit stewarding, and stalled-DNF timing. When the pit-stop module is enabled, cars automatically form bounded pit trains when there is enough rolling gap, brake to the limiter by the main lane start, follow the fast lane, pass through their assigned colored team queue spot, roll into the team service area, change tire compound, and return through the exit. Team-mates share one service area; every car passes through the queue spot first, but it only waits there if the active service area is occupied. A second team car waits without blocking the fast lane before following a short queue-release route into the active service area after the previous car has physically cleared it. Tire condition can request a stop automatically: below the configured request threshold the car asks to pit if free, and below the commit threshold it keeps retrying until served. The speed limiter is active on the straight main pit lane/working lane, not on the entry and exit connector roads.
131
131
 
132
132
  ## Progress
133
133
 
@@ -161,7 +161,11 @@ Per-car timing exposes both interval to the car ahead and direct same-lead-lap g
161
161
 
162
162
  A ruleset is a named preset for race-rule defaults. `paddock` is the package default, `grandPrix2025` / `fia2025` are 2024-2025-era grand-prix-style presets, and `custom` is for host-owned behavior.
163
163
 
164
- A rule module is an advanced subsystem under `rules.modules`, such as pit stops, tire strategy, penalties, weather, reliability, or fuel load. Presets set defaults, but explicit module config wins.
164
+ A rule module is an advanced subsystem under `rules.modules`. Active modules include pit stops, tire strategy, tire degradation, stalled off-track DNF, and penalties. Weather, reliability, and fuel load are reserved placeholders and do not currently change grip, power, failure risk, mass, or pace. Presets set defaults, but explicit module config wins.
165
+
166
+ `pitStops` and `tireStrategy` are intentionally separate. `pitStops.enabled` owns automatic pit routing, pit-lane status, and successful pit intent requests. `tireStrategy.enabled` owns compound lists, tire-requirement stewarding, and pit target choices. A two-compound rule does not make cars pit unless `pitStops` is enabled separately or the selected ruleset preset enables both modules.
167
+
168
+ Stalled off-track DNF is an opt-in retirement rule. When `rules.modules.stalledDnf.enabled` is `true`, it marks a car DNF when the car is off legal racing/pit surfaces and remains below the configured speed threshold for the configured time, while excluding pre-start, red flag, active pit handling, and already-finished cars.
165
169
 
166
170
  Penalty strictness is a stewarding value from `0` to `1`. `0` means the penalty subsection is not enforced. `1` means the subsection applies close to its configured rule margin.
167
171