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.
- cuju/render/__init__.py +759 -0
- cuju/render/__main__.py +1065 -0
- cuju/render/_export/__init__.py +3 -0
- cuju/render/_export/feed.py +542 -0
- cuju/render/_export/sizes.py +157 -0
- cuju/render/_export/video.py +1618 -0
- cuju/render/_export/video_jobs.py +408 -0
- cuju/render/_format/__init__.py +3 -0
- cuju/render/_format/annotations.py +235 -0
- cuju/render/_format/codec.py +307 -0
- cuju/render/_format/keep.py +121 -0
- cuju/render/_format/recording.py +860 -0
- cuju/render/_format/writer.py +54 -0
- cuju/render/_guide/__init__.py +3 -0
- cuju/render/_guide/doctor.py +523 -0
- cuju/render/_guide/help.py +62 -0
- cuju/render/_guide/tutorial.py +254 -0
- cuju/render/_http/__init__.py +3 -0
- cuju/render/_http/commands.py +57 -0
- cuju/render/_http/headers.py +103 -0
- cuju/render/_http/launch.py +500 -0
- cuju/render/_http/launchers.py +408 -0
- cuju/render/_http/pwa.py +128 -0
- cuju/render/_http/server.py +2105 -0
- cuju/render/_http/window.py +360 -0
- cuju/render/_moments/__init__.py +3 -0
- cuju/render/_moments/captions.py +284 -0
- cuju/render/_moments/events.py +1123 -0
- cuju/render/_moments/search.py +694 -0
- cuju/render/_packages/__init__.py +3 -0
- cuju/render/_packages/glsl.py +746 -0
- cuju/render/_packages/loudness.py +334 -0
- cuju/render/_packages/pack.py +1959 -0
- cuju/render/_packages/presentation.py +1376 -0
- cuju/render/_packages/vocabulary.py +609 -0
- cuju/render/_playing/__init__.py +3 -0
- cuju/render/_playing/achievements.py +463 -0
- cuju/render/_playing/bench.py +609 -0
- cuju/render/_playing/challenges.py +1813 -0
- cuju/render/_playing/demo.py +323 -0
- cuju/render/_playing/draw.py +211 -0
- cuju/render/_playing/errors.py +20 -0
- cuju/render/_playing/game.py +645 -0
- cuju/render/_playing/limits.py +339 -0
- cuju/render/_playing/play.py +926 -0
- cuju/render/_playing/policy_worker.py +250 -0
- cuju/render/_playing/strict.py +810 -0
- cuju/render/_registry/__init__.py +3 -0
- cuju/render/_registry/clips.py +394 -0
- cuju/render/_registry/joints.py +108 -0
- cuju/render/_registry/teams.py +2058 -0
- cuju/render/_store/__init__.py +3 -0
- cuju/render/_store/home.py +70 -0
- cuju/render/_store/library.py +604 -0
- cuju/render/_store/samples.py +163 -0
- cuju/render/_text/__init__.py +3 -0
- cuju/render/_text/languages.py +637 -0
- cuju/render/_text/messages.py +1396 -0
- cuju/render/_tournaments/__init__.py +3 -0
- cuju/render/_tournaments/results.py +547 -0
- cuju/render/_tournaments/tournament.py +2742 -0
- cuju/render/_tournaments/tournament_cli.py +587 -0
- cuju/render/_tournaments/tournament_runner.py +1975 -0
- cuju/render/_version.py +6 -0
- cuju/render/_watch/__init__.py +3 -0
- cuju/render/_watch/grid.py +265 -0
- cuju/render/_watch/live.py +664 -0
- cuju/render/_watch/notebook.py +79 -0
- cuju/render/_watch/show.py +171 -0
- cuju/render/samples/sample-11v11.cjr +0 -0
- cuju/render/samples/sample-5v5.cjr +0 -0
- cuju/render/samples/teams/starter/main.py +33 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_easy.py +46 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_hard.py +46 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_medium.py +46 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_easy.py +34 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_hard.py +34 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_medium.py +34 -0
- cuju/render/web/assets/adboard_polygon.webp +0 -0
- cuju/render/web/assets/adboard_your_ad_here.webp +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-combatmelee.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-general.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-movementadvanced.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-movementbasic.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-simulation.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-special.glb +0 -0
- cuju/render/web/assets/ball.glb +0 -0
- cuju/render/web/assets/challenges/3_vs_1_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/3_vs_1_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/corner.mp4 +0 -0
- cuju/render/web/assets/challenges/corner.webp +0 -0
- cuju/render/web/assets/challenges/counterattack_easy.mp4 +0 -0
- cuju/render/web/assets/challenges/counterattack_easy.webp +0 -0
- cuju/render/web/assets/challenges/counterattack_hard.mp4 +0 -0
- cuju/render/web/assets/challenges/counterattack_hard.webp +0 -0
- cuju/render/web/assets/challenges/empty_goal.mp4 +0 -0
- cuju/render/web/assets/challenges/empty_goal.webp +0 -0
- cuju/render/web/assets/challenges/empty_goal_close.mp4 +0 -0
- cuju/render/web/assets/challenges/empty_goal_close.webp +0 -0
- cuju/render/web/assets/challenges/manifest.json +375 -0
- cuju/render/web/assets/challenges/one_on_one.mp4 +0 -0
- cuju/render/web/assets/challenges/one_on_one.webp +0 -0
- cuju/render/web/assets/challenges/one_on_one_hard.mp4 +0 -0
- cuju/render/web/assets/challenges/one_on_one_hard.webp +0 -0
- cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/run_to_score.mp4 +0 -0
- cuju/render/web/assets/challenges/run_to_score.webp +0 -0
- cuju/render/web/assets/challenges/run_to_score_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/run_to_score_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/single_goal_versus_lazy.mp4 +0 -0
- cuju/render/web/assets/challenges/single_goal_versus_lazy.webp +0 -0
- cuju/render/web/assets/crowd.webp +0 -0
- cuju/render/web/assets/crowd_normal.webp +0 -0
- cuju/render/web/assets/cuju-logo-384.png +0 -0
- cuju/render/web/assets/cuju-logo.png +0 -0
- cuju/render/web/assets/cuju-logo.svg +46 -0
- cuju/render/web/assets/cuju-mark-light.svg +33 -0
- cuju/render/web/assets/cuju-mark.svg +33 -0
- cuju/render/web/assets/goal.glb +0 -0
- cuju/render/web/assets/grass.webp +0 -0
- cuju/render/web/assets/hair_black.png +0 -0
- cuju/render/web/assets/hair_blonde.png +0 -0
- cuju/render/web/assets/hair_brown.png +0 -0
- cuju/render/web/assets/hair_darkblonde.png +0 -0
- cuju/render/web/assets/hair_long01.glb +0 -0
- cuju/render/web/assets/hair_long02.glb +0 -0
- cuju/render/web/assets/hair_medium01.glb +0 -0
- cuju/render/web/assets/hair_red.png +0 -0
- cuju/render/web/assets/hair_short01.glb +0 -0
- cuju/render/web/assets/hair_short02.glb +0 -0
- cuju/render/web/assets/icons/cuju-192.png +0 -0
- cuju/render/web/assets/icons/cuju-512.png +0 -0
- cuju/render/web/assets/icons/cuju-maskable-512.png +0 -0
- cuju/render/web/assets/kit_goalkeeper.webp +0 -0
- cuju/render/web/assets/kit_mask.png +0 -0
- cuju/render/web/assets/kit_normal.webp +0 -0
- cuju/render/web/assets/manifest.json +601 -0
- cuju/render/web/assets/models/kaykit-barbarian.glb +0 -0
- cuju/render/web/assets/models/kaykit-barbarian.json +23 -0
- cuju/render/web/assets/models/kaykit-knight.glb +0 -0
- cuju/render/web/assets/models/kaykit-knight.json +23 -0
- cuju/render/web/assets/models/kaykit-mage.glb +0 -0
- cuju/render/web/assets/models/kaykit-mage.json +23 -0
- cuju/render/web/assets/models/kaykit-mannequin-medium.glb +0 -0
- cuju/render/web/assets/models/kaykit-mannequin-medium.json +23 -0
- cuju/render/web/assets/models/kaykit-ranger.glb +0 -0
- cuju/render/web/assets/models/kaykit-ranger.json +23 -0
- cuju/render/web/assets/models/kaykit-rogue-hooded.glb +0 -0
- cuju/render/web/assets/models/kaykit-rogue-hooded.json +23 -0
- cuju/render/web/assets/models/kaykit-rogue.glb +0 -0
- cuju/render/web/assets/models/kaykit-rogue.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-mage.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-mage.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-minion.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-minion.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-rogue.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-rogue.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-warrior.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-warrior.json +23 -0
- cuju/render/web/assets/pitch.webp +0 -0
- cuju/render/web/assets/pitch_normal.webp +0 -0
- cuju/render/web/assets/player.glb +0 -0
- cuju/render/web/assets/shoe.webp +0 -0
- cuju/render/web/assets/shoe_sole.png +0 -0
- cuju/render/web/assets/skin_01.png +0 -0
- cuju/render/web/assets/skin_02.png +0 -0
- cuju/render/web/assets/skin_03.png +0 -0
- cuju/render/web/assets/skin_04.png +0 -0
- cuju/render/web/assets/sounds/applause.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-1.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-2.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-3.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-4.ogg +0 -0
- cuju/render/web/assets/sounds/city-horn.ogg +0 -0
- cuju/render/web/assets/sounds/city-loop.ogg +0 -0
- cuju/render/web/assets/sounds/city-siren.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-cheer.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-loop.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-ooh.ogg +0 -0
- cuju/render/web/assets/sounds/gulls.ogg +0 -0
- cuju/render/web/assets/sounds/neon-sting.ogg +0 -0
- cuju/render/web/assets/sounds/rain-loop.ogg +0 -0
- cuju/render/web/assets/sounds/sounds.json +46 -0
- cuju/render/web/assets/sounds/space-beep.ogg +0 -0
- cuju/render/web/assets/sounds/space-hum.ogg +0 -0
- cuju/render/web/assets/sounds/space-radio.ogg +0 -0
- cuju/render/web/assets/sounds/whistle-long.ogg +0 -0
- cuju/render/web/assets/sounds/whistle-short.ogg +0 -0
- cuju/render/web/assets/sounds/wind-loop.ogg +0 -0
- cuju/render/web/easy.css +357 -0
- cuju/render/web/export.html +23 -0
- cuju/render/web/feed.css +262 -0
- cuju/render/web/feed.html +28 -0
- cuju/render/web/flat.css +120 -0
- cuju/render/web/home.css +837 -0
- cuju/render/web/index.html +373 -0
- cuju/render/web/js/boot.js +50 -0
- cuju/render/web/js/broadcast/audio.js +2141 -0
- cuju/render/web/js/broadcast/celebrate.js +1238 -0
- cuju/render/web/js/broadcast/decisions.js +609 -0
- cuju/render/web/js/broadcast/hud.js +352 -0
- cuju/render/web/js/broadcast/show.js +3614 -0
- cuju/render/web/js/broadcast/transition.js +2231 -0
- cuju/render/web/js/broadcast/voices.js +153 -0
- cuju/render/web/js/capture.js +101 -0
- cuju/render/web/js/core/accessibility.js +90 -0
- cuju/render/web/js/core/api.js +429 -0
- cuju/render/web/js/core/comfort.js +135 -0
- cuju/render/web/js/core/i18n.js +705 -0
- cuju/render/web/js/core/icons.js +167 -0
- cuju/render/web/js/core/input-settings.js +156 -0
- cuju/render/web/js/core/keys.js +277 -0
- cuju/render/web/js/core/language-ui.js +125 -0
- cuju/render/web/js/core/menu.js +853 -0
- cuju/render/web/js/core/names.js +113 -0
- cuju/render/web/js/core/page-structure.js +110 -0
- cuju/render/web/js/core/scenarios.js +109 -0
- cuju/render/web/js/core/spoilers.js +236 -0
- cuju/render/web/js/core/typeahead.js +38 -0
- cuju/render/web/js/export/dom-picture.js +330 -0
- cuju/render/web/js/export/export-host.js +97 -0
- cuju/render/web/js/export/feed-dialog.js +191 -0
- cuju/render/web/js/export/feed-params.js +266 -0
- cuju/render/web/js/export/feed.js +363 -0
- cuju/render/web/js/export/mp4-mux.js +295 -0
- cuju/render/web/js/export/photo-core.js +313 -0
- cuju/render/web/js/export/photo.js +1314 -0
- cuju/render/web/js/export/safe-frame.js +59 -0
- cuju/render/web/js/export/size-fields.js +103 -0
- cuju/render/web/js/export/sizes.js +178 -0
- cuju/render/web/js/export/video-boot.js +55 -0
- cuju/render/web/js/export/video-browser.js +325 -0
- cuju/render/web/js/export/video-export.js +417 -0
- cuju/render/web/js/export/video-plan.js +207 -0
- cuju/render/web/js/export/video-range.js +185 -0
- cuju/render/web/js/export/webm-mux.js +198 -0
- cuju/render/web/js/feed-boot.js +151 -0
- cuju/render/web/js/feed-frame.js +101 -0
- cuju/render/web/js/help/doctor.js +488 -0
- cuju/render/web/js/help/firstrun.js +379 -0
- cuju/render/web/js/help/help-center.js +438 -0
- cuju/render/web/js/help/install-offer.js +164 -0
- cuju/render/web/js/help/install-state.js +104 -0
- cuju/render/web/js/help/lessons-core.js +207 -0
- cuju/render/web/js/help/lessons.js +463 -0
- cuju/render/web/js/help/palette.js +584 -0
- cuju/render/web/js/help/pwa.js +257 -0
- cuju/render/web/js/help/tour-core.js +241 -0
- cuju/render/web/js/help/tour.js +461 -0
- cuju/render/web/js/main.js +2513 -0
- cuju/render/web/js/pages/draw.js +188 -0
- cuju/render/web/js/pages/fieldpage.js +530 -0
- cuju/render/web/js/pages/home.js +521 -0
- cuju/render/web/js/pages/matches.js +779 -0
- cuju/render/web/js/pages/moments-ui.js +359 -0
- cuju/render/web/js/pages/queue-filters.js +63 -0
- cuju/render/web/js/pages/recording-groups.js +80 -0
- cuju/render/web/js/pages/recordings.js +1069 -0
- cuju/render/web/js/play/bench.js +347 -0
- cuju/render/web/js/play/challenges.js +1302 -0
- cuju/render/web/js/play/medals.js +127 -0
- cuju/render/web/js/play/play.js +1953 -0
- cuju/render/web/js/play/teasers.js +196 -0
- cuju/render/web/js/play/touch.js +350 -0
- cuju/render/web/js/play/tournament-layout.js +487 -0
- cuju/render/web/js/play/tournament-results.js +441 -0
- cuju/render/web/js/play/tournament-strict.js +560 -0
- cuju/render/web/js/play/tournament.js +2585 -0
- cuju/render/web/js/players/animate.js +972 -0
- cuju/render/web/js/players/clips.js +224 -0
- cuju/render/web/js/players/kaykit.js +162 -0
- cuju/render/web/js/players/kits.js +676 -0
- cuju/render/web/js/players/number-patch.js +82 -0
- cuju/render/web/js/players/players.js +2694 -0
- cuju/render/web/js/players/recolor.js +893 -0
- cuju/render/web/js/players/retarget.js +325 -0
- cuju/render/web/js/players/rig-numbers.js +174 -0
- cuju/render/web/js/presentation/animator.js +231 -0
- cuju/render/web/js/presentation/assign.js +85 -0
- cuju/render/web/js/presentation/behaviors.js +440 -0
- cuju/render/web/js/presentation/budget.js +209 -0
- cuju/render/web/js/presentation/catalog.js +1674 -0
- cuju/render/web/js/presentation/clocks.js +72 -0
- cuju/render/web/js/presentation/commands.js +301 -0
- cuju/render/web/js/presentation/contract.js +240 -0
- cuju/render/web/js/presentation/effects.js +904 -0
- cuju/render/web/js/presentation/flash.js +605 -0
- cuju/render/web/js/presentation/freeze-worklet.js +22 -0
- cuju/render/web/js/presentation/glsl.js +464 -0
- cuju/render/web/js/presentation/host.js +323 -0
- cuju/render/web/js/presentation/index.js +2066 -0
- cuju/render/web/js/presentation/loudness-worklet.js +235 -0
- cuju/render/web/js/presentation/loudness.js +740 -0
- cuju/render/web/js/presentation/moments.js +536 -0
- cuju/render/web/js/presentation/post.js +327 -0
- cuju/render/web/js/presentation/preview.js +180 -0
- cuju/render/web/js/presentation/protocol.js +174 -0
- cuju/render/web/js/presentation/reach.js +158 -0
- cuju/render/web/js/presentation/rigging.js +130 -0
- cuju/render/web/js/presentation/sandbox.js +157 -0
- cuju/render/web/js/presentation/shaders.js +235 -0
- cuju/render/web/js/presentation/stage.js +402 -0
- cuju/render/web/js/presentation/teamaudio.js +722 -0
- cuju/render/web/js/presentation/tracks.js +197 -0
- cuju/render/web/js/presentation/values.js +81 -0
- cuju/render/web/js/recording/annotation-data.js +203 -0
- cuju/render/web/js/recording/captions.js +491 -0
- cuju/render/web/js/recording/codec.js +185 -0
- cuju/render/web/js/recording/decode-worker.js +70 -0
- cuju/render/web/js/recording/decode.js +94 -0
- cuju/render/web/js/recording/episodes.js +46 -0
- cuju/render/web/js/recording/format.js +461 -0
- cuju/render/web/js/recording/live.js +348 -0
- cuju/render/web/js/recording/moments-query.js +59 -0
- cuju/render/web/js/recording/moments.js +149 -0
- cuju/render/web/js/recording/pose.js +86 -0
- cuju/render/web/js/recording/timeline.js +275 -0
- cuju/render/web/js/teams/art.js +50 -0
- cuju/render/web/js/teams/clip-editor.js +293 -0
- cuju/render/web/js/teams/editor.js +1979 -0
- cuju/render/web/js/teams/library.js +344 -0
- cuju/render/web/js/teams/painter.js +633 -0
- cuju/render/web/js/teams/surprise-ui.js +195 -0
- cuju/render/web/js/teams/surprise.js +497 -0
- cuju/render/web/js/teams/team-check.js +127 -0
- cuju/render/web/js/watch/annotations.js +270 -0
- cuju/render/web/js/watch/flat.js +366 -0
- cuju/render/web/js/watch/grid.js +436 -0
- cuju/render/web/js/watch/pitch2d.js +289 -0
- cuju/render/web/js/watch/player-tools.js +192 -0
- cuju/render/web/js/watch/presets.js +201 -0
- cuju/render/web/js/world/arenas.js +997 -0
- cuju/render/web/js/world/ball.js +204 -0
- cuju/render/web/js/world/cameras.js +982 -0
- cuju/render/web/js/world/crowd.js +376 -0
- cuju/render/web/js/world/field.js +227 -0
- cuju/render/web/js/world/framing.js +145 -0
- cuju/render/web/js/world/occlusion.js +48 -0
- cuju/render/web/js/world/pitch.js +703 -0
- cuju/render/web/js/world/scene.js +395 -0
- cuju/render/web/js/world/sky.js +325 -0
- cuju/render/web/js/world/stadium.js +1262 -0
- cuju/render/web/js/world/vendor-sky.js +367 -0
- cuju/render/web/js/world/weather.js +264 -0
- cuju/render/web/languages/en.json +4750 -0
- cuju/render/web/lessons.json +137 -0
- cuju/render/web/manifest.webmanifest +23 -0
- cuju/render/web/style.css +8399 -0
- cuju/render/web/sw.js +235 -0
- cuju/render/web/vendor/fabric/LICENSE +16 -0
- cuju/render/web/vendor/fabric/fabric.min.mjs +1 -0
- cuju/render/web/vendor/lucide/LICENSE +43 -0
- cuju/render/web/vendor/lucide/icons.js +141 -0
- cuju/render/web/vendor/three/LICENSE +21 -0
- cuju/render/web/vendor/three/build/three.core.js +60586 -0
- cuju/render/web/vendor/three/build/three.module.js +19719 -0
- cuju/render/web/vendor/three/examples/jsm/loaders/GLTFLoader.js +4925 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/EffectComposer.js +365 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/MaskPass.js +195 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/OutputPass.js +148 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/Pass.js +191 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/RenderPass.js +193 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/ShaderPass.js +135 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/UnrealBloomPass.js +541 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/CopyShader.js +52 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/LuminosityHighPassShader.js +68 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/OutputShader.js +103 -0
- cuju/render/web/vendor/three/examples/jsm/utils/BufferGeometryUtils.js +1501 -0
- cuju/render/web/vendor/three/examples/jsm/utils/SkeletonUtils.js +496 -0
- cuju_render-0.1.0.dist-info/METADATA +116 -0
- cuju_render-0.1.0.dist-info/RECORD +382 -0
- cuju_render-0.1.0.dist-info/WHEEL +4 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSE +202 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +396 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/CC0-1.0.txt +127 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/ISC.txt +16 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/MIT.txt +24 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/Unlicense.txt +13 -0
- cuju_render-0.1.0.dist-info/licenses/NOTICE +35 -0
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The Matches page, where two teams are picked for a match that plays on this computer, and the
|
|
5
|
+
// queue of requested matches with their progress, cancelling and watching.
|
|
6
|
+
|
|
7
|
+
import { api, controlLabel, uiLocale } from '../core/api.js';
|
|
8
|
+
import { formatNumber, serverText, t, tn } from '../core/i18n.js';
|
|
9
|
+
import { css, normalizeStyle } from '../players/kits.js';
|
|
10
|
+
import { Dropdown, el as h, enhanceSelect, menuItem } from '../core/menu.js';
|
|
11
|
+
import { icon, stateBadge, symbolOnly } from '../core/icons.js';
|
|
12
|
+
import { keptName, teamName } from '../core/names.js';
|
|
13
|
+
import { listedScenarios, scenarioName, scenarioText } from '../core/scenarios.js';
|
|
14
|
+
import { RANDOM, againOf, drawableTeams, drawnParts, fits, fittingScenarios, hiddenPart, requestOf, scenarioChoices, unfit, wasDrawn } from './draw.js';
|
|
15
|
+
import { filterScenarios, filterTeams, passesScenario, passesTeam, scenarioValue, shownTeams, teamValue, unknownTeams } from './queue-filters.js';
|
|
16
|
+
|
|
17
|
+
export { fits } from './draw.js';
|
|
18
|
+
|
|
19
|
+
const DEFAULT_SCENARIO = '11_vs_11_kaggle';
|
|
20
|
+
const FORMAT_NAMES = { '5v5': t('matches.format.5v5'), '11v11': t('matches.format.11v11') };
|
|
21
|
+
// The list of requested matches shows this many more rows each time Show more is pressed.
|
|
22
|
+
export const REQUEST_PAGE = 50;
|
|
23
|
+
|
|
24
|
+
// A small shirt in a team's kit, its pattern drawn with the same classes as the editor's.
|
|
25
|
+
export function kitShirt(team, side = 0, size = 'small') {
|
|
26
|
+
const style = normalizeStyle(team, side);
|
|
27
|
+
const k = style.kit;
|
|
28
|
+
// The shirt is clipped to its outline, the wrapper draws an edge around it that shows a
|
|
29
|
+
// white shirt on a white page.
|
|
30
|
+
return h('span', { class: `kit-shirt ${size}`, 'aria-hidden': 'true' },
|
|
31
|
+
h('span', {
|
|
32
|
+
class: `kit-shirt-body p-${k.pattern}`,
|
|
33
|
+
style: `--a:${css(k.primary)};--b:${css(k.secondary)};--c:${css(k.shorts)}`,
|
|
34
|
+
}, style.badge ? h('b', { class: 'kit-badge' }, style.badge) : null));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The state of a request, a failed one by the id of its message, or by its English text for an
|
|
38
|
+
// entry of a server that named no id.
|
|
39
|
+
function stateOf(r) {
|
|
40
|
+
const id = typeof r.error_id === 'string' ? r.error_id : null;
|
|
41
|
+
if (r.state === 'failed' && (id ? id === 'server.requests.cancelled' : r.error === 'cancelled')) {
|
|
42
|
+
return 'cancelled';
|
|
43
|
+
}
|
|
44
|
+
// A restart of the server marks the requests that waited or played as interrupted.
|
|
45
|
+
const interrupted = id ? id.startsWith('server.requests.interrupted')
|
|
46
|
+
: typeof r.error === 'string' && r.error.startsWith('interrupted by a server restart');
|
|
47
|
+
if (r.state === 'failed' && interrupted) {
|
|
48
|
+
return 'interrupted';
|
|
49
|
+
}
|
|
50
|
+
return r.state;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const STATE_LABELS = {
|
|
54
|
+
queued: t('matches.state.queued'), running: t('matches.state.running'), done: t('matches.state.done'),
|
|
55
|
+
failed: t('matches.state.failed'), cancelled: t('matches.state.cancelled'), interrupted: t('matches.state.interrupted'),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// The players that a submission controls, one when its setting names none.
|
|
59
|
+
function controlledPlayers(team) {
|
|
60
|
+
const players = team && team.control ? team.control.players : null;
|
|
61
|
+
return Number.isInteger(players) && players > 0 ? players : 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// An ability in the numbers of the language, with two decimals.
|
|
65
|
+
function abilityText(value) {
|
|
66
|
+
return formatNumber(Number(value), { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The time a request was made, its saved time, or for an older entry the time in its identifier,
|
|
70
|
+
// r-YYYYMMDD-HHMMSS-xxxx.
|
|
71
|
+
function requestTime(r) {
|
|
72
|
+
if (typeof r.created === 'number' && Number.isFinite(r.created)) {
|
|
73
|
+
return new Date(r.created * 1000);
|
|
74
|
+
}
|
|
75
|
+
const m = /^r-(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})/.exec(r.id || '');
|
|
76
|
+
return m ? new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]) : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The seconds a request played, from its start to its end or to now while it plays.
|
|
80
|
+
function playedSeconds(r) {
|
|
81
|
+
if (typeof r.started !== 'number') {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const end = typeof r.finished === 'number' ? r.finished : Date.now() / 1000;
|
|
85
|
+
return Math.max(0, Math.floor(end - r.started));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class MatchesPage {
|
|
89
|
+
constructor(app) {
|
|
90
|
+
this.app = app;
|
|
91
|
+
this.root = document.getElementById('page-matches');
|
|
92
|
+
this.setTeams([]);
|
|
93
|
+
this.scenarios = [];
|
|
94
|
+
// The players of the scenarios' sides, which the server sends once and the page reads to
|
|
95
|
+
// tell the teams that can play a side, null until they come.
|
|
96
|
+
this.formations = null;
|
|
97
|
+
this.requests = [];
|
|
98
|
+
this.seen = new Set();
|
|
99
|
+
// The teams that requests named and the registry did not hold when the page looked.
|
|
100
|
+
this.looked = new Set();
|
|
101
|
+
this.primed = false;
|
|
102
|
+
// A side or the scenario may be Random, which the server draws from the seed, a side from
|
|
103
|
+
// the teams of the pool, every team but those unticked.
|
|
104
|
+
this.state = {
|
|
105
|
+
left: null, right: 'builtin', scenario: null, seed: '', ability: [null, null],
|
|
106
|
+
unticked: new Set(), sameTeam: false,
|
|
107
|
+
};
|
|
108
|
+
this.filters = { status: 'all', team: 'all', scenario: 'all', date: 'any', sort: 'newest' };
|
|
109
|
+
this.limit = REQUEST_PAGE;
|
|
110
|
+
this.error = null;
|
|
111
|
+
this.token = 0;
|
|
112
|
+
this.built = false;
|
|
113
|
+
setInterval(() => this.poll(), 1500);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get visible() {
|
|
117
|
+
return !this.root.hidden;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async refresh() {
|
|
121
|
+
const token = ++this.token;
|
|
122
|
+
if (!this.formations && !this.loadingFormations) {
|
|
123
|
+
// The players come apart from the rest, so a slow first answer holds up nothing.
|
|
124
|
+
this.loadingFormations = api.scenarioSides().then((sides) => {
|
|
125
|
+
this.formations = sides && typeof sides === 'object' ? sides : null;
|
|
126
|
+
this.updateSetup();
|
|
127
|
+
}).catch(() => null).finally(() => {
|
|
128
|
+
this.loadingFormations = null;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const [teams, scenarios, requests] = await Promise.all([
|
|
133
|
+
api.teams(), api.scenarios().catch(() => []), api.requests().catch(() => []),
|
|
134
|
+
]);
|
|
135
|
+
if (token !== this.token) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.setTeams(teams);
|
|
139
|
+
const listed = listedScenarios(scenarios);
|
|
140
|
+
this.scenarios = listed.length ? listed : [DEFAULT_SCENARIO];
|
|
141
|
+
this.requests = requests;
|
|
142
|
+
this.error = null;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
if (token !== this.token) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.error = err;
|
|
148
|
+
}
|
|
149
|
+
this.render();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The registry, and a count of its changes, so the cards of the list redraw when a team is
|
|
153
|
+
// renamed, restyled or removed.
|
|
154
|
+
setTeams(teams) {
|
|
155
|
+
const text = JSON.stringify(teams);
|
|
156
|
+
if (text !== this.teamsText) {
|
|
157
|
+
this.teamsText = text;
|
|
158
|
+
this.teamsVersion = (this.teamsVersion || 0) + 1;
|
|
159
|
+
}
|
|
160
|
+
this.teams = teams;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
team(id) {
|
|
164
|
+
return this.teams.find((x) => x.id === id) || null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// The name of a team of the registry, or for a team that the registry no longer holds the name
|
|
168
|
+
// that a request kept, marked as removed, that of `kept` or else of the newest request that
|
|
169
|
+
// shows the team, or Removed team alone for a request of before that kept none.
|
|
170
|
+
teamName(id, kept = null) {
|
|
171
|
+
if (id === RANDOM) {
|
|
172
|
+
return t('matches.team.random');
|
|
173
|
+
}
|
|
174
|
+
const x = this.team(id);
|
|
175
|
+
if (x) {
|
|
176
|
+
return teamName(x);
|
|
177
|
+
}
|
|
178
|
+
const name = keptName(kept) || this.keptName(id);
|
|
179
|
+
return name ? t('matches.team.removed.named', { name }) : t('matches.team.removed');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// The name that the newest request showing a team kept for it, or the empty text.
|
|
183
|
+
keptName(id) {
|
|
184
|
+
for (const r of this.requests) {
|
|
185
|
+
const names = Array.isArray(r.team_names) ? r.team_names : [];
|
|
186
|
+
const at = shownTeams(r).indexOf(id);
|
|
187
|
+
const name = at >= 0 ? keptName(names[at]) : '';
|
|
188
|
+
if (name) {
|
|
189
|
+
return name;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return '';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The second line of a team in the team lists and the Team filter, its kind and format, or
|
|
196
|
+
// the identifier of a team that the registry no longer holds.
|
|
197
|
+
teamHint(id) {
|
|
198
|
+
const x = this.team(id);
|
|
199
|
+
if (!x) {
|
|
200
|
+
return id;
|
|
201
|
+
}
|
|
202
|
+
return x.kind === 'builtin' ? t('matches.team.builtin.hint')
|
|
203
|
+
: FORMAT_NAMES[x.format] ? t('matches.team.submission.hint', { format: FORMAT_NAMES[x.format], count: controlledPlayers(x) }) : t('matches.team.submission');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
render() {
|
|
207
|
+
if (this.error) {
|
|
208
|
+
this.built = false;
|
|
209
|
+
this.root.replaceChildren(h('div', { class: 'page-pad' }, h('div', { class: 'notice error' },
|
|
210
|
+
h('p', {}, this.error.message),
|
|
211
|
+
h('p', { class: 'muted' }, tn('matches.offline.hint')),
|
|
212
|
+
h('button', { class: 'btn', type: 'button', ...symbolOnly(t('matches.offline.retry')), onclick: () => this.refresh() }, icon('refresh')))));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const s = this.state;
|
|
216
|
+
if (s.left !== RANDOM && !this.team(s.left)) {
|
|
217
|
+
s.left = (this.teams.find((x) => x.kind !== 'builtin') || this.teams[0] || {}).id || null;
|
|
218
|
+
}
|
|
219
|
+
if (s.right !== RANDOM && !this.team(s.right)) {
|
|
220
|
+
s.right = (this.teams.find((x) => x.kind === 'builtin') || this.teams[0] || {}).id || null;
|
|
221
|
+
}
|
|
222
|
+
if (!this.built) {
|
|
223
|
+
this.build();
|
|
224
|
+
}
|
|
225
|
+
this.updateSetup();
|
|
226
|
+
this.renderQueue();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// The page is built once, later refreshes update its parts in place.
|
|
230
|
+
build() {
|
|
231
|
+
this.built = true;
|
|
232
|
+
// A page built again, as after an error, takes away the lists of the sides it built before.
|
|
233
|
+
for (const side of this.sides || []) {
|
|
234
|
+
side.dropdown.close(false);
|
|
235
|
+
side.dropdown.panel.remove();
|
|
236
|
+
}
|
|
237
|
+
this.sides = [0, 1].map((side) => this.sidePicker(side));
|
|
238
|
+
this.scenarioSelect = h('select', { 'aria-label': t('matches.setup.scenario') });
|
|
239
|
+
this.scenarioSelect.addEventListener('change', () => {
|
|
240
|
+
this.state.scenario = this.scenarioSelect.value;
|
|
241
|
+
// The cards of random sides count the teams that the scenario lets them draw.
|
|
242
|
+
this.updateSetup();
|
|
243
|
+
});
|
|
244
|
+
this.scenarioText = h('p', { class: 'scenario-text muted small', 'aria-live': 'polite' });
|
|
245
|
+
this.seedInput = h('input', { type: 'number', min: '0', step: '1', placeholder: t('matches.setup.seed.placeholder'), value: this.state.seed, 'aria-label': t('matches.setup.seed.label') });
|
|
246
|
+
this.seedInput.addEventListener('input', () => {
|
|
247
|
+
this.state.seed = this.seedInput.value;
|
|
248
|
+
});
|
|
249
|
+
this.clash = h('div', { class: 'clash', role: 'alert', hidden: true });
|
|
250
|
+
this.pool = this.buildPool();
|
|
251
|
+
this.playButton = h('button', { type: 'button', class: 'btn primary play-btn', onclick: () => this.play() },
|
|
252
|
+
icon('play'), t('matches.setup.play'));
|
|
253
|
+
this.queueHost = h('div', { class: 'queue-list' });
|
|
254
|
+
this.filterBar = this.buildFilters();
|
|
255
|
+
const setup = h('section', { class: 'match-setup', 'aria-labelledby': 'setup-title' },
|
|
256
|
+
h('header', { class: 'setup-head' },
|
|
257
|
+
h('h2', { id: 'setup-title' }, t('matches.setup.title')),
|
|
258
|
+
h('p', { class: 'muted' }, tn('matches.setup.text'))),
|
|
259
|
+
h('div', { class: 'versus' }, this.sides[0].root, h('div', { class: 'vs', 'aria-hidden': 'true' }, t('matches.versus')), this.sides[1].root),
|
|
260
|
+
this.pool.root,
|
|
261
|
+
h('div', { class: 'setup-row' },
|
|
262
|
+
h('label', { class: 'field' }, t('matches.setup.scenario'), this.scenarioSelect),
|
|
263
|
+
h('label', { class: 'field seed-field' }, t('matches.setup.seed'),
|
|
264
|
+
h('span', { class: 'row' }, this.seedInput,
|
|
265
|
+
h('button', { type: 'button', class: 'btn ghost', ...symbolOnly(t('matches.setup.draw'), t('matches.setup.draw.title')), onclick: () => this.drawSeed() }, icon('dice'))))),
|
|
266
|
+
this.scenarioText,
|
|
267
|
+
this.clash,
|
|
268
|
+
h('div', { class: 'play-row' }, this.playButton));
|
|
269
|
+
const queue = h('section', { class: 'match-queue', 'aria-labelledby': 'queue-title' },
|
|
270
|
+
h('header', { class: 'queue-head' }, h('h2', { id: 'queue-title' }, t('matches.queue.title')), this.filterBar),
|
|
271
|
+
this.queueHost);
|
|
272
|
+
this.root.replaceChildren(h('div', { class: 'matches-layout' }, setup, queue));
|
|
273
|
+
enhanceSelect(this.scenarioSelect, { label: t('matches.setup.scenario'), className: 'wide' });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// The short text of the chosen scenario under the list.
|
|
277
|
+
describeScenario() {
|
|
278
|
+
const sc = this.state.scenario || '';
|
|
279
|
+
this.scenarioText.textContent = sc === RANDOM ? t('matches.setup.scenario.random.text') : scenarioText(sc);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// The pool of the draw, a box to tick for each team that a random side may draw, and whether a
|
|
283
|
+
// team may meet itself. It shows while a side is Random.
|
|
284
|
+
buildPool() {
|
|
285
|
+
const list = h('div', { class: 'draw-pool-list' });
|
|
286
|
+
const same = h('input', { type: 'checkbox' });
|
|
287
|
+
same.addEventListener('change', () => {
|
|
288
|
+
this.state.sameTeam = same.checked;
|
|
289
|
+
this.updateSetup();
|
|
290
|
+
});
|
|
291
|
+
const root = h('fieldset', { class: 'draw-pool', hidden: true },
|
|
292
|
+
h('legend', {}, t('matches.draw.title')),
|
|
293
|
+
h('p', { class: 'muted small' }, tn('matches.draw.text')),
|
|
294
|
+
list,
|
|
295
|
+
h('label', { class: 'check' }, same, t('matches.draw.sameTeam')));
|
|
296
|
+
return { root, list, same, key: '' };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Why a team of the pool cannot be drawn in the chosen scenario, by any random side, or the
|
|
300
|
+
// empty text when it can. A random scenario asks for a scenario that both sides can play.
|
|
301
|
+
poolFault(x, scenario) {
|
|
302
|
+
const s = this.state;
|
|
303
|
+
const random = [0, 1].filter((side) => [s.left, s.right][side] === RANDOM);
|
|
304
|
+
if (!scenario || scenario === RANDOM) {
|
|
305
|
+
const fitting = fittingScenarios(this.scenarios, this.teams, new Set(), s.left, s.right, this.formations);
|
|
306
|
+
return fitting.some((sc) => random.some((side) => fits(x, sc, side, this.formations))) ? '' : t('matches.draw.unfit.none');
|
|
307
|
+
}
|
|
308
|
+
const faults = random.map((side) => unfit(x, scenario, side, this.formations));
|
|
309
|
+
const fault = faults.every(Boolean) ? faults[0] : null;
|
|
310
|
+
if (!fault) {
|
|
311
|
+
return '';
|
|
312
|
+
}
|
|
313
|
+
return fault.id === 'matches.draw.unfit.format'
|
|
314
|
+
? t('matches.draw.unfit.format', { format: FORMAT_NAMES[fault.values.format] || fault.values.format })
|
|
315
|
+
: t('matches.draw.unfit.control', { reason: t(fault.id, fault.values) });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Fills the boxes of the pool when the registry changes, and ticks them as the state says, so
|
|
319
|
+
// a box keeps its focus while the person ticks it. A team that cannot be drawn in the chosen
|
|
320
|
+
// scenario shows faded, with the reason as its tooltip and for a screen reader.
|
|
321
|
+
fillPool(scenario = this.state.scenario) {
|
|
322
|
+
const pool = this.pool;
|
|
323
|
+
const key = this.teams.map((x) => `${x.id}|${teamName(x)}`).join('\n');
|
|
324
|
+
if (key !== pool.key) {
|
|
325
|
+
pool.key = key;
|
|
326
|
+
pool.list.replaceChildren(...this.teams.map((x, i) => {
|
|
327
|
+
const box = h('input', { type: 'checkbox', value: x.id });
|
|
328
|
+
box.addEventListener('change', () => {
|
|
329
|
+
if (box.checked) {
|
|
330
|
+
this.state.unticked.delete(x.id);
|
|
331
|
+
} else {
|
|
332
|
+
this.state.unticked.add(x.id);
|
|
333
|
+
}
|
|
334
|
+
this.updateSetup();
|
|
335
|
+
});
|
|
336
|
+
return h('label', { class: 'check draw-pool-team', 'data-id': x.id }, box, kitShirt(x, i % 2), h('span', {}, teamName(x)),
|
|
337
|
+
h('span', { class: 'sr-only draw-pool-fault' }));
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
for (const row of pool.list.querySelectorAll('.draw-pool-team')) {
|
|
341
|
+
const x = this.team(row.dataset.id);
|
|
342
|
+
const fault = x ? this.poolFault(x, scenario) : '';
|
|
343
|
+
row.classList.toggle('unfit', !!fault);
|
|
344
|
+
row.title = fault;
|
|
345
|
+
row.querySelector('.draw-pool-fault').textContent = fault ? `, ${fault}` : '';
|
|
346
|
+
}
|
|
347
|
+
for (const box of pool.list.querySelectorAll('input')) {
|
|
348
|
+
box.checked = !this.state.unticked.has(box.value);
|
|
349
|
+
}
|
|
350
|
+
pool.same.checked = this.state.sameTeam;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// One side of the match, a large card of the chosen team that opens a list of every team.
|
|
354
|
+
sidePicker(side) {
|
|
355
|
+
const teamLabel = t(side ? 'matches.side.right.team' : 'matches.side.left.team');
|
|
356
|
+
const button = h('button', { type: 'button', class: 'team-card', 'aria-label': teamLabel });
|
|
357
|
+
const panel = h('div', { role: 'menu', class: 'team-menu', 'aria-label': teamLabel });
|
|
358
|
+
document.getElementById('app').appendChild(panel);
|
|
359
|
+
const fill = () => {
|
|
360
|
+
const current = side ? this.state.right : this.state.left;
|
|
361
|
+
const random = menuItem(t('matches.team.random'), {
|
|
362
|
+
checked: current === RANDOM,
|
|
363
|
+
hint: t('matches.team.random.hint'),
|
|
364
|
+
icon: icon('dice'),
|
|
365
|
+
onSelect: () => {
|
|
366
|
+
this.state[side ? 'right' : 'left'] = RANDOM;
|
|
367
|
+
this.updateSetup();
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
panel.replaceChildren(random, ...this.teams.map((x, i) => menuItem(teamName(x), {
|
|
371
|
+
checked: x.id === current,
|
|
372
|
+
hint: this.teamHint(x.id),
|
|
373
|
+
icon: kitShirt(x, i % 2),
|
|
374
|
+
onSelect: () => {
|
|
375
|
+
this.state[side ? 'right' : 'left'] = x.id;
|
|
376
|
+
this.updateSetup();
|
|
377
|
+
},
|
|
378
|
+
})));
|
|
379
|
+
};
|
|
380
|
+
const dropdown = new Dropdown(button, panel, { onOpen: fill, align: side ? 'right' : 'left' });
|
|
381
|
+
// The ability of the built-in AI on this side, GRF's team difficulty, which steers every
|
|
382
|
+
// player the built-in AI controls. Left alone it is the scenario's own.
|
|
383
|
+
const name = side ? 'right' : 'left';
|
|
384
|
+
const own = h('input', { type: 'checkbox', 'aria-label': t(`matches.side.${name}.ability.set`) });
|
|
385
|
+
const range = h('input', {
|
|
386
|
+
type: 'range', min: '0', max: '1', step: '0.05', value: '0.5', disabled: true,
|
|
387
|
+
'aria-label': t(`matches.side.${name}.ability.range`),
|
|
388
|
+
});
|
|
389
|
+
const shown = h('output', { class: 'ability-value' }, t('matches.ability.scenario'));
|
|
390
|
+
const sync = () => {
|
|
391
|
+
const value = own.checked ? Number(range.value) : null;
|
|
392
|
+
this.state.ability[side] = value;
|
|
393
|
+
range.disabled = !own.checked;
|
|
394
|
+
shown.textContent = value === null ? t('matches.ability.scenario') : abilityText(value);
|
|
395
|
+
};
|
|
396
|
+
own.addEventListener('change', sync);
|
|
397
|
+
range.addEventListener('input', sync);
|
|
398
|
+
const ability = h('div', { class: 'ability', title: t('matches.ability.title') },
|
|
399
|
+
h('label', { class: 'check' }, own, t('matches.ability')), range, shown);
|
|
400
|
+
const root = h('div', { class: `side side-${side ? 'right' : 'left'}` },
|
|
401
|
+
h('div', { class: 'side-label' }, t(`matches.side.${name}`)), button, ability);
|
|
402
|
+
return { root, button, dropdown };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
updateSetup() {
|
|
406
|
+
if (!this.built) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const s = this.state;
|
|
410
|
+
const { listed, keep } = scenarioChoices(this.scenarios, this.teams, s.unticked, s.left, s.right, s.scenario, DEFAULT_SCENARIO, this.formations);
|
|
411
|
+
[s.left, s.right].forEach((id, side) => {
|
|
412
|
+
const x = this.team(id);
|
|
413
|
+
const card = this.sides[side].button;
|
|
414
|
+
card.title = '';
|
|
415
|
+
if (id === RANDOM) {
|
|
416
|
+
card.replaceChildren(
|
|
417
|
+
h('span', { class: 'kit-shirt large team-card-random', 'aria-hidden': 'true' }, icon('dice')),
|
|
418
|
+
h('span', { class: 'team-card-text' },
|
|
419
|
+
h('span', { class: 'team-card-name' }, t('matches.team.random')),
|
|
420
|
+
h('span', { class: 'team-card-meta' }, t('matches.team.random.meta', { count: drawableTeams(this.teams, s.unticked, { ...s, scenario: keep }, side, this.scenarios, this.formations).length }))),
|
|
421
|
+
h('span', { class: 'dd-caret', 'aria-hidden': 'true' }));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (!x) {
|
|
425
|
+
card.replaceChildren(h('span', { class: 'team-card-empty' }, t('matches.team.none')));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
// A submission shows its format and the players it controls, and its whole control
|
|
429
|
+
// setting as the tooltip of the card.
|
|
430
|
+
const builtin = x.kind === 'builtin';
|
|
431
|
+
card.title = builtin ? '' : t('matches.team.submission.title', { control: controlLabel(x.control, x.format) });
|
|
432
|
+
card.replaceChildren(
|
|
433
|
+
kitShirt(x, side, 'large'),
|
|
434
|
+
h('span', { class: 'team-card-text' },
|
|
435
|
+
h('span', { class: 'team-card-name' }, teamName(x)),
|
|
436
|
+
h('span', { class: 'team-card-meta' }, builtin ? t('matches.team.builtin.meta') : t('matches.team.submission.meta', { format: FORMAT_NAMES[x.format] || x.format, count: controlledPlayers(x) }))),
|
|
437
|
+
h('span', { class: 'dd-caret', 'aria-hidden': 'true' }));
|
|
438
|
+
});
|
|
439
|
+
const random = [s.left, s.right].includes(RANDOM);
|
|
440
|
+
const l = s.left === RANDOM ? null : this.team(s.left);
|
|
441
|
+
const r = s.right === RANDOM ? null : this.team(s.right);
|
|
442
|
+
const chosen = (l || s.left === RANDOM) && (r || s.right === RANDOM);
|
|
443
|
+
const clash = l && r && l.kind !== 'builtin' && r.kind !== 'builtin' && l.format && r.format && l.format !== r.format;
|
|
444
|
+
if (clash) {
|
|
445
|
+
this.clash.replaceChildren(...tn('matches.setup.clash'));
|
|
446
|
+
} else if (!chosen) {
|
|
447
|
+
this.clash.replaceChildren(t('matches.setup.noTeams'));
|
|
448
|
+
}
|
|
449
|
+
this.clash.hidden = !clash && !!chosen;
|
|
450
|
+
this.pool.root.hidden = !random;
|
|
451
|
+
if (random) {
|
|
452
|
+
this.fillPool(keep);
|
|
453
|
+
}
|
|
454
|
+
const name = (sc) => (sc === RANDOM ? t('matches.setup.scenario.random') : scenarioName(sc));
|
|
455
|
+
this.scenarioSelect.replaceChildren(...(listed.length ? listed : [t('matches.setup.noScenario')]).map((sc) =>
|
|
456
|
+
h('option', { value: listed.length ? sc : '' }, listed.length ? name(sc) : sc)));
|
|
457
|
+
this.scenarioSelect.value = keep || '';
|
|
458
|
+
this.scenarioSelect.disabled = !listed.length;
|
|
459
|
+
s.scenario = keep || null;
|
|
460
|
+
this.describeScenario();
|
|
461
|
+
this.playButton.disabled = !listed.length || !chosen;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
drawSeed() {
|
|
465
|
+
const v = crypto.getRandomValues(new Uint32Array(1))[0];
|
|
466
|
+
this.seedInput.value = String(v);
|
|
467
|
+
this.state.seed = this.seedInput.value;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async play() {
|
|
471
|
+
const s = this.state;
|
|
472
|
+
const text = this.seedInput.value.trim();
|
|
473
|
+
if (text !== '' && !/^\d+$/.test(text)) {
|
|
474
|
+
this.app.hud.toast(t('matches.setup.seed.invalid'), 'error');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
this.playButton.disabled = true;
|
|
478
|
+
try {
|
|
479
|
+
await api.requestMatch(requestOf(s, this.teams, text === '' ? null : text));
|
|
480
|
+
this.app.hud.toast(t('matches.toast.queued', { left: this.teamName(s.left), right: this.teamName(s.right) }));
|
|
481
|
+
this.playButton.classList.remove('kicked');
|
|
482
|
+
void this.playButton.offsetWidth;
|
|
483
|
+
this.playButton.classList.add('kicked');
|
|
484
|
+
this.requests = await api.requests();
|
|
485
|
+
this.renderQueue();
|
|
486
|
+
} catch (err) {
|
|
487
|
+
this.app.hud.toast(err.message, 'error', 7000);
|
|
488
|
+
} finally {
|
|
489
|
+
this.updateSetup();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
buildFilters() {
|
|
494
|
+
const select = (key, label, options) => {
|
|
495
|
+
const el = h('select', { 'aria-label': label }, options.map(([v, text]) => h('option', { value: v }, text)));
|
|
496
|
+
el.value = this.filters[key];
|
|
497
|
+
el.addEventListener('change', () => {
|
|
498
|
+
this.filters[key] = el.value;
|
|
499
|
+
this.renderQueue();
|
|
500
|
+
});
|
|
501
|
+
return el;
|
|
502
|
+
};
|
|
503
|
+
this.statusFilter = select('status', t('matches.filter.status'), [['all', t('matches.filter.status.all')], ['active', t('matches.filter.status.active')], ['done', STATE_LABELS.done], ['failed', STATE_LABELS.failed], ['cancelled', STATE_LABELS.cancelled], ['interrupted', STATE_LABELS.interrupted]]);
|
|
504
|
+
this.teamFilter = select('team', t('matches.filter.team'), [['all', t('matches.filter.team.all')]]);
|
|
505
|
+
this.scenarioFilter = select('scenario', t('matches.filter.scenario'), [['all', t('matches.filter.scenario.all')]]);
|
|
506
|
+
this.dateFilter = select('date', t('matches.filter.date'), [['any', t('matches.filter.date.any')], ['hour', t('matches.filter.date.hour')], ['today', t('matches.filter.date.today')], ['week', t('matches.filter.date.week')]]);
|
|
507
|
+
this.sortSelect = select('sort', t('matches.filter.sort'), [['newest', t('matches.filter.sort.newest')], ['oldest', t('matches.filter.sort.oldest')]]);
|
|
508
|
+
const bar = h('div', { class: 'filter-bar' }, this.statusFilter, this.teamFilter, this.scenarioFilter, this.dateFilter, this.sortSelect);
|
|
509
|
+
queueMicrotask(() => {
|
|
510
|
+
enhanceSelect(this.statusFilter, { label: t('matches.filter.status'), icon: 'filter' });
|
|
511
|
+
enhanceSelect(this.teamFilter, { label: t('matches.filter.team'), icon: 'shirt' });
|
|
512
|
+
enhanceSelect(this.scenarioFilter, { label: t('matches.filter.scenario'), icon: 'field' });
|
|
513
|
+
enhanceSelect(this.dateFilter, { label: t('matches.filter.date'), icon: 'calendar' });
|
|
514
|
+
enhanceSelect(this.sortSelect, { label: t('matches.filter.sort'), icon: 'sort', align: 'right' });
|
|
515
|
+
});
|
|
516
|
+
return bar;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// The team and scenario filters list what the requests show, as queue-filters.js describes.
|
|
520
|
+
refillFilters() {
|
|
521
|
+
const fill = (select, key, all, entries) => {
|
|
522
|
+
const keep = this.filters[key];
|
|
523
|
+
const want = [['all', all], ...entries];
|
|
524
|
+
const now = [...select.options].map((o) => `${o.value}|${o.textContent}|${o.dataset.hint || ''}`).join('\n');
|
|
525
|
+
if (now !== want.map(([v, text, hint]) => `${v}|${text}|${hint || ''}`).join('\n')) {
|
|
526
|
+
select.replaceChildren(...want.map(([v, text, hint]) => h('option', { value: v, 'data-hint': hint || null }, text)));
|
|
527
|
+
}
|
|
528
|
+
this.filters[key] = want.some(([v]) => v === keep) ? keep : 'all';
|
|
529
|
+
select.value = this.filters[key];
|
|
530
|
+
};
|
|
531
|
+
fill(this.teamFilter, 'team', t('matches.filter.team.all'), filterTeams(this.requests, this.teams).map((id) => [teamValue(id), this.teamName(id), this.teamHint(id)]));
|
|
532
|
+
fill(this.scenarioFilter, 'scenario', t('matches.filter.scenario.all'), filterScenarios(this.requests).map((s) => [scenarioValue(s), scenarioName(s)]));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
renderQueue() {
|
|
536
|
+
if (!this.built) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
this.refillFilters();
|
|
540
|
+
const f = this.filters;
|
|
541
|
+
let rows = this.requests.filter((r) => {
|
|
542
|
+
const st = stateOf(r);
|
|
543
|
+
if (f.status === 'active' && st !== 'queued' && st !== 'running') {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
if (f.status !== 'all' && f.status !== 'active' && st !== f.status) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
if (!passesTeam(r, f.team)) {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
if (f.date !== 'any') {
|
|
553
|
+
const when = requestTime(r);
|
|
554
|
+
const now = new Date();
|
|
555
|
+
const from = f.date === 'hour' ? now.getTime() - 36e5
|
|
556
|
+
: f.date === 'today' ? new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()
|
|
557
|
+
: now.getTime() - 7 * 864e5;
|
|
558
|
+
if (!when || when.getTime() < from) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return passesScenario(r, f.scenario);
|
|
563
|
+
});
|
|
564
|
+
// The server lists the requests newest first.
|
|
565
|
+
if (f.sort === 'oldest') {
|
|
566
|
+
rows = rows.slice().reverse();
|
|
567
|
+
}
|
|
568
|
+
if (!this.requests.length) {
|
|
569
|
+
this.queueHost.replaceChildren(h('div', { class: 'empty-state' },
|
|
570
|
+
h('p', { class: 'empty-title' }, t('matches.queue.empty.title')),
|
|
571
|
+
h('p', { class: 'muted' }, tn('matches.queue.empty.text'))));
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (!rows.length) {
|
|
575
|
+
this.queueHost.replaceChildren(h('p', { class: 'muted pad' }, t('matches.queue.filtered')));
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const more = rows.length - this.limit;
|
|
579
|
+
rows = rows.slice(0, this.limit);
|
|
580
|
+
// Rows are kept by request, so a progress bar moves smoothly instead of being redrawn.
|
|
581
|
+
const old = new Map([...this.queueHost.querySelectorAll('.req-card')].map((el) => [el.dataset.id, el]));
|
|
582
|
+
const list = this.queueHost.querySelector('.req-cards') || h('ul', { class: 'req-cards' });
|
|
583
|
+
const items = rows.map((r) => {
|
|
584
|
+
const key = `${r.id}|${stateOf(r)}|${r.match || ''}|${r.error || ''}|${r.persistence_error || ''}|${this.teamsVersion}`;
|
|
585
|
+
let li = old.get(r.id);
|
|
586
|
+
if (!li || li.dataset.key !== key) {
|
|
587
|
+
li = this.requestCard(r);
|
|
588
|
+
li.dataset.key = key;
|
|
589
|
+
}
|
|
590
|
+
const played = li.querySelector('.req-played');
|
|
591
|
+
const seconds = playedSeconds(r);
|
|
592
|
+
if (played && seconds !== null) {
|
|
593
|
+
played.textContent = t('matches.request.played', { count: seconds });
|
|
594
|
+
}
|
|
595
|
+
const bar = li.querySelector('.progress .bar');
|
|
596
|
+
if (bar) {
|
|
597
|
+
const pct = Math.round((r.progress || 0) * 100);
|
|
598
|
+
bar.style.width = `${pct}%`;
|
|
599
|
+
const label = li.querySelector('.pct');
|
|
600
|
+
if (label) {
|
|
601
|
+
label.textContent = t('matches.request.percent', { pct });
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return li;
|
|
605
|
+
});
|
|
606
|
+
list.replaceChildren(...items);
|
|
607
|
+
if (list.parentNode !== this.queueHost) {
|
|
608
|
+
this.queueHost.replaceChildren(list);
|
|
609
|
+
}
|
|
610
|
+
this.showMore(more);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// The button under the list that shows the next rows, kept in place while it has focus.
|
|
614
|
+
showMore(more) {
|
|
615
|
+
let button = this.queueHost.querySelector('.req-more');
|
|
616
|
+
if (more <= 0) {
|
|
617
|
+
if (button && button === document.activeElement) {
|
|
618
|
+
// The last rows take the focus of the button that showed them.
|
|
619
|
+
this.queueHost.querySelector('.req-card:last-child button')?.focus();
|
|
620
|
+
}
|
|
621
|
+
button?.remove();
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (!button) {
|
|
625
|
+
button = h('button', { type: 'button', class: 'btn small ghost req-more', onclick: () => {
|
|
626
|
+
this.limit += REQUEST_PAGE;
|
|
627
|
+
this.renderQueue();
|
|
628
|
+
} });
|
|
629
|
+
this.queueHost.appendChild(button);
|
|
630
|
+
}
|
|
631
|
+
button.textContent = t('matches.queue.more', { shown: Math.min(more, REQUEST_PAGE), count: more });
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
requestCard(r) {
|
|
635
|
+
const st = stateOf(r);
|
|
636
|
+
const when = requestTime(r);
|
|
637
|
+
const parts = drawnParts(r);
|
|
638
|
+
// A part drawn at random shows as Random while the match waits, and as what was drawn,
|
|
639
|
+
// with a mark, once it starts.
|
|
640
|
+
const drawnMark = () => h('span', { class: 'req-drawn', title: t('matches.request.drawn') },
|
|
641
|
+
icon('dice'), h('span', { class: 'sr-only' }, t('matches.request.drawn')));
|
|
642
|
+
const side = (id, i) => {
|
|
643
|
+
const part = i ? 'right' : 'left';
|
|
644
|
+
if (hiddenPart(r, part)) {
|
|
645
|
+
return h('span', { class: 'req-team req-random' }, icon('dice'), h('span', { class: 'req-team-name' }, t('matches.team.random')));
|
|
646
|
+
}
|
|
647
|
+
const x = this.team(id);
|
|
648
|
+
const kept = Array.isArray(r.team_names) ? r.team_names[i] : null;
|
|
649
|
+
return h('span', { class: 'req-team' }, x ? kitShirt(x, i) : null, h('span', { class: 'req-team-name', title: x ? null : id }, this.teamName(id, kept)),
|
|
650
|
+
parts[part] ? drawnMark() : null);
|
|
651
|
+
};
|
|
652
|
+
const scenario = hiddenPart(r, 'scenario') ? h('span', {}, t('matches.setup.scenario.random'))
|
|
653
|
+
: h('span', {}, scenarioName(r.scenario), parts.scenario ? drawnMark() : null);
|
|
654
|
+
const actions = [];
|
|
655
|
+
if (st === 'queued' || st === 'running') {
|
|
656
|
+
actions.push(h('button', { type: 'button', class: 'btn small ghost danger', ...symbolOnly(t('matches.request.cancel')), onclick: () => this.cancel(r) }, icon('close')));
|
|
657
|
+
}
|
|
658
|
+
if (st === 'done' && r.match) {
|
|
659
|
+
actions.push(h('button', { type: 'button', class: 'btn small primary', onclick: () => this.app.openMatchInWatch(r.match) },
|
|
660
|
+
icon('play'), t('matches.request.watch')));
|
|
661
|
+
}
|
|
662
|
+
if (st !== 'queued' && st !== 'running') {
|
|
663
|
+
// Retry and Repeat queue a new request with the same seed, teams, scenario and ability.
|
|
664
|
+
// A match drawn at random repeats with a new seed, which draws again.
|
|
665
|
+
const again = st === 'done' ? 'repeat' : 'retry';
|
|
666
|
+
const title = wasDrawn(r) ? `matches.request.${again}.draw.title` : `matches.request.${again}.title`;
|
|
667
|
+
actions.push(h('button', { type: 'button', class: 'btn small', ...symbolOnly(t(`matches.request.${again}`), t(title, { seed: r.seed })),
|
|
668
|
+
onclick: (event) => this.again(r, event.currentTarget, again === 'repeat') }, icon(again === 'repeat' ? 'replay' : 'refresh')));
|
|
669
|
+
}
|
|
670
|
+
const li = h('li', { class: `req-card state-${st}`, 'data-id': r.id },
|
|
671
|
+
h('div', { class: 'req-main' },
|
|
672
|
+
h('div', { class: 'req-teams' }, side(r.left, 0), h('span', { class: 'req-vs' }, t('matches.versus')), side(r.right, 1)),
|
|
673
|
+
h('div', { class: 'req-meta' },
|
|
674
|
+
stateBadge(st, STATE_LABELS[st] || st),
|
|
675
|
+
scenario,
|
|
676
|
+
r.seed != null ? h('span', {}, t('matches.request.seed', { seed: r.seed })) : null,
|
|
677
|
+
Array.isArray(r.ability) && r.ability.some((v) => v !== null)
|
|
678
|
+
? h('span', {}, t('matches.request.ability', Object.fromEntries(['left', 'right'].map((key, i) => [key, r.ability[i] === null ? t('matches.request.ability.scenario') : abilityText(r.ability[i])]))))
|
|
679
|
+
: null,
|
|
680
|
+
when ? h('span', {}, new Intl.DateTimeFormat(uiLocale(), { timeStyle: 'short', dateStyle: 'medium' }).format(when)) : null,
|
|
681
|
+
h('span', { class: 'req-played' })),
|
|
682
|
+
st === 'queued' || st === 'running'
|
|
683
|
+
? h('div', { class: 'req-progress' },
|
|
684
|
+
h('div', { class: `progress${st === 'queued' ? ' waiting' : ''}` }, h('div', { class: 'bar' })),
|
|
685
|
+
h('span', { class: 'pct muted small' }, st === 'queued' ? t('matches.request.queued') : ''))
|
|
686
|
+
: null,
|
|
687
|
+
(st === 'failed' || st === 'interrupted') && r.error ? h('div', { class: 'error-text' }, serverText(r, 'error')) : null,
|
|
688
|
+
r.persistence_error ? h('div', { class: 'error-text' }, serverText(r, 'persistence_error')) : null),
|
|
689
|
+
h('div', { class: 'req-actions' }, actions));
|
|
690
|
+
if (st === 'queued') {
|
|
691
|
+
li.querySelector('.pct').classList.remove('pct');
|
|
692
|
+
}
|
|
693
|
+
return li;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async again(r, button, repeat = false) {
|
|
697
|
+
button.disabled = true;
|
|
698
|
+
try {
|
|
699
|
+
const spec = againOf(r, repeat);
|
|
700
|
+
await api.requestMatch(spec);
|
|
701
|
+
const names = { left: this.teamName(spec.left), right: this.teamName(spec.right) };
|
|
702
|
+
this.app.hud.toast(spec.seed == null ? t('matches.toast.queued', names) : t('matches.toast.queuedSeed', { ...names, seed: r.seed }));
|
|
703
|
+
this.requests = await api.requests();
|
|
704
|
+
this.renderQueue();
|
|
705
|
+
} catch (err) {
|
|
706
|
+
this.app.hud.toast(err.message, 'error', 7000);
|
|
707
|
+
button.disabled = false;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async cancel(r) {
|
|
712
|
+
try {
|
|
713
|
+
await api.cancelRequest(r.id);
|
|
714
|
+
this.seen.add(r.id);
|
|
715
|
+
this.app.hud.toast(t('matches.toast.cancelled'));
|
|
716
|
+
} catch (err) {
|
|
717
|
+
this.app.hud.toast(err.status === 404 ? t('matches.toast.ended') : err.message, 'error');
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
this.requests = await api.requests();
|
|
721
|
+
} catch {
|
|
722
|
+
// The next poll tries again.
|
|
723
|
+
}
|
|
724
|
+
this.renderQueue();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Watches the queue in the background and tells when a match is ready or failed.
|
|
728
|
+
async poll() {
|
|
729
|
+
if (document.hidden || document.body.classList.contains('offline')) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const active = this.requests.some((r) => r.state === 'queued' || r.state === 'running');
|
|
733
|
+
if (!active && this.primed && !this.visible) {
|
|
734
|
+
// Nothing runs, a slower check is enough while the page is elsewhere.
|
|
735
|
+
this.idleTicks = (this.idleTicks || 0) + 1;
|
|
736
|
+
if (this.idleTicks % 6) {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
let list;
|
|
741
|
+
try {
|
|
742
|
+
list = await api.requests();
|
|
743
|
+
} catch {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
// A request may name a team added since the page loaded the registry, from the command
|
|
747
|
+
// line or another tab, and the page then loads the registry again, once for each such
|
|
748
|
+
// team, so a team that was removed is not looked up on every check.
|
|
749
|
+
const before = this.teamsVersion;
|
|
750
|
+
const unknown = unknownTeams(list, this.teams).filter((id) => !this.looked.has(id));
|
|
751
|
+
if (unknown.length) {
|
|
752
|
+
try {
|
|
753
|
+
this.setTeams(await api.teams());
|
|
754
|
+
unknown.forEach((id) => this.looked.add(id));
|
|
755
|
+
} catch {
|
|
756
|
+
// The next check tries again.
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
for (const r of list) {
|
|
760
|
+
const st = stateOf(r);
|
|
761
|
+
if ((st === 'done' || st === 'failed' || st === 'cancelled') && !this.seen.has(r.id)) {
|
|
762
|
+
this.seen.add(r.id);
|
|
763
|
+
if (this.primed && st === 'done') {
|
|
764
|
+
this.app.hud.toast(t(this.visible ? 'matches.toast.ready' : 'matches.toast.ready.away', { left: this.teamName(r.left), right: this.teamName(r.right) }), 'info', 6000);
|
|
765
|
+
} else if (this.primed && st === 'failed') {
|
|
766
|
+
this.app.hud.toast(t('matches.toast.failed', { error: r.error ? serverText(r, 'error') : '' }), 'error', 7000);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
this.primed = true;
|
|
771
|
+
const changed = JSON.stringify(list) !== JSON.stringify(this.requests);
|
|
772
|
+
this.requests = list;
|
|
773
|
+
if (this.teamsVersion !== before && this.visible) {
|
|
774
|
+
this.render();
|
|
775
|
+
} else if (changed && this.visible) {
|
|
776
|
+
this.renderQueue();
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|