cuju-render 0.1.0__py3-none-any.whl

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 (382) hide show
  1. cuju/render/__init__.py +759 -0
  2. cuju/render/__main__.py +1065 -0
  3. cuju/render/_export/__init__.py +3 -0
  4. cuju/render/_export/feed.py +542 -0
  5. cuju/render/_export/sizes.py +157 -0
  6. cuju/render/_export/video.py +1618 -0
  7. cuju/render/_export/video_jobs.py +408 -0
  8. cuju/render/_format/__init__.py +3 -0
  9. cuju/render/_format/annotations.py +235 -0
  10. cuju/render/_format/codec.py +307 -0
  11. cuju/render/_format/keep.py +121 -0
  12. cuju/render/_format/recording.py +860 -0
  13. cuju/render/_format/writer.py +54 -0
  14. cuju/render/_guide/__init__.py +3 -0
  15. cuju/render/_guide/doctor.py +523 -0
  16. cuju/render/_guide/help.py +62 -0
  17. cuju/render/_guide/tutorial.py +254 -0
  18. cuju/render/_http/__init__.py +3 -0
  19. cuju/render/_http/commands.py +57 -0
  20. cuju/render/_http/headers.py +103 -0
  21. cuju/render/_http/launch.py +500 -0
  22. cuju/render/_http/launchers.py +408 -0
  23. cuju/render/_http/pwa.py +128 -0
  24. cuju/render/_http/server.py +2105 -0
  25. cuju/render/_http/window.py +360 -0
  26. cuju/render/_moments/__init__.py +3 -0
  27. cuju/render/_moments/captions.py +284 -0
  28. cuju/render/_moments/events.py +1123 -0
  29. cuju/render/_moments/search.py +694 -0
  30. cuju/render/_packages/__init__.py +3 -0
  31. cuju/render/_packages/glsl.py +746 -0
  32. cuju/render/_packages/loudness.py +334 -0
  33. cuju/render/_packages/pack.py +1959 -0
  34. cuju/render/_packages/presentation.py +1376 -0
  35. cuju/render/_packages/vocabulary.py +609 -0
  36. cuju/render/_playing/__init__.py +3 -0
  37. cuju/render/_playing/achievements.py +463 -0
  38. cuju/render/_playing/bench.py +609 -0
  39. cuju/render/_playing/challenges.py +1813 -0
  40. cuju/render/_playing/demo.py +323 -0
  41. cuju/render/_playing/draw.py +211 -0
  42. cuju/render/_playing/errors.py +20 -0
  43. cuju/render/_playing/game.py +645 -0
  44. cuju/render/_playing/limits.py +339 -0
  45. cuju/render/_playing/play.py +926 -0
  46. cuju/render/_playing/policy_worker.py +250 -0
  47. cuju/render/_playing/strict.py +810 -0
  48. cuju/render/_registry/__init__.py +3 -0
  49. cuju/render/_registry/clips.py +394 -0
  50. cuju/render/_registry/joints.py +108 -0
  51. cuju/render/_registry/teams.py +2058 -0
  52. cuju/render/_store/__init__.py +3 -0
  53. cuju/render/_store/home.py +70 -0
  54. cuju/render/_store/library.py +604 -0
  55. cuju/render/_store/samples.py +163 -0
  56. cuju/render/_text/__init__.py +3 -0
  57. cuju/render/_text/languages.py +637 -0
  58. cuju/render/_text/messages.py +1396 -0
  59. cuju/render/_tournaments/__init__.py +3 -0
  60. cuju/render/_tournaments/results.py +547 -0
  61. cuju/render/_tournaments/tournament.py +2742 -0
  62. cuju/render/_tournaments/tournament_cli.py +587 -0
  63. cuju/render/_tournaments/tournament_runner.py +1975 -0
  64. cuju/render/_version.py +6 -0
  65. cuju/render/_watch/__init__.py +3 -0
  66. cuju/render/_watch/grid.py +265 -0
  67. cuju/render/_watch/live.py +664 -0
  68. cuju/render/_watch/notebook.py +79 -0
  69. cuju/render/_watch/show.py +171 -0
  70. cuju/render/samples/sample-11v11.cjr +0 -0
  71. cuju/render/samples/sample-5v5.cjr +0 -0
  72. cuju/render/samples/teams/starter/main.py +33 -0
  73. cuju/render/scenarios/benchmark_full_game_11_vs_11_easy.py +46 -0
  74. cuju/render/scenarios/benchmark_full_game_11_vs_11_hard.py +46 -0
  75. cuju/render/scenarios/benchmark_full_game_11_vs_11_medium.py +46 -0
  76. cuju/render/scenarios/benchmark_full_game_5_vs_5_easy.py +34 -0
  77. cuju/render/scenarios/benchmark_full_game_5_vs_5_hard.py +34 -0
  78. cuju/render/scenarios/benchmark_full_game_5_vs_5_medium.py +34 -0
  79. cuju/render/web/assets/adboard_polygon.webp +0 -0
  80. cuju/render/web/assets/adboard_your_ad_here.webp +0 -0
  81. cuju/render/web/assets/animations/kaykit-rig-medium-combatmelee.glb +0 -0
  82. cuju/render/web/assets/animations/kaykit-rig-medium-general.glb +0 -0
  83. cuju/render/web/assets/animations/kaykit-rig-medium-movementadvanced.glb +0 -0
  84. cuju/render/web/assets/animations/kaykit-rig-medium-movementbasic.glb +0 -0
  85. cuju/render/web/assets/animations/kaykit-rig-medium-simulation.glb +0 -0
  86. cuju/render/web/assets/animations/kaykit-rig-medium-special.glb +0 -0
  87. cuju/render/web/assets/ball.glb +0 -0
  88. cuju/render/web/assets/challenges/3_vs_1_with_keeper.mp4 +0 -0
  89. cuju/render/web/assets/challenges/3_vs_1_with_keeper.webp +0 -0
  90. cuju/render/web/assets/challenges/corner.mp4 +0 -0
  91. cuju/render/web/assets/challenges/corner.webp +0 -0
  92. cuju/render/web/assets/challenges/counterattack_easy.mp4 +0 -0
  93. cuju/render/web/assets/challenges/counterattack_easy.webp +0 -0
  94. cuju/render/web/assets/challenges/counterattack_hard.mp4 +0 -0
  95. cuju/render/web/assets/challenges/counterattack_hard.webp +0 -0
  96. cuju/render/web/assets/challenges/empty_goal.mp4 +0 -0
  97. cuju/render/web/assets/challenges/empty_goal.webp +0 -0
  98. cuju/render/web/assets/challenges/empty_goal_close.mp4 +0 -0
  99. cuju/render/web/assets/challenges/empty_goal_close.webp +0 -0
  100. cuju/render/web/assets/challenges/manifest.json +375 -0
  101. cuju/render/web/assets/challenges/one_on_one.mp4 +0 -0
  102. cuju/render/web/assets/challenges/one_on_one.webp +0 -0
  103. cuju/render/web/assets/challenges/one_on_one_hard.mp4 +0 -0
  104. cuju/render/web/assets/challenges/one_on_one_hard.webp +0 -0
  105. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.mp4 +0 -0
  106. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.webp +0 -0
  107. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.mp4 +0 -0
  108. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.webp +0 -0
  109. cuju/render/web/assets/challenges/run_to_score.mp4 +0 -0
  110. cuju/render/web/assets/challenges/run_to_score.webp +0 -0
  111. cuju/render/web/assets/challenges/run_to_score_with_keeper.mp4 +0 -0
  112. cuju/render/web/assets/challenges/run_to_score_with_keeper.webp +0 -0
  113. cuju/render/web/assets/challenges/single_goal_versus_lazy.mp4 +0 -0
  114. cuju/render/web/assets/challenges/single_goal_versus_lazy.webp +0 -0
  115. cuju/render/web/assets/crowd.webp +0 -0
  116. cuju/render/web/assets/crowd_normal.webp +0 -0
  117. cuju/render/web/assets/cuju-logo-384.png +0 -0
  118. cuju/render/web/assets/cuju-logo.png +0 -0
  119. cuju/render/web/assets/cuju-logo.svg +46 -0
  120. cuju/render/web/assets/cuju-mark-light.svg +33 -0
  121. cuju/render/web/assets/cuju-mark.svg +33 -0
  122. cuju/render/web/assets/goal.glb +0 -0
  123. cuju/render/web/assets/grass.webp +0 -0
  124. cuju/render/web/assets/hair_black.png +0 -0
  125. cuju/render/web/assets/hair_blonde.png +0 -0
  126. cuju/render/web/assets/hair_brown.png +0 -0
  127. cuju/render/web/assets/hair_darkblonde.png +0 -0
  128. cuju/render/web/assets/hair_long01.glb +0 -0
  129. cuju/render/web/assets/hair_long02.glb +0 -0
  130. cuju/render/web/assets/hair_medium01.glb +0 -0
  131. cuju/render/web/assets/hair_red.png +0 -0
  132. cuju/render/web/assets/hair_short01.glb +0 -0
  133. cuju/render/web/assets/hair_short02.glb +0 -0
  134. cuju/render/web/assets/icons/cuju-192.png +0 -0
  135. cuju/render/web/assets/icons/cuju-512.png +0 -0
  136. cuju/render/web/assets/icons/cuju-maskable-512.png +0 -0
  137. cuju/render/web/assets/kit_goalkeeper.webp +0 -0
  138. cuju/render/web/assets/kit_mask.png +0 -0
  139. cuju/render/web/assets/kit_normal.webp +0 -0
  140. cuju/render/web/assets/manifest.json +601 -0
  141. cuju/render/web/assets/models/kaykit-barbarian.glb +0 -0
  142. cuju/render/web/assets/models/kaykit-barbarian.json +23 -0
  143. cuju/render/web/assets/models/kaykit-knight.glb +0 -0
  144. cuju/render/web/assets/models/kaykit-knight.json +23 -0
  145. cuju/render/web/assets/models/kaykit-mage.glb +0 -0
  146. cuju/render/web/assets/models/kaykit-mage.json +23 -0
  147. cuju/render/web/assets/models/kaykit-mannequin-medium.glb +0 -0
  148. cuju/render/web/assets/models/kaykit-mannequin-medium.json +23 -0
  149. cuju/render/web/assets/models/kaykit-ranger.glb +0 -0
  150. cuju/render/web/assets/models/kaykit-ranger.json +23 -0
  151. cuju/render/web/assets/models/kaykit-rogue-hooded.glb +0 -0
  152. cuju/render/web/assets/models/kaykit-rogue-hooded.json +23 -0
  153. cuju/render/web/assets/models/kaykit-rogue.glb +0 -0
  154. cuju/render/web/assets/models/kaykit-rogue.json +23 -0
  155. cuju/render/web/assets/models/kaykit-skeleton-mage.glb +0 -0
  156. cuju/render/web/assets/models/kaykit-skeleton-mage.json +23 -0
  157. cuju/render/web/assets/models/kaykit-skeleton-minion.glb +0 -0
  158. cuju/render/web/assets/models/kaykit-skeleton-minion.json +23 -0
  159. cuju/render/web/assets/models/kaykit-skeleton-rogue.glb +0 -0
  160. cuju/render/web/assets/models/kaykit-skeleton-rogue.json +23 -0
  161. cuju/render/web/assets/models/kaykit-skeleton-warrior.glb +0 -0
  162. cuju/render/web/assets/models/kaykit-skeleton-warrior.json +23 -0
  163. cuju/render/web/assets/pitch.webp +0 -0
  164. cuju/render/web/assets/pitch_normal.webp +0 -0
  165. cuju/render/web/assets/player.glb +0 -0
  166. cuju/render/web/assets/shoe.webp +0 -0
  167. cuju/render/web/assets/shoe_sole.png +0 -0
  168. cuju/render/web/assets/skin_01.png +0 -0
  169. cuju/render/web/assets/skin_02.png +0 -0
  170. cuju/render/web/assets/skin_03.png +0 -0
  171. cuju/render/web/assets/skin_04.png +0 -0
  172. cuju/render/web/assets/sounds/applause.ogg +0 -0
  173. cuju/render/web/assets/sounds/beach-wave-1.ogg +0 -0
  174. cuju/render/web/assets/sounds/beach-wave-2.ogg +0 -0
  175. cuju/render/web/assets/sounds/beach-wave-3.ogg +0 -0
  176. cuju/render/web/assets/sounds/beach-wave-4.ogg +0 -0
  177. cuju/render/web/assets/sounds/city-horn.ogg +0 -0
  178. cuju/render/web/assets/sounds/city-loop.ogg +0 -0
  179. cuju/render/web/assets/sounds/city-siren.ogg +0 -0
  180. cuju/render/web/assets/sounds/crowd-cheer.ogg +0 -0
  181. cuju/render/web/assets/sounds/crowd-loop.ogg +0 -0
  182. cuju/render/web/assets/sounds/crowd-ooh.ogg +0 -0
  183. cuju/render/web/assets/sounds/gulls.ogg +0 -0
  184. cuju/render/web/assets/sounds/neon-sting.ogg +0 -0
  185. cuju/render/web/assets/sounds/rain-loop.ogg +0 -0
  186. cuju/render/web/assets/sounds/sounds.json +46 -0
  187. cuju/render/web/assets/sounds/space-beep.ogg +0 -0
  188. cuju/render/web/assets/sounds/space-hum.ogg +0 -0
  189. cuju/render/web/assets/sounds/space-radio.ogg +0 -0
  190. cuju/render/web/assets/sounds/whistle-long.ogg +0 -0
  191. cuju/render/web/assets/sounds/whistle-short.ogg +0 -0
  192. cuju/render/web/assets/sounds/wind-loop.ogg +0 -0
  193. cuju/render/web/easy.css +357 -0
  194. cuju/render/web/export.html +23 -0
  195. cuju/render/web/feed.css +262 -0
  196. cuju/render/web/feed.html +28 -0
  197. cuju/render/web/flat.css +120 -0
  198. cuju/render/web/home.css +837 -0
  199. cuju/render/web/index.html +373 -0
  200. cuju/render/web/js/boot.js +50 -0
  201. cuju/render/web/js/broadcast/audio.js +2141 -0
  202. cuju/render/web/js/broadcast/celebrate.js +1238 -0
  203. cuju/render/web/js/broadcast/decisions.js +609 -0
  204. cuju/render/web/js/broadcast/hud.js +352 -0
  205. cuju/render/web/js/broadcast/show.js +3614 -0
  206. cuju/render/web/js/broadcast/transition.js +2231 -0
  207. cuju/render/web/js/broadcast/voices.js +153 -0
  208. cuju/render/web/js/capture.js +101 -0
  209. cuju/render/web/js/core/accessibility.js +90 -0
  210. cuju/render/web/js/core/api.js +429 -0
  211. cuju/render/web/js/core/comfort.js +135 -0
  212. cuju/render/web/js/core/i18n.js +705 -0
  213. cuju/render/web/js/core/icons.js +167 -0
  214. cuju/render/web/js/core/input-settings.js +156 -0
  215. cuju/render/web/js/core/keys.js +277 -0
  216. cuju/render/web/js/core/language-ui.js +125 -0
  217. cuju/render/web/js/core/menu.js +853 -0
  218. cuju/render/web/js/core/names.js +113 -0
  219. cuju/render/web/js/core/page-structure.js +110 -0
  220. cuju/render/web/js/core/scenarios.js +109 -0
  221. cuju/render/web/js/core/spoilers.js +236 -0
  222. cuju/render/web/js/core/typeahead.js +38 -0
  223. cuju/render/web/js/export/dom-picture.js +330 -0
  224. cuju/render/web/js/export/export-host.js +97 -0
  225. cuju/render/web/js/export/feed-dialog.js +191 -0
  226. cuju/render/web/js/export/feed-params.js +266 -0
  227. cuju/render/web/js/export/feed.js +363 -0
  228. cuju/render/web/js/export/mp4-mux.js +295 -0
  229. cuju/render/web/js/export/photo-core.js +313 -0
  230. cuju/render/web/js/export/photo.js +1314 -0
  231. cuju/render/web/js/export/safe-frame.js +59 -0
  232. cuju/render/web/js/export/size-fields.js +103 -0
  233. cuju/render/web/js/export/sizes.js +178 -0
  234. cuju/render/web/js/export/video-boot.js +55 -0
  235. cuju/render/web/js/export/video-browser.js +325 -0
  236. cuju/render/web/js/export/video-export.js +417 -0
  237. cuju/render/web/js/export/video-plan.js +207 -0
  238. cuju/render/web/js/export/video-range.js +185 -0
  239. cuju/render/web/js/export/webm-mux.js +198 -0
  240. cuju/render/web/js/feed-boot.js +151 -0
  241. cuju/render/web/js/feed-frame.js +101 -0
  242. cuju/render/web/js/help/doctor.js +488 -0
  243. cuju/render/web/js/help/firstrun.js +379 -0
  244. cuju/render/web/js/help/help-center.js +438 -0
  245. cuju/render/web/js/help/install-offer.js +164 -0
  246. cuju/render/web/js/help/install-state.js +104 -0
  247. cuju/render/web/js/help/lessons-core.js +207 -0
  248. cuju/render/web/js/help/lessons.js +463 -0
  249. cuju/render/web/js/help/palette.js +584 -0
  250. cuju/render/web/js/help/pwa.js +257 -0
  251. cuju/render/web/js/help/tour-core.js +241 -0
  252. cuju/render/web/js/help/tour.js +461 -0
  253. cuju/render/web/js/main.js +2513 -0
  254. cuju/render/web/js/pages/draw.js +188 -0
  255. cuju/render/web/js/pages/fieldpage.js +530 -0
  256. cuju/render/web/js/pages/home.js +521 -0
  257. cuju/render/web/js/pages/matches.js +779 -0
  258. cuju/render/web/js/pages/moments-ui.js +359 -0
  259. cuju/render/web/js/pages/queue-filters.js +63 -0
  260. cuju/render/web/js/pages/recording-groups.js +80 -0
  261. cuju/render/web/js/pages/recordings.js +1069 -0
  262. cuju/render/web/js/play/bench.js +347 -0
  263. cuju/render/web/js/play/challenges.js +1302 -0
  264. cuju/render/web/js/play/medals.js +127 -0
  265. cuju/render/web/js/play/play.js +1953 -0
  266. cuju/render/web/js/play/teasers.js +196 -0
  267. cuju/render/web/js/play/touch.js +350 -0
  268. cuju/render/web/js/play/tournament-layout.js +487 -0
  269. cuju/render/web/js/play/tournament-results.js +441 -0
  270. cuju/render/web/js/play/tournament-strict.js +560 -0
  271. cuju/render/web/js/play/tournament.js +2585 -0
  272. cuju/render/web/js/players/animate.js +972 -0
  273. cuju/render/web/js/players/clips.js +224 -0
  274. cuju/render/web/js/players/kaykit.js +162 -0
  275. cuju/render/web/js/players/kits.js +676 -0
  276. cuju/render/web/js/players/number-patch.js +82 -0
  277. cuju/render/web/js/players/players.js +2694 -0
  278. cuju/render/web/js/players/recolor.js +893 -0
  279. cuju/render/web/js/players/retarget.js +325 -0
  280. cuju/render/web/js/players/rig-numbers.js +174 -0
  281. cuju/render/web/js/presentation/animator.js +231 -0
  282. cuju/render/web/js/presentation/assign.js +85 -0
  283. cuju/render/web/js/presentation/behaviors.js +440 -0
  284. cuju/render/web/js/presentation/budget.js +209 -0
  285. cuju/render/web/js/presentation/catalog.js +1674 -0
  286. cuju/render/web/js/presentation/clocks.js +72 -0
  287. cuju/render/web/js/presentation/commands.js +301 -0
  288. cuju/render/web/js/presentation/contract.js +240 -0
  289. cuju/render/web/js/presentation/effects.js +904 -0
  290. cuju/render/web/js/presentation/flash.js +605 -0
  291. cuju/render/web/js/presentation/freeze-worklet.js +22 -0
  292. cuju/render/web/js/presentation/glsl.js +464 -0
  293. cuju/render/web/js/presentation/host.js +323 -0
  294. cuju/render/web/js/presentation/index.js +2066 -0
  295. cuju/render/web/js/presentation/loudness-worklet.js +235 -0
  296. cuju/render/web/js/presentation/loudness.js +740 -0
  297. cuju/render/web/js/presentation/moments.js +536 -0
  298. cuju/render/web/js/presentation/post.js +327 -0
  299. cuju/render/web/js/presentation/preview.js +180 -0
  300. cuju/render/web/js/presentation/protocol.js +174 -0
  301. cuju/render/web/js/presentation/reach.js +158 -0
  302. cuju/render/web/js/presentation/rigging.js +130 -0
  303. cuju/render/web/js/presentation/sandbox.js +157 -0
  304. cuju/render/web/js/presentation/shaders.js +235 -0
  305. cuju/render/web/js/presentation/stage.js +402 -0
  306. cuju/render/web/js/presentation/teamaudio.js +722 -0
  307. cuju/render/web/js/presentation/tracks.js +197 -0
  308. cuju/render/web/js/presentation/values.js +81 -0
  309. cuju/render/web/js/recording/annotation-data.js +203 -0
  310. cuju/render/web/js/recording/captions.js +491 -0
  311. cuju/render/web/js/recording/codec.js +185 -0
  312. cuju/render/web/js/recording/decode-worker.js +70 -0
  313. cuju/render/web/js/recording/decode.js +94 -0
  314. cuju/render/web/js/recording/episodes.js +46 -0
  315. cuju/render/web/js/recording/format.js +461 -0
  316. cuju/render/web/js/recording/live.js +348 -0
  317. cuju/render/web/js/recording/moments-query.js +59 -0
  318. cuju/render/web/js/recording/moments.js +149 -0
  319. cuju/render/web/js/recording/pose.js +86 -0
  320. cuju/render/web/js/recording/timeline.js +275 -0
  321. cuju/render/web/js/teams/art.js +50 -0
  322. cuju/render/web/js/teams/clip-editor.js +293 -0
  323. cuju/render/web/js/teams/editor.js +1979 -0
  324. cuju/render/web/js/teams/library.js +344 -0
  325. cuju/render/web/js/teams/painter.js +633 -0
  326. cuju/render/web/js/teams/surprise-ui.js +195 -0
  327. cuju/render/web/js/teams/surprise.js +497 -0
  328. cuju/render/web/js/teams/team-check.js +127 -0
  329. cuju/render/web/js/watch/annotations.js +270 -0
  330. cuju/render/web/js/watch/flat.js +366 -0
  331. cuju/render/web/js/watch/grid.js +436 -0
  332. cuju/render/web/js/watch/pitch2d.js +289 -0
  333. cuju/render/web/js/watch/player-tools.js +192 -0
  334. cuju/render/web/js/watch/presets.js +201 -0
  335. cuju/render/web/js/world/arenas.js +997 -0
  336. cuju/render/web/js/world/ball.js +204 -0
  337. cuju/render/web/js/world/cameras.js +982 -0
  338. cuju/render/web/js/world/crowd.js +376 -0
  339. cuju/render/web/js/world/field.js +227 -0
  340. cuju/render/web/js/world/framing.js +145 -0
  341. cuju/render/web/js/world/occlusion.js +48 -0
  342. cuju/render/web/js/world/pitch.js +703 -0
  343. cuju/render/web/js/world/scene.js +395 -0
  344. cuju/render/web/js/world/sky.js +325 -0
  345. cuju/render/web/js/world/stadium.js +1262 -0
  346. cuju/render/web/js/world/vendor-sky.js +367 -0
  347. cuju/render/web/js/world/weather.js +264 -0
  348. cuju/render/web/languages/en.json +4750 -0
  349. cuju/render/web/lessons.json +137 -0
  350. cuju/render/web/manifest.webmanifest +23 -0
  351. cuju/render/web/style.css +8399 -0
  352. cuju/render/web/sw.js +235 -0
  353. cuju/render/web/vendor/fabric/LICENSE +16 -0
  354. cuju/render/web/vendor/fabric/fabric.min.mjs +1 -0
  355. cuju/render/web/vendor/lucide/LICENSE +43 -0
  356. cuju/render/web/vendor/lucide/icons.js +141 -0
  357. cuju/render/web/vendor/three/LICENSE +21 -0
  358. cuju/render/web/vendor/three/build/three.core.js +60586 -0
  359. cuju/render/web/vendor/three/build/three.module.js +19719 -0
  360. cuju/render/web/vendor/three/examples/jsm/loaders/GLTFLoader.js +4925 -0
  361. cuju/render/web/vendor/three/examples/jsm/postprocessing/EffectComposer.js +365 -0
  362. cuju/render/web/vendor/three/examples/jsm/postprocessing/MaskPass.js +195 -0
  363. cuju/render/web/vendor/three/examples/jsm/postprocessing/OutputPass.js +148 -0
  364. cuju/render/web/vendor/three/examples/jsm/postprocessing/Pass.js +191 -0
  365. cuju/render/web/vendor/three/examples/jsm/postprocessing/RenderPass.js +193 -0
  366. cuju/render/web/vendor/three/examples/jsm/postprocessing/ShaderPass.js +135 -0
  367. cuju/render/web/vendor/three/examples/jsm/postprocessing/UnrealBloomPass.js +541 -0
  368. cuju/render/web/vendor/three/examples/jsm/shaders/CopyShader.js +52 -0
  369. cuju/render/web/vendor/three/examples/jsm/shaders/LuminosityHighPassShader.js +68 -0
  370. cuju/render/web/vendor/three/examples/jsm/shaders/OutputShader.js +103 -0
  371. cuju/render/web/vendor/three/examples/jsm/utils/BufferGeometryUtils.js +1501 -0
  372. cuju/render/web/vendor/three/examples/jsm/utils/SkeletonUtils.js +496 -0
  373. cuju_render-0.1.0.dist-info/METADATA +116 -0
  374. cuju_render-0.1.0.dist-info/RECORD +382 -0
  375. cuju_render-0.1.0.dist-info/WHEEL +4 -0
  376. cuju_render-0.1.0.dist-info/licenses/LICENSE +202 -0
  377. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +396 -0
  378. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC0-1.0.txt +127 -0
  379. cuju_render-0.1.0.dist-info/licenses/LICENSES/ISC.txt +16 -0
  380. cuju_render-0.1.0.dist-info/licenses/LICENSES/MIT.txt +24 -0
  381. cuju_render-0.1.0.dist-info/licenses/LICENSES/Unlicense.txt +13 -0
  382. cuju_render-0.1.0.dist-info/licenses/NOTICE +35 -0
@@ -0,0 +1,609 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """The vocabulary of team presentation packages, events, reflexes, effects, sound and features.
5
+
6
+ Section 4 of the contract lists the events a package reacts to, section 11 the reflexes and
7
+ commands that answer them. Effects, lights, shaders, post, sound, music, environment moments,
8
+ camera and time suggestions and overlay slots grow with later renderer work, so their names and
9
+ shapes live here, apart from the stable core of `_presentation`, and the page keeps the same lists
10
+ in `web/js/presentation/commands.js` and `catalog.js`.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import difflib
16
+ import re
17
+ from typing import Any
18
+
19
+ from cuju.errors import ArgumentError
20
+
21
+ # The events of the catalog, read from frames, inferred, of the show and of the transport.
22
+ FRAME_EVENTS = (
23
+ 'touch',
24
+ 'action.begin',
25
+ 'action.end',
26
+ 'possession',
27
+ 'goal',
28
+ 'restart.begin',
29
+ 'restart.taken',
30
+ 'foul',
31
+ 'card',
32
+ 'sentOff',
33
+ 'onPitch',
34
+ 'offside',
35
+ 'control',
36
+ 'episode.begin',
37
+ 'episode.end',
38
+ 'halftime',
39
+ 'fulltime',
40
+ )
41
+ INFERRED_EVENTS = (
42
+ 'pass',
43
+ 'shot',
44
+ 'header',
45
+ 'volley',
46
+ 'slide',
47
+ 'save',
48
+ 'catch',
49
+ 'deflection',
50
+ 'block',
51
+ 'woodwork',
52
+ 'nearMiss',
53
+ 'sprint.begin',
54
+ 'sprint.end',
55
+ 'dribble.begin',
56
+ 'dribble.end',
57
+ )
58
+ PRESENTATION_EVENTS = (
59
+ 'lineup.begin',
60
+ 'lineup.end',
61
+ 'celebration.begin',
62
+ 'celebration.end',
63
+ 'reaction.begin',
64
+ 'reaction.end',
65
+ 'replay.begin',
66
+ 'replay.end',
67
+ 'mvp.reveal',
68
+ 'winner.reveal',
69
+ 'appearance',
70
+ 'entity.remove',
71
+ )
72
+ TRANSPORT_EVENTS = ('seek', 'pause', 'play', 'speed', 'reverse', 'reducedMotion')
73
+ EVENTS = FRAME_EVENTS + INFERRED_EVENTS + PRESENTATION_EVENTS + TRANSPORT_EVENTS
74
+ PHASES = ('forecast', 'occur', 'resolve')
75
+ TEAMS = ('self', 'opponent', 'any')
76
+
77
+ # The features a package may require or prefer, those this renderer implements first.
78
+ FEATURES = (
79
+ 'drivers.grf',
80
+ 'drivers.team',
81
+ 'drivers.worker',
82
+ 'lookahead',
83
+ 'lookahead.outcomes',
84
+ 'inference.pass/1',
85
+ 'inference.shot/1',
86
+ 'inference.header/1',
87
+ 'inference.volley/1',
88
+ 'inference.slide/1',
89
+ 'inference.save/1',
90
+ 'inference.goal/1',
91
+ 'inference.sprint/1',
92
+ 'inference.dribble/1',
93
+ 'clips.markers',
94
+ 'drivers.grf+layers',
95
+ 'bend.ball',
96
+ 'ik.fabrik',
97
+ 'shader.custom',
98
+ 'shader.sceneColor',
99
+ 'post.layer',
100
+ 'post.fullscreen',
101
+ 'geometry.worker',
102
+ 'overlay.offscreen',
103
+ 'audio.worklet',
104
+ 'audio.music',
105
+ 'camera.suggest',
106
+ 'time.suggest',
107
+ 'environment.sky',
108
+ 'environment.lightning',
109
+ 'fans.tifo',
110
+ )
111
+ # Features of the contract still to come, none since contract 1.1.
112
+ PLANNED: tuple[str, ...] = ()
113
+ # The features a worker alone uses, whose lack drops the worker and keeps the manifest.
114
+ WORKER_FEATURES = ('drivers.worker', 'geometry.worker', 'overlay.offscreen')
115
+ _FEATURE = re.compile(r'[a-z][a-z0-9]*(\.[A-Za-z0-9_+/]+)*')
116
+
117
+ ACTIONS = (
118
+ 'audio',
119
+ 'effect',
120
+ 'clip',
121
+ 'behaviour',
122
+ 'claim',
123
+ 'camera',
124
+ 'time',
125
+ 'overlay',
126
+ 'environment',
127
+ 'music',
128
+ )
129
+ CLASSES = ('moment', 'action', 'bed')
130
+ BUSES = ('effects', 'fans', 'music', 'voice')
131
+ REPLAY = ('repeat', 'quiet', 'skip')
132
+ CLAIMS = ('ball', 'goal', 'sky', 'floodlights', 'fans')
133
+ SLOTS = ('badge', 'lowerThird', 'sting', 'card', 'lineup')
134
+ SHOTS = ('closeup', 'orbit', 'low', 'behindBall', 'path')
135
+ _ATTACH = re.compile(r'ball|path|world|self|socket:[A-Za-z0-9_]{1,40}')
136
+ _NAME = re.compile(r'[A-Za-z0-9_.-]{1,40}')
137
+
138
+ MAXIMUM_REFLEXES = 64
139
+ MAXIMUM_ACTIONS = 8
140
+
141
+
142
+ def features(value: Any, name: str) -> list[str]:
143
+ """Checks a list of feature names, the requires or prefers of a manifest."""
144
+ if not isinstance(value, list) or len(value) > 64:
145
+ raise ArgumentError(f'expected {name} as a list of at most 64 feature names')
146
+ for item in value:
147
+ if not isinstance(item, str) or len(item) > 60 or not _FEATURE.fullmatch(item):
148
+ raise ArgumentError(f'expected a feature name in {name}, found {item!r}')
149
+ return list(value)
150
+
151
+
152
+ def _name(value: Any, what: str) -> str:
153
+ if not isinstance(value, str) or not _NAME.fullmatch(value):
154
+ raise ArgumentError(f'expected {what} of up to 40 letters, digits, dots and underscores')
155
+ return value
156
+
157
+
158
+ _IMAGES = ('.png', '.jpg', '.webp', '.ktx2')
159
+ _SOUNDS = ('.ogg', '.mp3', '.wav')
160
+ EFFECT_TYPES = ('particles', 'trail', 'decal', 'light', 'mesh', 'post')
161
+ PASSES = ('glow', 'outline', 'pixelate', 'tint', 'chromatic', 'blur', 'custom')
162
+ BLENDS = ('additive', 'normal')
163
+ MAXIMUM_LIGHTS = 16
164
+ MAXIMUM_PROCESSORS = 4
165
+
166
+
167
+ def _real(value: Any, low: float, high: float, what: str) -> float:
168
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or not low <= value <= high:
169
+ raise ArgumentError(f'expected {what} from {low} to {high}, found {value!r}')
170
+ return float(value)
171
+
172
+
173
+ def _pair(value: Any, low: float, high: float, what: str) -> list[float]:
174
+ """Checks a number or a pair of numbers, a start and an end or a least and a most."""
175
+ items = value if isinstance(value, list) else [value, value]
176
+ if len(items) != 2:
177
+ raise ArgumentError(f'expected {what} as a number or two')
178
+ return [_real(v, low, high, what) for v in items]
179
+
180
+
181
+ def _rgb(value: Any, what: str) -> list[int]:
182
+ if (
183
+ not isinstance(value, list)
184
+ or len(value) != 3
185
+ or not all(isinstance(c, int) and not isinstance(c, bool) and 0 <= c <= 255 for c in value)
186
+ ):
187
+ raise ArgumentError(f'expected {what} as three integers from 0 to 255')
188
+ return list(value)
189
+
190
+
191
+ def _colors(value: Any, what: str) -> list[list[int]]:
192
+ """Checks one colour or a start and an end colour."""
193
+ if isinstance(value, list) and value and isinstance(value[0], list):
194
+ if len(value) != 2:
195
+ raise ArgumentError(f'expected {what} as one colour or two')
196
+ return [_rgb(value[0], what), _rgb(value[1], what)]
197
+ color = _rgb(value, what)
198
+ return [color, list(color)]
199
+
200
+
201
+ # The ranges of the numeric fields of each type of effect, the lowest, the highest and whether a
202
+ # field takes a pair, a start and an end or a least and a most.
203
+ _RANGES: dict[str, dict[str, tuple[float, float, bool]]] = {
204
+ 'particles': {
205
+ 'count': (1, 2000, False),
206
+ 'rate': (0, 5000, False),
207
+ 'life': (0.05, 10, True),
208
+ 'speed': (0, 60, True),
209
+ 'spread': (0, 3.15, False),
210
+ 'gravity': (-30, 30, False),
211
+ 'drag': (0, 20, False),
212
+ 'size': (0, 2, True),
213
+ },
214
+ 'trail': {'length': (0.05, 3, False), 'width': (0, 2, True), 'segments': (4, 64, False)},
215
+ 'decal': {'size': (0.1, 3.16, False), 'fade': (0.5, 10, False), 'opacity': (0, 0.7, True)},
216
+ 'light': {
217
+ 'intensity': (0, 50, False),
218
+ 'distance': (0.5, 30, False),
219
+ 'decay': (0, 2, False),
220
+ 'attack': (0, 5, False),
221
+ 'release': (0, 10, False),
222
+ 'duration': (0.05, 30, False),
223
+ },
224
+ 'mesh': {'scale': (0.01, 10, False), 'spin': (-50, 50, False), 'fade': (0, 10, False)},
225
+ 'post': {'strength': (0, 4, False)},
226
+ }
227
+ _COMMON = {'type', 'file', 'texture', 'shader', 'blend', 'color', 'opacity', 'duration'}
228
+ _EXTRA = {
229
+ 'particles': {'direction'},
230
+ 'decal': {'life', 'rotation'},
231
+ 'post': {'pass', 'scope'},
232
+ }
233
+
234
+
235
+ def _effect(key: str, spec: dict[str, Any], file: Any) -> tuple[dict[str, Any], set[str]]:
236
+ """Checks one effect of a type of contract 1.1 and returns it with the fields it knows."""
237
+ kind = spec['type']
238
+ what = f'effect {key}'
239
+ out: dict[str, Any] = {'type': kind}
240
+ if 'texture' in spec:
241
+ out['texture'] = file(spec['texture'], f'the texture of {what}', _IMAGES)
242
+ if 'shader' in spec:
243
+ out['shader'] = file(spec['shader'], f'the shader of {what}', ('.glsl',))
244
+ if 'blend' in spec:
245
+ if spec['blend'] not in BLENDS:
246
+ raise ArgumentError(f'expected the blend of {what} among {BLENDS}')
247
+ out['blend'] = spec['blend']
248
+ if 'color' in spec:
249
+ out['color'] = _colors(spec['color'], f'the colour of {what}')
250
+ if 'opacity' in spec:
251
+ out['opacity'] = _pair(spec['opacity'], 0, 1, f'the opacity of {what}')
252
+ if 'duration' in spec:
253
+ out['duration'] = _real(spec['duration'], 0, 30, f'the duration of {what}')
254
+ for name, (low, high, pair) in _RANGES[kind].items():
255
+ if name in spec:
256
+ check = _pair if pair else _real
257
+ out[name] = check(spec[name], low, high, f'the {name} of {what}')
258
+ if kind == 'particles' and 'direction' in spec:
259
+ direction = spec['direction']
260
+ if not isinstance(direction, list) or len(direction) != 3:
261
+ raise ArgumentError(f'expected the direction of {what} as three numbers')
262
+ out['direction'] = [_real(v, -1e3, 1e3, 'a direction') for v in direction]
263
+ elif kind == 'decal':
264
+ life = spec.get('life', 8)
265
+ out['life'] = life if life == 'half' else _real(life, 0.5, 600, f'the life of {what}')
266
+ rotation = spec.get('rotation', 'random')
267
+ out['rotation'] = rotation if rotation == 'random' else _real(rotation, -360, 360, 'a turn')
268
+ elif kind == 'mesh':
269
+ out['file'] = file(spec.get('file'), f'the model of {what}', ('.glb',))
270
+ elif kind == 'post':
271
+ if spec.get('pass') not in PASSES:
272
+ raise ArgumentError(f'expected the pass of {what} among {PASSES}')
273
+ out['pass'] = spec['pass']
274
+ out['scope'] = spec.get('scope', 'layer')
275
+ if out['scope'] not in ('layer', 'fullscreen'):
276
+ raise ArgumentError(f'expected the scope of {what} as layer or fullscreen')
277
+ if out['pass'] == 'custom' and 'shader' not in out:
278
+ raise ArgumentError(f'expected the shader of the custom pass of {what}')
279
+ if out['scope'] == 'fullscreen':
280
+ out['duration'] = min(out.get('duration', 1.5), 3.0)
281
+ return out, _COMMON | set(_RANGES[kind]) | _EXTRA.get(kind, set())
282
+
283
+
284
+ def effects(value: Any, file: Any, warnings: list[str]) -> dict[str, Any]:
285
+ """Checks the effects of a manifest, each a type, its fields and the package files it names.
286
+
287
+ The types of contract 1.1 are particles, trail, decal, light, mesh and post. An effect of
288
+ another type stays as it is for a later renderer, which this one counts and leaves undrawn.
289
+ """
290
+ if not isinstance(value, dict) or len(value) > 64:
291
+ raise ArgumentError('expected effects as a mapping of at most 64 names')
292
+ out = {}
293
+ for key, spec in value.items():
294
+ _name(key, 'the name of an effect')
295
+ if not isinstance(spec, dict) or not isinstance(spec.get('type'), str):
296
+ raise ArgumentError(f'expected effect {key} with a type')
297
+ try:
298
+ if spec['type'] not in EFFECT_TYPES:
299
+ item = dict(spec)
300
+ if 'file' in spec:
301
+ item['file'] = file(spec['file'], f'effect {key}')
302
+ warnings.append(f'effect {key} of type {spec["type"]} waits for a later renderer')
303
+ out[key] = item
304
+ continue
305
+ item, known = _effect(key, spec, file)
306
+ warnings.extend(f'effect {key} ignores {k}' for k in sorted(set(spec) - known))
307
+ out[key] = item
308
+ except ArgumentError as error:
309
+ warnings.append(f'effect {key} left out, {error}')
310
+ return out
311
+
312
+
313
+ def lights(value: Any, sockets: dict[str, Any], warnings: list[str]) -> dict[str, Any]:
314
+ """Checks the lights of a manifest, persistent lights on the sockets of the team's players."""
315
+ if not isinstance(value, dict) or len(value) > MAXIMUM_LIGHTS:
316
+ raise ArgumentError(f'expected lights as a mapping of at most {MAXIMUM_LIGHTS} names')
317
+ out = {}
318
+ known = {'socket', 'offset', 'color', 'intensity', 'distance', 'decay', 'kind', 'when'}
319
+ for key, spec in value.items():
320
+ try:
321
+ _name(key, 'the name of a light')
322
+ if not isinstance(spec, dict):
323
+ raise ArgumentError('expected a description')
324
+ socket = spec.get('socket', 'root')
325
+ if socket != 'root' and socket not in sockets:
326
+ raise ArgumentError('expected a socket of the rig')
327
+ offset = spec.get('offset', [0, 0, 0])
328
+ if not isinstance(offset, list) or len(offset) != 3:
329
+ raise ArgumentError('expected the offset as three numbers')
330
+ kind = spec.get('kind', 'local')
331
+ if kind not in ('pool', 'local'):
332
+ raise ArgumentError('expected the kind of a light as pool or local')
333
+ item = {
334
+ 'socket': socket,
335
+ 'offset': [_real(v, -10, 10, 'an offset') for v in offset],
336
+ 'color': _rgb(spec.get('color', [255, 255, 255]), 'the colour of a light'),
337
+ 'intensity': _real(spec.get('intensity', 1), 0, 50, 'the intensity of a light'),
338
+ 'distance': _real(spec.get('distance', 4), 0.5, 30, 'the distance of a light'),
339
+ 'decay': _real(spec.get('decay', 2), 0, 2, 'the decay of a light'),
340
+ 'kind': kind,
341
+ }
342
+ if 'when' in spec:
343
+ item['when'] = spec['when']
344
+ warnings.extend(f'light {key} ignores {k}' for k in sorted(set(spec) - known))
345
+ out[key] = item
346
+ except ArgumentError as error:
347
+ warnings.append(f'light {key} left out, {error}')
348
+ return out
349
+
350
+
351
+ def post(value: Any, file: Any) -> dict[str, Any]:
352
+ """Checks the post section, a persistent pass on the team's own layer."""
353
+ if not isinstance(value, dict) or set(value) - {'layer'}:
354
+ raise ArgumentError('expected post to hold a layer')
355
+ spec = value.get('layer')
356
+ if not isinstance(spec, dict) or spec.get('pass') not in PASSES:
357
+ raise ArgumentError(f'expected the layer pass among {PASSES}')
358
+ out: dict[str, Any] = {'pass': spec['pass']}
359
+ if 'shader' in spec:
360
+ out['shader'] = file(spec['shader'], 'the shader of the layer pass', ('.glsl',))
361
+ elif spec['pass'] == 'custom':
362
+ raise ArgumentError('expected the shader of the custom layer pass')
363
+ if 'color' in spec:
364
+ out['color'] = _rgb(spec['color'], 'the colour of the layer pass')
365
+ out['strength'] = _real(spec.get('strength', 1), 0, 4, 'the strength of the layer pass')
366
+ return {'layer': out}
367
+
368
+
369
+ def overlays(value: Any) -> dict[str, Any]:
370
+ """Checks the overlay slots a worker draws in, each among the renderer's reserved slots."""
371
+ if not isinstance(value, dict) or not set(value) <= set(SLOTS):
372
+ raise ArgumentError(f'expected overlays among the slots {SLOTS}')
373
+ return {slot: {} for slot in value}
374
+
375
+
376
+ def _music(key: str, spec: Any, file: Any) -> dict[str, Any]:
377
+ _name(key, 'the name of music')
378
+ stems = spec.get('stems') if isinstance(spec, dict) else None
379
+ if isinstance(stems, dict):
380
+ raise ArgumentError(
381
+ f'expected the stems of music {key} as a list of files, found a mapping, and a music '
382
+ 'action picks stems by their places in that list from 0'
383
+ )
384
+ if not isinstance(stems, list) or not 0 < len(stems) <= 16:
385
+ raise ArgumentError(f'expected music {key} with one to sixteen stems')
386
+ return {
387
+ 'stems': [file(s, f'music {key}', _SOUNDS) for s in stems],
388
+ 'bpm': _real(spec.get('bpm', 120), 20, 300, f'the bpm of music {key}'),
389
+ 'beats': int(_real(spec.get('beats', 4), 1, 16, f'the beats of music {key}')),
390
+ 'gain': _real(spec.get('gain', 1), 0, 1, f'the gain of music {key}'),
391
+ }
392
+
393
+
394
+ def _processor(key: str, spec: Any) -> dict[str, Any]:
395
+ _name(key, 'the name of a processor')
396
+ if not isinstance(spec, dict) or not isinstance(spec.get('processor'), str):
397
+ raise ArgumentError(f'expected processor {key} to name a registered processor')
398
+ parameters = spec.get('parameters', {})
399
+ if not isinstance(parameters, dict) or len(parameters) > 32:
400
+ raise ArgumentError(f'expected the parameters of processor {key} as a mapping')
401
+ bus = spec.get('bus', 'effects')
402
+ if bus not in BUSES:
403
+ raise ArgumentError(f'expected the bus of processor {key} among {BUSES}')
404
+ options = spec.get('options', {})
405
+ return {
406
+ 'processor': _name(spec['processor'], 'a processor name'),
407
+ 'bus': bus,
408
+ 'parameters': {
409
+ _name(k, 'a parameter'): _real(v, -1e6, 1e6, 'a parameter')
410
+ for k, v in parameters.items()
411
+ },
412
+ 'options': options if isinstance(options, dict) else {},
413
+ }
414
+
415
+
416
+ def audio(value: Any, file: Any, warnings: list[str]) -> dict[str, Any]:
417
+ """Checks the audio of a manifest, its cues, music, worklet and processors."""
418
+ if not isinstance(value, dict):
419
+ raise ArgumentError('expected audio as a mapping')
420
+ out: dict[str, Any] = {}
421
+ cues = value.get('cues', {})
422
+ if not isinstance(cues, dict) or len(cues) > 64:
423
+ raise ArgumentError('expected the cues of audio as a mapping of at most 64 names')
424
+ out['cues'] = {}
425
+ for key, files in cues.items():
426
+ _name(key, 'the name of a cue')
427
+ files = files if isinstance(files, list) else [files]
428
+ if not 0 < len(files) <= 16:
429
+ raise ArgumentError(f'expected one to sixteen files of cue {key}')
430
+ out['cues'][key] = [file(item, f'cue {key}', _SOUNDS) for item in files]
431
+ music = value.get('music', {})
432
+ if not isinstance(music, dict) or len(music) > 16:
433
+ raise ArgumentError('expected music as a mapping of at most 16 names')
434
+ out['music'] = {}
435
+ for key, spec in music.items():
436
+ try:
437
+ out['music'][key] = _music(key, spec, file)
438
+ except ArgumentError as error:
439
+ warnings.append(f'music {key} left out, {error}')
440
+ if 'worklet' in value:
441
+ out['worklet'] = file(value['worklet'], 'the worklet', ('.js',))
442
+ processors = value.get('processors', {})
443
+ if not isinstance(processors, dict) or len(processors) > MAXIMUM_PROCESSORS:
444
+ raise ArgumentError(f'expected at most {MAXIMUM_PROCESSORS} processors')
445
+ out['processors'] = {}
446
+ for key, spec in processors.items():
447
+ try:
448
+ if 'worklet' not in out:
449
+ raise ArgumentError('expected the worklet module of the processors')
450
+ out['processors'][key] = _processor(key, spec)
451
+ except ArgumentError as error:
452
+ warnings.append(f'processor {key} left out, {error}')
453
+ for key in sorted(set(value) - {'cues', 'music', 'worklet', 'processors'}):
454
+ warnings.append(f'audio ignores {key}')
455
+ return out
456
+
457
+
458
+ # The parts of an environment moment and the feature each needs.
459
+ ENVIRONMENT = {
460
+ 'sky': 'environment.sky',
461
+ 'floodlights': 'environment.sky',
462
+ 'fog': 'environment.sky',
463
+ 'lightning': 'environment.lightning',
464
+ 'tifo': 'fans.tifo',
465
+ }
466
+
467
+
468
+ def environment(value: Any, what: str) -> dict[str, Any]:
469
+ """Checks an environment moment of a reflex or a worker, brief and on the team's own event."""
470
+ if (
471
+ not isinstance(value, dict)
472
+ or not set(value) <= {*ENVIRONMENT, 'duration'}
473
+ or not set(value) & set(ENVIRONMENT)
474
+ ):
475
+ raise ArgumentError(f'expected the environment of {what} among {tuple(ENVIRONMENT)}')
476
+ out: dict[str, Any] = {'duration': _real(value.get('duration', 4), 0.3, 6, 'a duration')}
477
+ for key in ('sky', 'floodlights', 'fog'):
478
+ if key in value:
479
+ part = value[key]
480
+ if not isinstance(part, dict):
481
+ raise ArgumentError(f'expected the {key} of {what} as a description')
482
+ out[key] = {
483
+ 'color': _rgb(part.get('tint', part.get('color', [255, 255, 255])), 'a colour'),
484
+ 'amount': _real(part.get('amount', part.get('level', 1)), 0, 1.5, 'an amount'),
485
+ }
486
+ if 'lightning' in value:
487
+ part = value['lightning']
488
+ target = part.get('target', 'goal') if isinstance(part, dict) else 'goal'
489
+ if target not in ('goal', 'ball') and not (isinstance(target, list) and len(target) == 3):
490
+ raise ArgumentError(f'expected the lightning of {what} at the goal or the ball')
491
+ out['lightning'] = {'target': target}
492
+ if 'tifo' in value:
493
+ if not isinstance(value['tifo'], str):
494
+ raise ArgumentError(f'expected the tifo of {what} to name an image of the package')
495
+ out['tifo'] = value['tifo']
496
+ return out
497
+
498
+
499
+ def _time(value: Any, what: str) -> dict[str, Any]:
500
+ if not isinstance(value, dict):
501
+ raise ArgumentError(f'expected the time of {what} as a description')
502
+ out: dict[str, Any] = {'freeze': _real(value.get('freeze', 0), 0, 0.3, 'a freeze')}
503
+ if 'slow' in value:
504
+ slow = value['slow']
505
+ if not isinstance(slow, dict):
506
+ raise ArgumentError(f'expected the slow motion of {what} as a description')
507
+ out['slow'] = {
508
+ 'speed': _real(slow.get('speed', 0.5), 0.25, 1, 'a slow motion speed'),
509
+ 'seconds': _real(slow.get('seconds', 1), 0, 1.5, 'a slow motion length'),
510
+ }
511
+ return out
512
+
513
+
514
+ def _action(action: Any, index: int, names: dict[str, Any]) -> dict[str, Any]:
515
+ if not isinstance(action, dict) or not any(key in action for key in ACTIONS):
516
+ raise ArgumentError(f'expected action {index} to name one of {ACTIONS}')
517
+ out = dict(action)
518
+ if 'audio' in action and action['audio'] not in names.get('cues', {}):
519
+ raise ArgumentError(
520
+ f'expected action {index} to play a cue of the audio, found {action["audio"]!r}, '
521
+ f'{_among(names.get("cues", {}), "cues")}'
522
+ )
523
+ if 'effect' in action and action['effect'] not in names.get('effects', {}):
524
+ raise ArgumentError(
525
+ f'expected action {index} to start an effect of the manifest, found '
526
+ f'{action["effect"]!r}, {_among(names.get("effects", {}), "effects")}'
527
+ )
528
+ if 'clip' in action and not isinstance(action['clip'], str):
529
+ raise ArgumentError(f'expected action {index} to name a clip role')
530
+ if 'claim' in action and action['claim'] not in CLAIMS:
531
+ raise ArgumentError(f'expected the claim of action {index} among {CLAIMS}')
532
+ if 'attach' in action and not (
533
+ isinstance(action['attach'], str) and _ATTACH.fullmatch(action['attach'])
534
+ ):
535
+ raise ArgumentError(f'expected action {index} to attach to the ball, a socket or self')
536
+ for key, choices in (('class', CLASSES), ('bus', BUSES), ('replay', REPLAY)):
537
+ if key in action and action[key] not in choices:
538
+ raise ArgumentError(f'expected the {key} of action {index} among {choices}')
539
+ if 'overlay' in action and action['overlay'] not in SLOTS:
540
+ raise ArgumentError(f'expected the overlay of action {index} among {SLOTS}')
541
+ if 'camera' in action and (
542
+ not isinstance(action['camera'], dict) or action['camera'].get('shot') not in SHOTS
543
+ ):
544
+ raise ArgumentError(f'expected the camera of action {index} to suggest one of {SHOTS}')
545
+ if 'gain' in action:
546
+ gain = action['gain']
547
+ if isinstance(gain, bool) or not isinstance(gain, (int, float)) or not 0 <= gain <= 1:
548
+ raise ArgumentError(f'expected the gain of action {index} from 0 to 1')
549
+ if 'environment' in action:
550
+ out['environment'] = environment(action['environment'], f'action {index}')
551
+ tifo = out['environment'].get('tifo')
552
+ if tifo is not None and tifo not in names.get('files', ()):
553
+ raise ArgumentError(f'expected the tifo of action {index} to name an image')
554
+ if 'time' in action:
555
+ out['time'] = _time(action['time'], f'action {index}')
556
+ if 'music' in action and action['music'] not in names.get('music', {}):
557
+ raise ArgumentError(
558
+ f'expected action {index} to play music of the audio, found {action["music"]!r}, '
559
+ f'{_among(names.get("music", {}), "music")}'
560
+ )
561
+ return out
562
+
563
+
564
+ def _among(names: Any, what: str) -> str:
565
+ """Returns the end of a message that names what a manifest holds, or that it holds none."""
566
+ names = sorted(str(name) for name in names)
567
+ if not names:
568
+ return f'and the manifest keeps no {what}'
569
+ shown = ', '.join(names[:8]) + (f' and {len(names) - 8} more' if len(names) > 8 else '')
570
+ return f'the {what} are {shown}'
571
+
572
+
573
+ def _event(value: Any) -> str:
574
+ """Returns the end of a message for a reflex on an event outside the catalog."""
575
+ near = difflib.get_close_matches(value, EVENTS, 1, 0.5) if isinstance(value, str) else []
576
+ hint = f', the nearest is {near[0]}' if near else ''
577
+ return f'found {value!r}{hint}, and the section Events of docs/presentation.md lists them'
578
+
579
+
580
+ def reflexes(value: Any, names: dict[str, Any], warnings: list[str]) -> list[dict[str, Any]]:
581
+ """Checks the reflexes of a manifest, each an event, a filter and the actions it starts."""
582
+ if not isinstance(value, list) or len(value) > MAXIMUM_REFLEXES:
583
+ raise ArgumentError(f'expected reflexes as a list of at most {MAXIMUM_REFLEXES}')
584
+ out = []
585
+ # Reflexes and their actions count from 1 in messages, as an author counts them.
586
+ for index, spec in enumerate(value, 1):
587
+ try:
588
+ if not isinstance(spec, dict) or spec.get('on') not in EVENTS:
589
+ found = _event(spec.get('on') if isinstance(spec, dict) else spec)
590
+ raise ArgumentError(f'expected reflex {index} on an event of the catalog, {found}')
591
+ item: dict[str, Any] = {'on': spec['on'], 'phase': spec.get('phase', 'occur')}
592
+ if item['phase'] not in PHASES:
593
+ raise ArgumentError(f'expected the phase of reflex {index} among {PHASES}')
594
+ where = spec.get('where', {})
595
+ if not isinstance(where, dict) or where.get('team', 'self') not in TEAMS:
596
+ raise ArgumentError(f'expected reflex {index} to filter its team among {TEAMS}')
597
+ item['where'] = {'team': where.get('team', 'self')}
598
+ if where.get('role') in ('goalkeeper', 'outfield'):
599
+ item['where']['role'] = where['role']
600
+ if 'player' in where:
601
+ item['where']['player'] = where['player']
602
+ actions = spec.get('do')
603
+ if not isinstance(actions, list) or not 0 < len(actions) <= MAXIMUM_ACTIONS:
604
+ raise ArgumentError(f'expected one to {MAXIMUM_ACTIONS} actions in reflex {index}')
605
+ item['do'] = [_action(action, i, names) for i, action in enumerate(actions, 1)]
606
+ out.append(item)
607
+ except ArgumentError as error:
608
+ warnings.append(f'reflex {index} left out, {error}')
609
+ return out
@@ -0,0 +1,3 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Requested matches, games, the demo, challenges, achievements and policy errors."""