From 516d934666bda30343d311a210266d80f554f0ac Mon Sep 17 00:00:00 2001 From: Saransh Saini <66969478+saranshsaini@users.noreply.github.com> Date: Mon, 15 Dec 2025 22:10:46 -0800 Subject: [PATCH 01/19] Add snowdodgeball translations (#224) Co-authored-by: Saransh Saini --- _messages/af.json | 40 +++++++++++++++++++++++++++++++-------- _messages/bg.json | 24 +++++++++++++++++++++++ _messages/ca.json | 26 ++++++++++++++++++++++++- _messages/da.json | 30 ++++++++++++++++++++++++++--- _messages/de.json | 24 +++++++++++++++++++++++ _messages/el.json | 24 +++++++++++++++++++++++ _messages/en-GB.json | 24 +++++++++++++++++++++++ _messages/en.json | 24 +++++++++++++++++++++++ _messages/es-419.json | 26 ++++++++++++++++++++++++- _messages/es.json | 24 +++++++++++++++++++++++ _messages/et.json | 24 +++++++++++++++++++++++ _messages/eu.json | 44 +++++++++++++++++++++++++++++++++---------- _messages/fi.json | 24 +++++++++++++++++++++++ _messages/fr-CA.json | 24 +++++++++++++++++++++++ _messages/fr.json | 24 +++++++++++++++++++++++ _messages/gl.json | 30 ++++++++++++++++++++++++++--- _messages/hi.json | 28 +++++++++++++++++++++++++-- _messages/hr.json | 24 +++++++++++++++++++++++ _messages/hu.json | 24 +++++++++++++++++++++++ _messages/id.json | 24 +++++++++++++++++++++++ _messages/is.json | 40 +++++++++++++++++++++++++++++++-------- _messages/it.json | 24 +++++++++++++++++++++++ _messages/ja.json | 24 +++++++++++++++++++++++ _messages/ko.json | 26 ++++++++++++++++++++++++- _messages/lt.json | 24 +++++++++++++++++++++++ _messages/lv.json | 28 +++++++++++++++++++++++++-- _messages/ml.json | 24 +++++++++++++++++++++++ _messages/nl.json | 24 +++++++++++++++++++++++ _messages/no.json | 24 +++++++++++++++++++++++ _messages/pl.json | 24 +++++++++++++++++++++++ _messages/pt-BR.json | 26 ++++++++++++++++++++++++- _messages/pt-PT.json | 32 +++++++++++++++++++++++++++---- _messages/ro.json | 26 ++++++++++++++++++++++++- _messages/ru.json | 24 +++++++++++++++++++++++ _messages/sk.json | 24 +++++++++++++++++++++++ _messages/sl.json | 24 +++++++++++++++++++++++ _messages/sr.json | 24 +++++++++++++++++++++++ _messages/sv.json | 24 +++++++++++++++++++++++ _messages/ta.json | 24 +++++++++++++++++++++++ _messages/th.json | 24 +++++++++++++++++++++++ _messages/tl.json | 24 +++++++++++++++++++++++ _messages/uk.json | 24 +++++++++++++++++++++++ _messages/vi.json | 24 +++++++++++++++++++++++ _messages/zh-CN.json | 24 +++++++++++++++++++++++ _messages/zh-HK.json | 24 +++++++++++++++++++++++ _messages/zh-TW.json | 24 +++++++++++++++++++++++ 46 files changed, 1149 insertions(+), 45 deletions(-) diff --git a/_messages/af.json b/_messages/af.json index 9ccfa4e21..d1a8cc3fc 100644 --- a/_messages/af.json +++ b/_messages/af.json @@ -867,7 +867,7 @@ "message": "Op 24 Desember verander Santa se dorpie in 'n spoorsny-ervaring waar jy Kersvader en sy rendiere kan volg terwyl hulle presente vir kinders regoor die wêreld aflewer." }, "familyguide_secondary_cta_01": { - "message": "Volg op die Waar is Kersvader-webwerf of -Android-program" + "message": "Volg op die Waar is Kersvader-webwerf of -Android-app" }, "familyguide_secondary_cta_02": { "message": "Psst! Het jy geweet Kersvader se reis duur 25 uur? Hy maak sy eerste stop net ná 22:00 plaaslike tyd in verre oostelike Rusland wanneer dit 05:00 in New York en 11:00 in Parys is." @@ -885,10 +885,10 @@ "message": "\"Ok Google, wat's nuus by die Noordpool?\"" }, "familyguide_tips_tip02_body": { - "message": "Laai die Waar is Kersvader-program vir Android by die Play Winkel af. Daar's 'n eksklusiewe dorpietoneel met speletjies wat jy net in die program sal kry." + "message": "Laai die Waar is Kersvader-app vir Android by die Play Winkel af. Daar's 'n eksklusiewe dorpietoneel met speletjies wat jy net in die app sal kry." }, "familyguide_tips_tip02_title": { - "message": "Android-program" + "message": "Android-app" }, "familyguide_tips_tip03_body": { "message": "Volg Kersvader op jou pols met Android Wear-integrasie." @@ -1107,7 +1107,7 @@ "message": "Skakel aan" }, "lightdescription": { - "message": "Verhelder jou TV met die Waar is Kersvader-program vir Android en Chromecast." + "message": "Verhelder jou TV met die Waar is Kersvader-app vir Android en Chromecast." }, "lk_country_name": { "message": "Sri Lanka" @@ -1413,7 +1413,7 @@ "message": "Speel weer" }, "playdescription": { - "message": "Speel saam met vriende in Waar is Kersvader se Android-program." + "message": "Speel saam met vriende in Waar is Kersvader se Android-app." }, "playextra": { "message": "Speel ekstra vlakke" @@ -1503,7 +1503,7 @@ "message": "'n Nuwe manier om Kersvader te volg:" }, "press-whatsnew4-text": { - "message": "Gaan kyk na die Gboard-feestydplakkers wat beskikbaar is op \u003ca href=\"https://play.google.com/store/apps/details?id=com.google.android.inputmethod.latin\"\u003eAndroid\u003c/a\u003e, \u003ca href=\"https://itunes.apple.com/us/app/gboard-the-google-keyboard/id1091700242?mt=8\"\u003eiOS\u003c/a\u003e en ons \u003ca href=\"https://play.google.com/store/apps/details?id=com.google.android.apps.santatracker\"\u003eAndroid-program\u003c/a\u003e indien jy na meer feestydvreugde smag. En geniet 'n bietjie literêre pret deur vir die Google Assistent te sê \"Vertel my 'n feestydstorie\" en \"Waar's Kersvader?\"" + "message": "Gaan kyk na die Gboard-feestydplakkers wat beskikbaar is op \u003ca href=\"https://play.google.com/store/apps/details?id=com.google.android.inputmethod.latin\"\u003eAndroid\u003c/a\u003e, \u003ca href=\"https://itunes.apple.com/us/app/gboard-the-google-keyboard/id1091700242?mt=8\"\u003eiOS\u003c/a\u003e en ons \u003ca href=\"https://play.google.com/store/apps/details?id=com.google.android.apps.santatracker\"\u003eAndroid-app\u003c/a\u003e indien jy na meer feestydvreugde smag. En geniet 'n bietjie literêre pret deur vir die Google Assistent te sê \"Vertel my 'n feestydstorie\" en \"Waar's Kersvader?\"" }, "press-whatsnew4-title": { "message": "Nog feestydpret:" @@ -1668,7 +1668,7 @@ "message": "Sneeubal-hardloop" }, "scene_app": { - "message": "Android-program" + "message": "Android-app" }, "scene_blimp": { "message": "Herlaai presente" @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Sneeuput" }, + "scene_snowdodgeball": { + "message": "Snow Dodgeball" + }, "scene_snowflake": { "message": "Kodeer 'n sneeuvlokkie" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Wag tans vir nog Santa se elwe" }, + "snowdodgeball_instruction_dodge": { + "message": "Klik op ’n elf om sneeuballe te ontduik" + }, + "snowdodgeball_instruction_throw": { + "message": "Klik om sneeuballe te gooi" + }, + "snowdodgeball_match_lost": { + "message": "Wedstryd verloor" + }, + "snowdodgeball_match_won": { + "message": "Wedstryd gewen!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Akkuraatheid" + }, + "snowdodgeball_stats_hits": { + "message": "Kere getref" + }, + "snowdodgeball_stats_throws": { + "message": "Sneeuballe gegooi" + }, "snowflake_choose_background": { "message": "Kies 'n agtergrond" }, @@ -7269,7 +7293,7 @@ "message": "Verken, speel en leer die hele Desember saam met Kersvader se elwe" }, "village_get_the_app": { - "message": "Kry die Santa Tracker-program vir Android" + "message": "Kry die Santa Tracker-app vir Android" }, "village_more": { "message": "Meer maniere om te verken" diff --git a/_messages/bg.json b/_messages/bg.json index cb8fd074d..478ea03c4 100644 --- a/_messages/bg.json +++ b/_messages/bg.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Снежно творчество" }, + "scene_snowdodgeball": { + "message": "Народна топка в снега" + }, "scene_snowflake": { "message": "Кодиране на снежинка" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Изчакват се още джуджета" }, + "snowdodgeball_instruction_dodge": { + "message": "Кликнете върху елф, за да избегнете снежните топки" + }, + "snowdodgeball_instruction_throw": { + "message": "Кликнете, за да хвърлите снежни топки" + }, + "snowdodgeball_match_lost": { + "message": "Загубена игра" + }, + "snowdodgeball_match_won": { + "message": "Спечелихте играта!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Точност" + }, + "snowdodgeball_stats_hits": { + "message": "Приети удари" + }, + "snowdodgeball_stats_throws": { + "message": "Хвърлени снежни топки" + }, "snowflake_choose_background": { "message": "Изберете фон" }, diff --git a/_messages/ca.json b/_messages/ca.json index 73b5cec48..b957ee8fb 100644 --- a/_messages/ca.json +++ b/_messages/ca.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Zona de proves nevada" }, + "scene_snowdodgeball": { + "message": "Guerra de boles de neu" + }, "scene_snowflake": { "message": "Crea un floc de neu amb codi" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Estàs esperant que arribin més elfs del Pare Noel" }, + "snowdodgeball_instruction_dodge": { + "message": "Fes clic en un follet per esquivar les boles de neu" + }, + "snowdodgeball_instruction_throw": { + "message": "Fes clic per llançar boles de neu" + }, + "snowdodgeball_match_lost": { + "message": "Partida perduda" + }, + "snowdodgeball_match_won": { + "message": "Has guanyat la partida!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Precisió" + }, + "snowdodgeball_stats_hits": { + "message": "Cops rebuts" + }, + "snowdodgeball_stats_throws": { + "message": "Boles de neu llançades" + }, "snowflake_choose_background": { "message": "Tria un fons" }, @@ -3870,7 +3894,7 @@ "message": "Arizona" }, "trivia_beginner_025": { - "message": "Quin estat dels EUA hauríeu de visitar per travessar en cotxe el pont de la badia de Chesapeake i caminar pel passeig marítim a Ocean City?" + "message": "Quin estat dels EUA hauríeu de visitar per travessar amb cotxe el pont de la badia de Chesapeake i caminar pel passeig marítim a Ocean City?" }, "trivia_beginner_025_answer0": { "message": "Maryland" diff --git a/_messages/da.json b/_messages/da.json index 25d590a06..06e4a30bf 100644 --- a/_messages/da.json +++ b/_messages/da.json @@ -804,7 +804,7 @@ "message": "Når gløggen er drukket, og gaverne er pakket op, er der familiesjov til hele året med Google." }, "familyguide_doublecard_card01_body": { - "message": "Vi vil allesammen gerne have en sund planet nu og i fremtiden. Google giver dig nogle simple tip til, hvordan vi mennesker og planeten kan leve godt sammen." + "message": "Vi vil allesammen gerne have en sund planet nu og i fremtiden. Google giver dig nogle simple tips til, hvordan vi mennesker og planeten kan leve godt sammen." }, "familyguide_doublecard_card01_title": { "message": "Din plan, din planet" @@ -855,7 +855,7 @@ "message": "familietid" }, "familyguide_nav_link_05": { - "message": "tip og tricks" + "message": "tips og tricks" }, "familyguide_nav_link_06": { "message": "mere" @@ -909,7 +909,7 @@ "message": "Giv en gif" }, "familyguide_tips_title": { - "message": "Tip og tricks" + "message": "Tips og tricks" }, "familyguide_title": { "message": "Julehygge for hele familien" @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Snekasse" }, + "scene_snowdodgeball": { + "message": "Snedodgeball" + }, "scene_snowflake": { "message": "Lav et snefnug med kode" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Venter på flere af julemandens nisser" }, + "snowdodgeball_instruction_dodge": { + "message": "Klik på en alf for at undgå snebolde" + }, + "snowdodgeball_instruction_throw": { + "message": "Klik for at kaste snebolde" + }, + "snowdodgeball_match_lost": { + "message": "Du tabte kampen" + }, + "snowdodgeball_match_won": { + "message": "Du vandt kampen!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Nøjagtighed" + }, + "snowdodgeball_stats_hits": { + "message": "Antal gange, du blev ramt" + }, + "snowdodgeball_stats_throws": { + "message": "Snebolde kastet" + }, "snowflake_choose_background": { "message": "Vælg en baggrund" }, diff --git a/_messages/de.json b/_messages/de.json index 715c7a566..83aef252f 100644 --- a/_messages/de.json +++ b/_messages/de.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Schneebox" }, + "scene_snowdodgeball": { + "message": "Schneeball-Spiel" + }, "scene_snowflake": { "message": "Schneeflockencode" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Wir warten auf weitere Wichtel, die mitspielen" }, + "snowdodgeball_instruction_dodge": { + "message": "Klicke auf einen Wichtel, um Schneebällen auszuweichen" + }, + "snowdodgeball_instruction_throw": { + "message": "Klicke, um Schneebälle zu werfen" + }, + "snowdodgeball_match_lost": { + "message": "Spiel verloren" + }, + "snowdodgeball_match_won": { + "message": "Spiel gewonnen!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Genauigkeit" + }, + "snowdodgeball_stats_hits": { + "message": "Eingesteckte Treffer" + }, + "snowdodgeball_stats_throws": { + "message": "Geworfene Schneebälle" + }, "snowflake_choose_background": { "message": "Such einen Hintergrund aus" }, diff --git a/_messages/el.json b/_messages/el.json index 72c258434..18ab3a6a0 100644 --- a/_messages/el.json +++ b/_messages/el.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Χιονοκατασκευές" }, + "scene_snowdodgeball": { + "message": "Χιονοπόλεμος" + }, "scene_snowflake": { "message": "Κωδικοποίηση χιονονιφάδας" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Αναμονή για άλλα ξωτικά του Άη Βασίλη" }, + "snowdodgeball_instruction_dodge": { + "message": "Κάντε κλικ σε ένα ξωτικό για να αποφύγετε τις χιονόμπαλες" + }, + "snowdodgeball_instruction_throw": { + "message": "Κάντε κλικ για να ρίξετε χιονόμπαλες" + }, + "snowdodgeball_match_lost": { + "message": "Χάσατε" + }, + "snowdodgeball_match_won": { + "message": "Κερδίσατε!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Ακρίβεια" + }, + "snowdodgeball_stats_hits": { + "message": "Χτυπήματα που δεχτήκατε" + }, + "snowdodgeball_stats_throws": { + "message": "Χιονόμπαλες που ρίξατε" + }, "snowflake_choose_background": { "message": "Επιλέξτε ένα φόντο" }, diff --git a/_messages/en-GB.json b/_messages/en-GB.json index b7739d890..f74aa8ff3 100644 --- a/_messages/en-GB.json +++ b/_messages/en-GB.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Snowbox" }, + "scene_snowdodgeball": { + "message": "Snow dodgeball" + }, "scene_snowflake": { "message": "Code a Snowflake" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Waiting for more of Santa's elves" }, + "snowdodgeball_instruction_dodge": { + "message": "Click on an elf to dodge snowballs" + }, + "snowdodgeball_instruction_throw": { + "message": "Click to throw snowballs" + }, + "snowdodgeball_match_lost": { + "message": "Match lost" + }, + "snowdodgeball_match_won": { + "message": "Match won!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Accuracy" + }, + "snowdodgeball_stats_hits": { + "message": "Hits taken" + }, + "snowdodgeball_stats_throws": { + "message": "Snowballs thrown" + }, "snowflake_choose_background": { "message": "Pick a backdrop" }, diff --git a/_messages/en.json b/_messages/en.json index 4c0d9ae44..8cff57b25 100644 --- a/_messages/en.json +++ b/_messages/en.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Snowbox" }, + "scene_snowdodgeball": { + "message": "Snow Dodgeball" + }, "scene_snowflake": { "message": "Code a Snowflake" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Waiting for more of Santa's elves" }, + "snowdodgeball_instruction_dodge": { + "message": "Click on an elf to dodge snowballs" + }, + "snowdodgeball_instruction_throw": { + "message": "Click to throw snowballs" + }, + "snowdodgeball_match_lost": { + "message": "Match lost" + }, + "snowdodgeball_match_won": { + "message": "Match won!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Accuracy" + }, + "snowdodgeball_stats_hits": { + "message": "Hits Taken" + }, + "snowdodgeball_stats_throws": { + "message": "Snowballs Thrown" + }, "snowflake_choose_background": { "message": "Pick a backdrop" }, diff --git a/_messages/es-419.json b/_messages/es-419.json index 8fecc78ab..a371cb571 100644 --- a/_messages/es-419.json +++ b/_messages/es-419.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Juego libre en la nieve" }, + "scene_snowdodgeball": { + "message": "Dodgeball en la nieve" + }, "scene_snowflake": { "message": "Codifica un copo de nieve" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Esperando a más duendes de Santa" }, + "snowdodgeball_instruction_dodge": { + "message": "Haz clic en un elfo para esquivar las bolas de nieve" + }, + "snowdodgeball_instruction_throw": { + "message": "Haz clic para lanzar bolas de nieve" + }, + "snowdodgeball_match_lost": { + "message": "Perdiste la partida" + }, + "snowdodgeball_match_won": { + "message": "Ganaste la partida" + }, + "snowdodgeball_stats_accuracy": { + "message": "Precisión" + }, + "snowdodgeball_stats_hits": { + "message": "Golpes recibidos" + }, + "snowdodgeball_stats_throws": { + "message": "Bolas de nieve arrojadas" + }, "snowflake_choose_background": { "message": "Elige un fondo" }, @@ -6375,7 +6399,7 @@ "message": "Níger" }, "trivia_medium_031_answer2": { - "message": "Suazilandia" + "message": "Esuatini" }, "trivia_medium_032": { "message": "El estrecho de Skagerrak, ¿separa a Dinamarca de qué país?" diff --git a/_messages/es.json b/_messages/es.json index 097295861..0dc35d926 100644 --- a/_messages/es.json +++ b/_messages/es.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Zona de pruebas nevada" }, + "scene_snowdodgeball": { + "message": "Balón prisionero en la nieve" + }, "scene_snowflake": { "message": "Crea un copo de nieve con código" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Esperando a que se unan más elfos de Papá Noel" }, + "snowdodgeball_instruction_dodge": { + "message": "Haz clic en un elfo para esquivar las bolas de nieve" + }, + "snowdodgeball_instruction_throw": { + "message": "Haz clic para lanzar bolas de nieve" + }, + "snowdodgeball_match_lost": { + "message": "¡Has perdido!" + }, + "snowdodgeball_match_won": { + "message": "¡Has ganado!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Precisión" + }, + "snowdodgeball_stats_hits": { + "message": "Golpes recibidos" + }, + "snowdodgeball_stats_throws": { + "message": "Bolas de nieve lanzadas" + }, "snowflake_choose_background": { "message": "Escoge un fondo" }, diff --git a/_messages/et.json b/_messages/et.json index ffe87f346..1074f6fac 100644 --- a/_messages/et.json +++ b/_messages/et.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Lumekast" }, + "scene_snowdodgeball": { + "message": "Snow Dodgeball" + }, "scene_snowflake": { "message": "Lumehelbe kodeerimine" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Ootame teisi päkapikke" }, + "snowdodgeball_instruction_dodge": { + "message": "Klõpsake päkapikul, et lumepallide eest kõrvale põigelda." + }, + "snowdodgeball_instruction_throw": { + "message": "Klõpsake lumepallide viskamiseks." + }, + "snowdodgeball_match_lost": { + "message": "Kaotasite matši" + }, + "snowdodgeball_match_won": { + "message": "Võitsite matši!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Täpsus" + }, + "snowdodgeball_stats_hits": { + "message": "Pihtasaamisi" + }, + "snowdodgeball_stats_throws": { + "message": "Visatud lumepalle" + }, "snowflake_choose_background": { "message": "Valige taust" }, diff --git a/_messages/eu.json b/_messages/eu.json index 9bd98dae0..4c1f836c9 100644 --- a/_messages/eu.json +++ b/_messages/eu.json @@ -585,7 +585,7 @@ "message": "Kuba" }, "cz_country_name": { - "message": "Txekiar Errepublika" + "message": "Txekiako Errepublika" }, "da_language_name": { "message": "Daniera" @@ -885,7 +885,7 @@ "message": "\"Hey Google, zer berri Ipar Poloan?\"" }, "familyguide_tips_tip02_body": { - "message": "Deskargatu Android-erako Bizarzuriren lorratzak aplikazioa Play Store-n. Herrixkaren eszena esklusibo bat aurkituko duzu, aplikazioan bakarrik erabilgarri dauden jokoekin." + "message": "Deskargatu Android-erako Bizarzuriren lorratzak aplikazioa Google Play Store-n. Herrixkaren eszena esklusibo bat aurkituko duzu, aplikazioan bakarrik erabilgarri dauden jokoekin." }, "familyguide_tips_tip02_title": { "message": "Android-erako aplikazioa" @@ -897,7 +897,7 @@ "message": "Android Wear" }, "familyguide_tips_tip04_body": { - "message": "Bilatu Bizarzuri (\"Santa\") Google-n, Bizarzuri hiru dimentsiotan ikusteko. Errealitate areagotuan ikusi ahalko duzu iPhone gehienetan eta Android-eko gailu berrienetan, kamerari agur egiten." + "message": "Bilatu Bizarzuri (\"Santa\") Google-n, Bizarzuri hiru dimentsiotan ikusteko. EAn ikusi ahalko duzu iPhone gehienetan eta Android-eko gailu berrienetan, kamerari agur egiten." }, "familyguide_tips_tip04_title": { "message": "Ekarri Bizarzuri zure egongelara" @@ -1440,7 +1440,7 @@ "message": "Bizarzuriren herrixkan jaietako jarduera dibertigarrien, hezitzaileen eta interaktiboen hamabosgarren urteurrena ospatzen ari dira. Bizarzuriren emazteak zuzenduta, iratxoek herria eraberritu dute eta itxura dotore eta polita eman diote, eduki berri-berri eta guzti. Gainera, txokolate beroarekin funtzionatzen duen berotze-sistema berri bati esker, aurrerantzean ez da beharrezkoa izango abendua bukatzera arte itxarotea herria urtzen ikusteko: abenduaren 4an egongo dira erabilgarri 24 jarduerak. Orain, iratxoek (eta zuk zeuk) denbora gehiago daukate Bizarzuriren herrixkan zehar ibiltzeko eta bertan jolasteko." }, "press-about-village2": { - "message": "Herrixkan, oinarrizko programazio-gaitasunak praktikatu, artelanak sortu, geografian trebatu eta \u003ca href=\"https://www.khanacademy.org/\"\u003eKhan Academy\u003c/a\u003e, \u003ca href=\"https://code.org/\"\u003eCode.org\u003c/a\u003e eta antzeko karitatezko erakundeei buruzko informazio gehiago lor dezakezu. Irakasleek bideo-gidak dituzten \u003ca href=\"./educators.html\"\u003eikasgai-planak deskarga ditzakete\u003c/a\u003e ikasleei oinarrizko programazio-gaitasunak nahiz jaietako tradizioak irakasteko, ingelesez, gaztelaniaz, frantsesez, japonieraz eta koreeraz." + "message": "Herrixkan, oinarrizko programazio-gaitasunak praktikatu, artelanak sortu, geografian trebatu eta \u003ca href=\"https://www.khanacademy.org/\"\u003eKhan Academy\u003c/a\u003e, \u003ca href=\"https://code.org/\"\u003eCode.org\u003c/a\u003e eta antzeko karitatezko erakundeei buruzko informazio gehiago lor dezakezu. Irakasleek bideogidak dituzten \u003ca href=\"./educators.html\"\u003eikasgai-planak deskarga ditzakete\u003c/a\u003e ikasleei oinarrizko programazio-gaitasunak nahiz jaietako tradizioak irakasteko, ingelesez, gaztelaniaz, frantsesez, japonieraz eta koreeraz." }, "press-asset-animation": { "message": "Animazioa" @@ -1461,7 +1461,7 @@ "message": "Heziketa-abenturak" }, "press-edu1": { - "message": "\u003ca href=\"./educators.html\"\u003eHezitzaileentzako baliabide-orria\u003c/a\u003e zabaldu dugu irakasleei bideo-gidak eskaintzeko, ikasketa-planekin batera erabil ditzaten. Orain, bost hizkuntzatan daude erabilgarri. Gogoko dituzun jokoak bertan daude oraindik, eta programazioaren zein mundu osoko jaietako tradizioen oinarrizko kontzeptuak aurkezten dira. Google-ren Bizarzuriren lorratzak eginbideari esker, aurtengo jaietan gauza mordoa ikasiko dute txikitxoek. Joan \u003ca href=\"./educators.html\"\u003egure joko hezigarrietara\u003c/a\u003e informazio gehiago lortzeko." + "message": "\u003ca href=\"./educators.html\"\u003eHezitzaileentzako baliabide-orria\u003c/a\u003e zabaldu dugu hezitzaileei bideogidak eskaintzeko, ikasketa-planekin batera erabil ditzaten. Orain, bost hizkuntzatan daude erabilgarri. Gogoko dituzun jokoak bertan daude oraindik, eta programazioaren zein mundu osoko jaietako tradizioen oinarrizko kontzeptuak aurkezten dira. Google-ren Bizarzuriren lorratzak eginbideari esker, aurtengo jaietan gauza mordoa ikasiko dute txikitxoek. Joan \u003ca href=\"./educators.html\"\u003egure joko hezigarrietara\u003c/a\u003e informazio gehiago lortzeko." }, "press-games-title": { "message": "Dibertsio eta joko guztiak" @@ -1794,7 +1794,7 @@ "message": "Non dago Bizarzuri?" }, "scene_santaselfie": { - "message": "Bizarzuriren autoargazkia" + "message": "Bizarzuriren selfiea" }, "scene_seasonofcaring": { "message": "Laguntzeko garaia" @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Elurretako proben eremua" }, + "scene_snowdodgeball": { + "message": "Elurrezko zelai errea" + }, "scene_snowflake": { "message": "Programatu elur-maluta bat" }, @@ -1875,7 +1878,7 @@ "message": "Satelitea" }, "scene_videoscene_selfies": { - "message": "Bizarzuriren autoargazkiak" + "message": "Bizarzuriren selfieak" }, "scene_videoscene_slackingoff": { "message": "Alferkerian" @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Bizarzuriren iratxo gehiagoren zain" }, + "snowdodgeball_instruction_dodge": { + "message": "Egin klik iratxo batean elur-bolak saihesteko" + }, + "snowdodgeball_instruction_throw": { + "message": "Egin klik elur-bolak jaurtitzeko" + }, + "snowdodgeball_match_lost": { + "message": "Galdu duzu partida" + }, + "snowdodgeball_match_won": { + "message": "Irabazi duzu partida!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Zehaztasuna" + }, + "snowdodgeball_stats_hits": { + "message": "Jasotako kolpeak" + }, + "snowdodgeball_stats_throws": { + "message": "Jaurtitako elur-bolak" + }, "snowflake_choose_background": { "message": "Aukeratu atzeko planoa" }, @@ -4881,7 +4905,7 @@ "message": "Zein herrialde ez dago Balkanetako penintsulan?" }, "trivia_hard_012_answer0": { - "message": "Txekiar Errepublika" + "message": "Txekiako Errepublika" }, "trivia_hard_012_answer1": { "message": "Bulgaria" @@ -5079,7 +5103,7 @@ "message": "Israel" }, "trivia_hard_028_answer2": { - "message": "Bahrein" + "message": "Bahrain" }, "trivia_hard_029": { "message": "Zein herrialdek hartzen du longitude-gradu kopuru handiena?" @@ -6294,7 +6318,7 @@ "message": "Kinshasa" }, "trivia_medium_025": { - "message": "Zein da Txekiar Errepublikako hiririk handiena, Vltava ibaian kokatutako hiriburua dena?" + "message": "Zein da Txekiako Errepublikako hiririk handiena, Vltava ibaian kokatutako hiriburua dena?" }, "trivia_medium_025_answer0": { "message": "Praga" diff --git a/_messages/fi.json b/_messages/fi.json index 9f9ebf361..146345eae 100644 --- a/_messages/fi.json +++ b/_messages/fi.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Lumilaatikko" }, + "scene_snowdodgeball": { + "message": "Lumipallosota" + }, "scene_snowflake": { "message": "Koodaa lumihiutale" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Odotetaan lisää tonttuja" }, + "snowdodgeball_instruction_dodge": { + "message": "Vältä lumipalloja klikkaamalla tonttua" + }, + "snowdodgeball_instruction_throw": { + "message": "Heitä lumipalloja klikkaamalla" + }, + "snowdodgeball_match_lost": { + "message": "Ottelu hävitty" + }, + "snowdodgeball_match_won": { + "message": "Ottelu voitettu!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Tarkkuus" + }, + "snowdodgeball_stats_hits": { + "message": "Saamasi osumat" + }, + "snowdodgeball_stats_throws": { + "message": "Heitetyt lumipallot" + }, "snowflake_choose_background": { "message": "Valitse taustaväri." }, diff --git a/_messages/fr-CA.json b/_messages/fr-CA.json index 7356453f9..296a08aff 100644 --- a/_messages/fr-CA.json +++ b/_messages/fr-CA.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Bac à neige" }, + "scene_snowdodgeball": { + "message": "Ballon-chasseur dans la neige" + }, "scene_snowflake": { "message": "Coder un flocon de neige" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "En attente d'autres lutins du père Noël" }, + "snowdodgeball_instruction_dodge": { + "message": "Cliquez sur un lutin pour esquiver les boules de neige" + }, + "snowdodgeball_instruction_throw": { + "message": "Cliquez pour lancer des boules de neige" + }, + "snowdodgeball_match_lost": { + "message": "Match perdu" + }, + "snowdodgeball_match_won": { + "message": "Match gagné!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Précision" + }, + "snowdodgeball_stats_hits": { + "message": "Boules de neige reçues" + }, + "snowdodgeball_stats_throws": { + "message": "Boules de neige lancées" + }, "snowflake_choose_background": { "message": "Sélectionnez un arrière-plan" }, diff --git a/_messages/fr.json b/_messages/fr.json index f7ebb2e6f..f02c2f11a 100644 --- a/_messages/fr.json +++ b/_messages/fr.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Bac à neige" }, + "scene_snowdodgeball": { + "message": "Dodgeball de neige" + }, "scene_snowflake": { "message": "Codez un flocon de neige" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "En attente d'autres lutins du père Noël" }, + "snowdodgeball_instruction_dodge": { + "message": "Cliquez sur un elfe pour esquiver les boules de neige" + }, + "snowdodgeball_instruction_throw": { + "message": "Cliquez pour lancer des boules de neige" + }, + "snowdodgeball_match_lost": { + "message": "Match perdu" + }, + "snowdodgeball_match_won": { + "message": "Match gagné !" + }, + "snowdodgeball_stats_accuracy": { + "message": "Précision" + }, + "snowdodgeball_stats_hits": { + "message": "Coups encaissés" + }, + "snowdodgeball_stats_throws": { + "message": "Boules de neige lancées" + }, "snowflake_choose_background": { "message": "Choisissez un fond" }, diff --git a/_messages/gl.json b/_messages/gl.json index bd9892254..cb374b1ac 100644 --- a/_messages/gl.json +++ b/_messages/gl.json @@ -3,7 +3,7 @@ "message": "En Noiteboa, teremos o honor de presentar o panel de control de Papá Noel, a tecnoloxía que impulsa a súa zorra durante a súa viaxe por todo o mundo. Recibimos esta vista previa tan especial dun dos moitos elfos programadores de Papá Noel, que traballan duro no Polo Norte para axudarlle a prepararse para o gran día. O panel de control de Papá Noel, que inclúe a tecnoloxía máis recente e innovadora de Google Maps, permitirá seguir a súa viaxe polo mundo e coñecer un pouco os sitios que visite." }, "about-content-three": { - "message": "Google creou e desenvolveu Sigue a Papá Noel de Google Maps cun pouco de axuda dos elfos de Papá Noel. Necesitas axuda ou tes algunha outra pregunta? Visita o noso \u003ca href=\"//support.google.com/santatracker\"\u003eCentro de Axuda\u003c/a\u003e." + "message": "Google creou e desenvolveu Sigue a Papá Noel de Google Maps cun pouco de axuda dos elfos de Papá Noel. Necesitas axuda ou tes algunha outra pregunta? Visita o noso \u003ca href=\"//support.google.com/santatracker\"\u003eCentro de axuda\u003c/a\u003e." }, "about-content-two": { "message": "Os nosos amigos, os elfos, invitáronte a visitar a aldea de Papá Noel mentres el se prepara para a viaxe. Pasa e explora a aldea; quizais encontres actividades divertidas e coñezas elfos moi interesantes." @@ -1344,7 +1344,7 @@ "message": "Nova Zelandia" }, "nz_tradition": { - "message": "Como o Nadal é no verán en Nova Zelandia, os amigos e familiares reúnense arredor dunha grellada. As comidas típicas de Nadal son o cordeiro asado, ás veces cociñado nun hangi (un buraco ou forno baixo a terra). A sobremesa favorita é pavlova, un merengue cuberto de froita fresca, como amorodos ou kiwi." + "message": "Como o Nadal é no verán en Nova Zelandia, os amigos e familiares reúnense arredor dunha grellada. As comidas típicas de Nadal son o cordeiro asado, ás veces cociñado nun hangi (un burato ou forno baixo a terra). A sobremesa favorita é pavlova, un merengue cuberto de froita fresca, como amorodos ou kiwi." }, "offline": { "message": "Sen conexión" @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Zona de probas nevada" }, + "scene_snowdodgeball": { + "message": "Brilé na neve" + }, "scene_snowflake": { "message": "Codifica unha folerpa" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Agardando por máis elfos de Papá Noel" }, + "snowdodgeball_instruction_dodge": { + "message": "Fai clic nun elfo para esquivar as bólas de neve" + }, + "snowdodgeball_instruction_throw": { + "message": "Fai clic para lanzar bólas de neve" + }, + "snowdodgeball_match_lost": { + "message": "Perdiches" + }, + "snowdodgeball_match_won": { + "message": "Gañaches!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Precisión" + }, + "snowdodgeball_stats_hits": { + "message": "Golpes recibidos" + }, + "snowdodgeball_stats_throws": { + "message": "Bólas de neve lanzadas" + }, "snowflake_choose_background": { "message": "Selecciona un fondo de pantalla" }, @@ -5274,7 +5298,7 @@ "message": "Río Indo" }, "trivia_hard_045": { - "message": "Que pico está más preto do ecuador?" + "message": "Que pico está máis preto do ecuador?" }, "trivia_hard_045_answer0": { "message": "Kilimanjaro" diff --git a/_messages/hi.json b/_messages/hi.json index 3ad92ffdb..55991bd29 100644 --- a/_messages/hi.json +++ b/_messages/hi.json @@ -1062,7 +1062,7 @@ "message": "इटली" }, "it_language_name": { - "message": "इतालवी" + "message": "इटैलियन" }, "ja_language_name": { "message": "जापानी" @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "स्नोबॉक्स" }, + "scene_snowdodgeball": { + "message": "स्नो डॉजबॉल" + }, "scene_snowflake": { "message": "कोड करके अपना स्नोफ़्लेक बनाएं" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "सैंटा के और छोटे जादूगरों का इंतज़ार है" }, + "snowdodgeball_instruction_dodge": { + "message": "स्नोबॉल से बचने के लिए, एल्फ़ पर क्लिक करें" + }, + "snowdodgeball_instruction_throw": { + "message": "स्नोबॉल फेंकने के लिए क्लिक करें" + }, + "snowdodgeball_match_lost": { + "message": "आपकी टीम हार गई" + }, + "snowdodgeball_match_won": { + "message": "आपकी टीम जीत गई!" + }, + "snowdodgeball_stats_accuracy": { + "message": "सफलता का प्रतिशत" + }, + "snowdodgeball_stats_hits": { + "message": "आपकी टीम को लगी कुल स्नोबॉल" + }, + "snowdodgeball_stats_throws": { + "message": "फेंके गए स्नोबॉल" + }, "snowflake_choose_background": { "message": "बैकग्राउंड चुनें" }, @@ -2220,7 +2244,7 @@ "message": "केक" }, "speedsketch_item~calculator": { - "message": "कैल्क्युलेटर" + "message": "कैलकुलेटर" }, "speedsketch_item~calendar": { "message": "कैलेंडर" diff --git a/_messages/hr.json b/_messages/hr.json index c74b6004e..f76d42c38 100644 --- a/_messages/hr.json +++ b/_messages/hr.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Snježna radionica" }, + "scene_snowdodgeball": { + "message": "Dodgeball na snijegu" + }, "scene_snowflake": { "message": "Izrada pahuljice pomoću koda" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Čekamo još Djedovih vilenjaka" }, + "snowdodgeball_instruction_dodge": { + "message": "Kliknite vilenjaka da biste izbjegli grude" + }, + "snowdodgeball_instruction_throw": { + "message": "Kliknite da biste bacili grude" + }, + "snowdodgeball_match_lost": { + "message": "Izgubili ste igru" + }, + "snowdodgeball_match_won": { + "message": "Pobijedili ste u igri!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Točnost" + }, + "snowdodgeball_stats_hits": { + "message": "Broj primljenih gruda" + }, + "snowdodgeball_stats_throws": { + "message": "Broj bačenih gruda" + }, "snowflake_choose_background": { "message": "Odaberi pozadinu" }, diff --git a/_messages/hu.json b/_messages/hu.json index 77b8d9c8f..3224c1a09 100644 --- a/_messages/hu.json +++ b/_messages/hu.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Hóépítő" }, + "scene_snowdodgeball": { + "message": "Hógolyócsata" + }, "scene_snowflake": { "message": "Hópehelykódolás" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Várakozás a Mikulás további manóira" }, + "snowdodgeball_instruction_dodge": { + "message": "A hógolyók elkerüléséhez kattintson egy manóra" + }, + "snowdodgeball_instruction_throw": { + "message": "Hógolyó dobásához kattintson" + }, + "snowdodgeball_match_lost": { + "message": "Elvesztette a meccset" + }, + "snowdodgeball_match_won": { + "message": "Megnyerte a meccset!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Pontosság" + }, + "snowdodgeball_stats_hits": { + "message": "Kapott találatok" + }, + "snowdodgeball_stats_throws": { + "message": "Eldobott hógolyók" + }, "snowflake_choose_background": { "message": "Válassz hátteret" }, diff --git a/_messages/id.json b/_messages/id.json index cf12a9476..029ed7784 100644 --- a/_messages/id.json +++ b/_messages/id.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Snowbox" }, + "scene_snowdodgeball": { + "message": "Snow Dodgeball" + }, "scene_snowflake": { "message": "Buat Kode Snowflake" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Menunggu kurcaci Sinterklas lainnya" }, + "snowdodgeball_instruction_dodge": { + "message": "Klik kurcaci untuk menghindari bola salju" + }, + "snowdodgeball_instruction_throw": { + "message": "Klik ke arah mana pun untuk melempar bola salju" + }, + "snowdodgeball_match_lost": { + "message": "Kamu kalah" + }, + "snowdodgeball_match_won": { + "message": "Kamu menang!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Akurasi" + }, + "snowdodgeball_stats_hits": { + "message": "Terkena Lemparan" + }, + "snowdodgeball_stats_throws": { + "message": "Lemparan Bola Salju" + }, "snowflake_choose_background": { "message": "Pilih tampilan latar" }, diff --git a/_messages/is.json b/_messages/is.json index 30a3d6046..17e435d31 100644 --- a/_messages/is.json +++ b/_messages/is.json @@ -717,10 +717,10 @@ "message": "Fagnaðu allan liðlangan mánuðinn" }, "familyguide_assistant_body_01": { - "message": "Notaðu Google hjálparann til að fá frekari upplýsingar um jólasveininn í desember! Þessi samþætting er í boði á völdum tungumálum." + "message": "Notaðu Google-hjálparann til að fá frekari upplýsingar um jólasveininn í desember! Þessi samþætting er í boði á völdum tungumálum." }, "familyguide_assistant_body_02": { - "message": "Með leyfi foreldra geta börn undir þrettán ára aldri einnig fengið sérsniðið viðmót fyrir Google hjálparann þegar þau skrá sig inn á eigin reikning. Umsjón með reikningnum er í gegnum \u003ca href=\"https://families.google.com/familylink/\" target=\"_blank\"\u003eFamily Link\u003c/a\u003e." + "message": "Með leyfi foreldra geta börn undir þrettán ára aldri einnig fengið sérsniðið viðmót fyrir Google-hjálparann þegar þau skrá sig inn á eigin reikning. Umsjón með reikningnum er í gegnum \u003ca href=\"https://families.google.com/familylink/\" target=\"_blank\"\u003eFamily Link\u003c/a\u003e." }, "familyguide_assistant_card01_body": { "message": "Google-hjálparinn tengir þig við Norðurpólinn en þar er Sveinki á fullu að undirbúa tónleika. Hann kann bara eitt lag – getur þú hjálpað honum að finna út hvernig best er að spila það?" @@ -729,13 +729,13 @@ "message": "Hringdu í jólasveininn" }, "familyguide_assistant_card02_body": { - "message": "Fylgstu með fréttum frá Norðurpólnum daglega og fáðu að vita hvað Sveinki og álfarnir hans eru að bralla. Spurðu Google hjálparann bara hvað sé að frétta frá Norðurpólnum." + "message": "Fylgstu með fréttum frá Norðurpólnum daglega og fáðu að vita hvað Sveinki og álfarnir hans eru að bralla. Spurðu Google-hjálparann bara hvað sé að frétta frá Norðurpólnum." }, "familyguide_assistant_card02_title": { "message": "Fréttir frá Norðurpólnum" }, "familyguide_assistant_card03_body": { - "message": "Þegar Sveinki er ekki önnum kafinn með álfunum á vinnustofunni finnst honum gaman að spreyta sig á gríninu. Spurðu Google hjálparann um Sveinkagrín og þá færðu brandara frá jólasveininum." + "message": "Þegar Sveinki er ekki önnum kafinn með álfunum á vinnustofunni finnst honum gaman að spreyta sig á gríninu. Spurðu Google-hjálparann um Sveinkagrín og þá færðu brandara frá jólasveininum." }, "familyguide_assistant_card03_title": { "message": "Sveinkagrín" @@ -879,7 +879,7 @@ "message": "Fáðu meira úr Hvar er jólasveinninn þessi jólin með öllum uppáhalds Google vörunum þínum." }, "familyguide_tips_tip01_body": { - "message": "Biddu Google hjálparann um skemmtilegar fréttir á hverjum degi frá Spékoppi álfi." + "message": "Biddu Google-hjálparann um skemmtilegar fréttir á hverjum degi frá Spékoppi álfi." }, "familyguide_tips_tip01_title": { "message": "„OK Google, hvað er að frétta frá Norðurpólnum?“" @@ -915,7 +915,7 @@ "message": "Fjör fyrir alla fjölskylduna um hátíðarnar" }, "familylink": { - "message": "Bæta Google reikningi barnsins þíns við sem stjórnað er með Family Link" + "message": "Bæta Google-reikningi barnsins þíns við sem stjórnað er með Family Link" }, "featured": { "message": "Valið efni" @@ -1431,7 +1431,7 @@ "message": "Um Hvar er jólasveinninn" }, "press-about-tracker1": { - "message": "Þann 24. desember munum við ræsa Hvar er jólasveinninn, stjórnborð þar sem aðdáendur geta fylgst með ferðum hans þar sem hann hendist milli borga. Þú getur líka skoðað hvar hann er hverju sinni í Google-kortum og spurt Google hjálparann, „Ok Google, hvar er jólasveinninn?“ en þannig geturðu fylgst með ferðum Sveinka hvar sem þú ert." + "message": "Þann 24. desember munum við ræsa Hvar er jólasveinninn, stjórnborð þar sem aðdáendur geta fylgst með ferðum hans þar sem hann hendist milli borga. Þú getur líka skoðað hvar hann er hverju sinni í Google-kortum og spurt Google-hjálparann, „Ok Google, hvar er jólasveinninn?“ en þannig geturðu fylgst með ferðum Sveinka hvar sem þú ert." }, "press-about-village": { "message": "Um jólasveinaþorpið" @@ -1503,7 +1503,7 @@ "message": "Ný leið til að fylgjast með Sveinka:" }, "press-whatsnew4-text": { - "message": "Ef þú ert í stuði fyrir meira hátíðargaman skaltu skoða hátíðarlímmiða á Gboard sem fást fyrir \u003ca href=\"https://play.google.com/store/apps/details?id=com.google.android.inputmethod.latin\"\u003eAndroid\u003c/a\u003e, \u003ca href=\"https://itunes.apple.com/us/app/gboard-the-google-keyboard/id1091700242?mt=8\"\u003eiOS\u003c/a\u003e og \u003ca href=\"https://play.google.com/store/apps/details?id=com.google.android.apps.santatracker\"\u003eAndroid forritið okkar\u003c/a\u003e. Og njóttu þess líka að hlusta á sögu með því að spyrja Google hjálparann: „Segðu mér hátíðarsögu“ og „Hvar er jólasveinninn?“" + "message": "Ef þú ert í stuði fyrir meira hátíðargaman skaltu skoða hátíðarlímmiða á Gboard sem fást fyrir \u003ca href=\"https://play.google.com/store/apps/details?id=com.google.android.inputmethod.latin\"\u003eAndroid\u003c/a\u003e, \u003ca href=\"https://itunes.apple.com/us/app/gboard-the-google-keyboard/id1091700242?mt=8\"\u003eiOS\u003c/a\u003e og \u003ca href=\"https://play.google.com/store/apps/details?id=com.google.android.apps.santatracker\"\u003eAndroid forritið okkar\u003c/a\u003e. Og njóttu þess líka að hlusta á sögu með því að spyrja Google-hjálparann: „Segðu mér hátíðarsögu“ og „Hvar er jólasveinninn?“" }, "press-whatsnew4-title": { "message": "Meira hátíðafjör:" @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Snjókassi" }, + "scene_snowdodgeball": { + "message": "Snjókast" + }, "scene_snowflake": { "message": "Kóðaðu snjókorn" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Bíður eftir fleiri álfum Sveinka" }, + "snowdodgeball_instruction_dodge": { + "message": "Smelltu á álf til að forðast snjóbolta" + }, + "snowdodgeball_instruction_throw": { + "message": "Smelltu til að kasta snjóboltum" + }, + "snowdodgeball_match_lost": { + "message": "Þú tapaðir" + }, + "snowdodgeball_match_won": { + "message": "Þú vannst!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Nákvæmni" + }, + "snowdodgeball_stats_hits": { + "message": "Högg sem þú fékkst" + }, + "snowdodgeball_stats_throws": { + "message": "Snjóboltum kastað" + }, "snowflake_choose_background": { "message": "Veldu bakgrunn" }, diff --git a/_messages/it.json b/_messages/it.json index 7b469b4cc..3e6426226 100644 --- a/_messages/it.json +++ b/_messages/it.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Giochiamo con la neve" }, + "scene_snowdodgeball": { + "message": "Dodgeball a palle di neve" + }, "scene_snowflake": { "message": "Disegna con il codice" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "In attesa di altri folletti di Babbo Natale" }, + "snowdodgeball_instruction_dodge": { + "message": "Fai clic su un elfo per schivare le palle di neve" + }, + "snowdodgeball_instruction_throw": { + "message": "Fai clic per lanciare palle di neve" + }, + "snowdodgeball_match_lost": { + "message": "Partita persa" + }, + "snowdodgeball_match_won": { + "message": "Partita vinta!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Precisione" + }, + "snowdodgeball_stats_hits": { + "message": "Colpi ricevuti" + }, + "snowdodgeball_stats_throws": { + "message": "Palle di neve lanciate" + }, "snowflake_choose_background": { "message": "Scegli uno sfondo" }, diff --git a/_messages/ja.json b/_messages/ja.json index a91c27b5c..b76da1f15 100644 --- a/_messages/ja.json +++ b/_messages/ja.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "雪遊び" }, + "scene_snowdodgeball": { + "message": "雪玉ドッジボール" + }, "scene_snowflake": { "message": "雪の結晶を描こう" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "サンタさんの妖精たちがもっと集まるのを待っています" }, + "snowdodgeball_instruction_dodge": { + "message": "エルフをクリックして雪玉をよけよう" + }, + "snowdodgeball_instruction_throw": { + "message": "クリックして雪玉を投げよう" + }, + "snowdodgeball_match_lost": { + "message": "負け" + }, + "snowdodgeball_match_won": { + "message": "勝ち!" + }, + "snowdodgeball_stats_accuracy": { + "message": "成功率" + }, + "snowdodgeball_stats_hits": { + "message": "当てられた数" + }, + "snowdodgeball_stats_throws": { + "message": "投げた雪玉" + }, "snowflake_choose_background": { "message": "背景を選択します" }, diff --git a/_messages/ko.json b/_messages/ko.json index 2c89cade5..9df39568b 100644 --- a/_messages/ko.json +++ b/_messages/ko.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "스노우 블록 꾸미기" }, + "scene_snowdodgeball": { + "message": "눈덩이 피하기" + }, "scene_snowflake": { "message": "눈송이 코드" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "산타의 엘프 친구들을 기다리는 중" }, + "snowdodgeball_instruction_dodge": { + "message": "눈덩이를 피하려면 엘프를 클릭하세요." + }, + "snowdodgeball_instruction_throw": { + "message": "클릭하여 눈덩이를 던지세요." + }, + "snowdodgeball_match_lost": { + "message": "경기 패배" + }, + "snowdodgeball_match_won": { + "message": "경기 승리!" + }, + "snowdodgeball_stats_accuracy": { + "message": "정확성" + }, + "snowdodgeball_stats_hits": { + "message": "받은 타격" + }, + "snowdodgeball_stats_throws": { + "message": "던진 눈덩이 수" + }, "snowflake_choose_background": { "message": "배경을 선택하세요." }, @@ -3480,7 +3504,7 @@ "message": "터키" }, "tr_language_name": { - "message": "터키어" + "message": "튀르키예어" }, "tracker-game_awesome": { "message": "보물의 탑 게임 시작하기" diff --git a/_messages/lt.json b/_messages/lt.json index b301dd280..bfc5acf3b 100644 --- a/_messages/lt.json +++ b/_messages/lt.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Sniego dėžė" }, + "scene_snowdodgeball": { + "message": "Snow Dodgeball" + }, "scene_snowflake": { "message": "Sugeneruokite snaigės kodą" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Laukiama daugiau Kalėdų Senelio elfų" }, + "snowdodgeball_instruction_dodge": { + "message": "Spustelėkite elfą, kad išvengtumėte sniego gniūžčių" + }, + "snowdodgeball_instruction_throw": { + "message": "Spustelėkite, kad mestumėte sniego gniūžtes" + }, + "snowdodgeball_match_lost": { + "message": "Rungtynės pralaimėtos" + }, + "snowdodgeball_match_won": { + "message": "Rungtynės laimėtos!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Tikslumas" + }, + "snowdodgeball_stats_hits": { + "message": "Pataikymai" + }, + "snowdodgeball_stats_throws": { + "message": "Mestos sniego gniūžtės" + }, "snowflake_choose_background": { "message": "Pasirinkite foną" }, diff --git a/_messages/lv.json b/_messages/lv.json index 898ab59ae..353e690e7 100644 --- a/_messages/lv.json +++ b/_messages/lv.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Sniegakaste" }, + "scene_snowdodgeball": { + "message": "Sniega piku kaujas" + }, "scene_snowflake": { "message": "Sniegpārsliņas izveide ar kodu" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Notiek citu Ziemassvētku vecīša rūķīšu gaidīšana" }, + "snowdodgeball_instruction_dodge": { + "message": "Noklikšķiniet uz elfa, lai izvairītos no sniega pikām." + }, + "snowdodgeball_instruction_throw": { + "message": "Noklikšķiniet, lai mestu sniega pikas." + }, + "snowdodgeball_match_lost": { + "message": "Spēle zaudēta" + }, + "snowdodgeball_match_won": { + "message": "Spēle uzvarēta!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Precizitāte" + }, + "snowdodgeball_stats_hits": { + "message": "Saņemtie trāpījumi" + }, + "snowdodgeball_stats_throws": { + "message": "Aizmestās sniega pikas" + }, "snowflake_choose_background": { "message": "Izvēlieties fonu" }, @@ -4068,7 +4092,7 @@ "message": "Atēnas" }, "trivia_beginner_041_answer1": { - "message": "Kijeva" + "message": "Kijiva" }, "trivia_beginner_041_answer2": { "message": "Lima" @@ -5439,7 +5463,7 @@ "message": "Berlīne" }, "trivia_hard_058_answer2": { - "message": "Kijeva" + "message": "Kijiva" }, "trivia_hard_059": { "message": "Nosauciet Kostarikas lielāko pilsētu — nozīmīgu Panamerikas lielceļa pieturvietu." diff --git a/_messages/ml.json b/_messages/ml.json index e0631136e..6c0698acd 100644 --- a/_messages/ml.json +++ b/_messages/ml.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "സ്നോ‌ബോക്സ്" }, + "scene_snowdodgeball": { + "message": "സ്നോ ഡോഡ്ജ്ബോൾ" + }, "scene_snowflake": { "message": "കോഡ് എ സ്നോഫ്ലെയ്ക്ക്" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "കൂടുതൽ സാന്തയുടെ കുട്ടിച്ചാത്തൻമാർക്കായി കാത്തിരിക്കുന്നു" }, + "snowdodgeball_instruction_dodge": { + "message": "സ്നോബോളുകൾ ഡോഡ്ജ് ചെയ്യാൻ എൽഫിൽ ക്ലിക്ക് ചെയ്യുക" + }, + "snowdodgeball_instruction_throw": { + "message": "സ്നോബോളുകൾ ത്രോ ചെയ്യാൻ ക്ലിക്ക് ചെയ്യുക" + }, + "snowdodgeball_match_lost": { + "message": "മത്സരം തോറ്റു" + }, + "snowdodgeball_match_won": { + "message": "മത്സരം ജയിച്ചു!" + }, + "snowdodgeball_stats_accuracy": { + "message": "കൃത്യത" + }, + "snowdodgeball_stats_hits": { + "message": "ലഭിച്ച ഹിറ്റുകൾ" + }, + "snowdodgeball_stats_throws": { + "message": "ത്രോ ചെയ്ത സ്നോബോളുകൾ" + }, "snowflake_choose_background": { "message": "ഒരു ബാക്ക്‌ഡ്രോപ്പ് തിരഞ്ഞെടുക്കുക" }, diff --git a/_messages/nl.json b/_messages/nl.json index 9a1b86bce..1a753c72d 100644 --- a/_messages/nl.json +++ b/_messages/nl.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Sneeuwbox" }, + "scene_snowdodgeball": { + "message": "Sneeuwballen gooien" + }, "scene_snowflake": { "message": "Sneeuwvlokken coderen" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Wachten op meer elfen" }, + "snowdodgeball_instruction_dodge": { + "message": "Klik op een elf om sneeuwballen te ontwijken" + }, + "snowdodgeball_instruction_throw": { + "message": "Klik om sneeuwballen te gooien" + }, + "snowdodgeball_match_lost": { + "message": "Wedstrijd verloren" + }, + "snowdodgeball_match_won": { + "message": "Wedstrijd gewonnen!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Nauwkeurigheid" + }, + "snowdodgeball_stats_hits": { + "message": "Geraakt door sneeuwbal" + }, + "snowdodgeball_stats_throws": { + "message": "Sneeuwballen gegooid" + }, "snowflake_choose_background": { "message": "Een achtergrond kiezen" }, diff --git a/_messages/no.json b/_messages/no.json index 3c8a42202..b150a5898 100644 --- a/_messages/no.json +++ b/_messages/no.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Snøkasse" }, + "scene_snowdodgeball": { + "message": "Snødodgeball" + }, "scene_snowflake": { "message": "Koding av et snøfnugg" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Venter på flere av julenissens alver" }, + "snowdodgeball_instruction_dodge": { + "message": "Klikk på en alv for å unngå snøballer" + }, + "snowdodgeball_instruction_throw": { + "message": "Klikk for å kaste snøballer" + }, + "snowdodgeball_match_lost": { + "message": "Kampen er tapt" + }, + "snowdodgeball_match_won": { + "message": "Kampen er vunnet!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Nøyaktighet" + }, + "snowdodgeball_stats_hits": { + "message": "Antall ganger du ble truffet" + }, + "snowdodgeball_stats_throws": { + "message": "Snøballer kastet" + }, "snowflake_choose_background": { "message": "Velg en bakgrunn" }, diff --git a/_messages/pl.json b/_messages/pl.json index 1249cab6d..f683a4cad 100644 --- a/_messages/pl.json +++ b/_messages/pl.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Śnieżne klocki" }, + "scene_snowdodgeball": { + "message": "Snow Dodgeball" + }, "scene_snowflake": { "message": "Zakoduj śnieżynkę" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Czekamy na więcej elfów" }, + "snowdodgeball_instruction_dodge": { + "message": "Kliknij elfa, aby zrobić unik przed śnieżkami" + }, + "snowdodgeball_instruction_throw": { + "message": "Kliknij, aby rzucać śnieżkami" + }, + "snowdodgeball_match_lost": { + "message": "Mecz przegrany" + }, + "snowdodgeball_match_won": { + "message": "Mecz wygrany" + }, + "snowdodgeball_stats_accuracy": { + "message": "Dokładność" + }, + "snowdodgeball_stats_hits": { + "message": "Trafienia" + }, + "snowdodgeball_stats_throws": { + "message": "Rzucone śnieżki" + }, "snowflake_choose_background": { "message": "Wybierz tło" }, diff --git a/_messages/pt-BR.json b/_messages/pt-BR.json index a93f3f9bf..cbcc7ce56 100644 --- a/_messages/pt-BR.json +++ b/_messages/pt-BR.json @@ -1188,7 +1188,7 @@ "message": "Novo" }, "next": { - "message": "Próxima" + "message": "Avançar" }, "next_game": { "message": "Próximo jogo" @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Caixa de neve" }, + "scene_snowdodgeball": { + "message": "Queimada na neve" + }, "scene_snowflake": { "message": "Crie um floco de neve" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Esperando mais duendes do Papai Noel" }, + "snowdodgeball_instruction_dodge": { + "message": "Clique em um elfo para desviar das bolas de neve" + }, + "snowdodgeball_instruction_throw": { + "message": "Clique para jogar bolas de neve" + }, + "snowdodgeball_match_lost": { + "message": "Não foi dessa vez" + }, + "snowdodgeball_match_won": { + "message": "Você ganhou!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Precisão" + }, + "snowdodgeball_stats_hits": { + "message": "Boladas tomadas" + }, + "snowdodgeball_stats_throws": { + "message": "Bolas de neve lançadas" + }, "snowflake_choose_background": { "message": "Escolha um cenário" }, diff --git a/_messages/pt-PT.json b/_messages/pt-PT.json index 4511382d0..b553cd0d2 100644 --- a/_messages/pt-PT.json +++ b/_messages/pt-PT.json @@ -1071,10 +1071,10 @@ "message": "Jamaica" }, "kg_country_name": { - "message": "Quirguizistão" + "message": "Quirguistão" }, "kg_tradition": { - "message": "No Quirguizistão, o Ayaz Ata (Avô Gelo) chega à meia-noite na véspera de Ano Novo e deixa presentes debaixo da árvore de Ano Novo. Quando soam as doze badaladas, as famílias acendem fogo de artifício por todo o país para celebrar este bonito evento. As cidades iluminam-se de tal forma que parece ser de dia." + "message": "No Quirguistão, o Ayaz Ata (Avô Gelo) chega à meia-noite na véspera de Ano Novo e deixa presentes debaixo da árvore de Ano Novo. Quando soam as doze badaladas, as famílias acendem fogo de artifício por todo o país para celebrar este bonito evento. As cidades iluminam-se de tal forma que parece ser de dia." }, "ko_language_name": { "message": "Coreano" @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Caixa de neve" }, + "scene_snowdodgeball": { + "message": "Jogo do mata com bolas de neve" + }, "scene_snowflake": { "message": "Codifique um floco de neve" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "A aguardar mais elfos do Pai Natal" }, + "snowdodgeball_instruction_dodge": { + "message": "Clique num elfo para desviar-se das bolas de neve" + }, + "snowdodgeball_instruction_throw": { + "message": "Clique para atirar bolas de neve" + }, + "snowdodgeball_match_lost": { + "message": "Partida perdida" + }, + "snowdodgeball_match_won": { + "message": "Partida ganha!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Precisão" + }, + "snowdodgeball_stats_hits": { + "message": "Ataques sofridos" + }, + "snowdodgeball_stats_throws": { + "message": "Bolas de neve lançadas" + }, "snowflake_choose_background": { "message": "Escolha um fundo" }, @@ -2109,7 +2133,7 @@ "message": "beisebol" }, "speedsketch_item~baseball bat": { - "message": "taco de basebol" + "message": "taco de beisebol" }, "speedsketch_item~basket": { "message": "cesto" @@ -7131,7 +7155,7 @@ "message": "Índia" }, "trivia_medium_094_answer2": { - "message": "Quirguizistão" + "message": "Quirguistão" }, "trivia_medium_095": { "message": "O lago Nipigon é o maior lago de que província canadiana?" diff --git a/_messages/ro.json b/_messages/ro.json index 753975319..60992411a 100644 --- a/_messages/ro.json +++ b/_messages/ro.json @@ -819,7 +819,7 @@ "message": "Mai multe de la Google" }, "familyguide_group_01_body": { - "message": "Elfii își îmbunătățesc constant aptitudinile de codificare. Explorați jocurile care vă vor învăța noțiunile de bază. Orice poate încerca să codeze!" + "message": "Elfii își îmbunătățesc constant aptitudinile de codificare. Explorează jocurile care te vor învăța noțiunile de bază. Oricine poate încerca să codeze!" }, "familyguide_group_01_title": { "message": "Învățați să codificați cu elfii lui Moș Crăciun" @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Forme de zăpadă" }, + "scene_snowdodgeball": { + "message": "Snow Dodgeball" + }, "scene_snowflake": { "message": "Scrie codul unui fulg de zăpadă" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Se așteaptă mai mulți elfi ai lui Moș Crăciun" }, + "snowdodgeball_instruction_dodge": { + "message": "Dați clic pe un elf pentru a evita bulgării de zăpadă" + }, + "snowdodgeball_instruction_throw": { + "message": "Dați clic pentru a arunca bulgări de zăpadă" + }, + "snowdodgeball_match_lost": { + "message": "Meci pierdut" + }, + "snowdodgeball_match_won": { + "message": "Meci câștigat!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Acuratețe" + }, + "snowdodgeball_stats_hits": { + "message": "Lovituri încasate de echipă" + }, + "snowdodgeball_stats_throws": { + "message": "Bulgări de zăpadă aruncați" + }, "snowflake_choose_background": { "message": "Alege un fundal" }, diff --git a/_messages/ru.json b/_messages/ru.json index d6ce607af..ef277555e 100644 --- a/_messages/ru.json +++ b/_messages/ru.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Строим из снега" }, + "scene_snowdodgeball": { + "message": "Игра в снежки" + }, "scene_snowflake": { "message": "Создай снежинку из кода" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Ждем остальных эльфов Санты." }, + "snowdodgeball_instruction_dodge": { + "message": "Жмите на эльфа, чтобы уворачиваться от снежков." + }, + "snowdodgeball_instruction_throw": { + "message": "Жмите, чтобы бросать снежки." + }, + "snowdodgeball_match_lost": { + "message": "Поражение" + }, + "snowdodgeball_match_won": { + "message": "Победа!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Точность" + }, + "snowdodgeball_stats_hits": { + "message": "Попаданий по эльфу" + }, + "snowdodgeball_stats_throws": { + "message": "Снежков брошено" + }, "snowflake_choose_background": { "message": "Выберите фон" }, diff --git a/_messages/sk.json b/_messages/sk.json index f4f350915..f5e52656b 100644 --- a/_messages/sk.json +++ b/_messages/sk.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Snežné stavenisko" }, + "scene_snowdodgeball": { + "message": "Snežná vybíjaná" + }, "scene_snowflake": { "message": "Kódovanie snehovej vločky" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Čaká sa na ďalších Santových škriatkov" }, + "snowdodgeball_instruction_dodge": { + "message": "Kliknutím na elfa sa vyhýbate snehovým guľám" + }, + "snowdodgeball_instruction_throw": { + "message": "Kliknutím hádžete snehové gule" + }, + "snowdodgeball_match_lost": { + "message": "Zápas ste prehrali" + }, + "snowdodgeball_match_won": { + "message": "Zápas ste vyhrali!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Presnosť" + }, + "snowdodgeball_stats_hits": { + "message": "Prijaté zásahy" + }, + "snowdodgeball_stats_throws": { + "message": "Hodené snehové gule" + }, "snowflake_choose_background": { "message": "Vyberte pozadie" }, diff --git a/_messages/sl.json b/_messages/sl.json index 23b465cf0..9ca2f0085 100644 --- a/_messages/sl.json +++ b/_messages/sl.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Snegovnik" }, + "scene_snowdodgeball": { + "message": "Med dvema ognjema na snegu" + }, "scene_snowflake": { "message": "Kodiranje snežinke" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Čakanje na več Božičkovih palčkov" }, + "snowdodgeball_instruction_dodge": { + "message": "Kliknite škrata, da se izognete snežnim kepam" + }, + "snowdodgeball_instruction_throw": { + "message": "Kliknite, če želite metati snežne kepe" + }, + "snowdodgeball_match_lost": { + "message": "Poraženi ste" + }, + "snowdodgeball_match_won": { + "message": "Zmagali ste!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Natančnost" + }, + "snowdodgeball_stats_hits": { + "message": "Prejeti zadetki" + }, + "snowdodgeball_stats_throws": { + "message": "Vržene kepe" + }, "snowflake_choose_background": { "message": "Izberite ozadje" }, diff --git a/_messages/sr.json b/_messages/sr.json index c9f2024c3..6621454fc 100644 --- a/_messages/sr.json +++ b/_messages/sr.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Снегоград" }, + "scene_snowdodgeball": { + "message": "Грудвање" + }, "scene_snowflake": { "message": "Направи пахуљицу" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Чекамо на још вилењака" }, + "snowdodgeball_instruction_dodge": { + "message": "Кликните на вилењака да бисте избегли грудве" + }, + "snowdodgeball_instruction_throw": { + "message": "Кликните да бисте бацали грудве" + }, + "snowdodgeball_match_lost": { + "message": "Изгубили сте меч" + }, + "snowdodgeball_match_won": { + "message": "Победили сте у мечу." + }, + "snowdodgeball_stats_accuracy": { + "message": "Прецизност" + }, + "snowdodgeball_stats_hits": { + "message": "Колико пута су нас грудве погодиле" + }, + "snowdodgeball_stats_throws": { + "message": "Број бачених грудви" + }, "snowflake_choose_background": { "message": "Изаберите позадину" }, diff --git a/_messages/sv.json b/_messages/sv.json index dce02a955..fcc449b98 100644 --- a/_messages/sv.json +++ b/_messages/sv.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Snölåda" }, + "scene_snowdodgeball": { + "message": "Snödodgeball" + }, "scene_snowflake": { "message": "Koda en snöflinga" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Väntar på fler tomtenissar" }, + "snowdodgeball_instruction_dodge": { + "message": "Klicka på en tomtenisse för att ducka snöbollar" + }, + "snowdodgeball_instruction_throw": { + "message": "Klicka för att kasta snöbollar" + }, + "snowdodgeball_match_lost": { + "message": "Förlorade matchen" + }, + "snowdodgeball_match_won": { + "message": "Vann matchen!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Exakthet" + }, + "snowdodgeball_stats_hits": { + "message": "Gånger du har blivit träffad" + }, + "snowdodgeball_stats_throws": { + "message": "Kastade snöbollar" + }, "snowflake_choose_background": { "message": "Välj en bakgrund" }, diff --git a/_messages/ta.json b/_messages/ta.json index 820f74139..46152b1a7 100644 --- a/_messages/ta.json +++ b/_messages/ta.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Snowbox" }, + "scene_snowdodgeball": { + "message": "ஸ்னோ டாட்ஜ்பால்" + }, "scene_snowflake": { "message": "ஸ்னோஃப்ளேக் கோடிங்" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "சான்டாவின் பிற குட்டித் தேவதைகள் இணைவதற்காகக் காத்திருக்கின்றன" }, + "snowdodgeball_instruction_dodge": { + "message": "பனிப்பந்துகளைத் தவிர்க்க எல்ஃபைக் கிளிக் செய்யவும்" + }, + "snowdodgeball_instruction_throw": { + "message": "பனிப்பந்துகளை வீச கிளிக் செய்யவும்" + }, + "snowdodgeball_match_lost": { + "message": "போட்டியில் தோல்வி" + }, + "snowdodgeball_match_won": { + "message": "போட்டியில் வெற்றி!" + }, + "snowdodgeball_stats_accuracy": { + "message": "துல்லியம்" + }, + "snowdodgeball_stats_hits": { + "message": "ஹிட்களின் எண்ணிக்கை" + }, + "snowdodgeball_stats_throws": { + "message": "வீசப்பட்ட பனிப்பந்துகள்" + }, "snowflake_choose_background": { "message": "பின்னணியைத் தேர்வுசெய்யவும்" }, diff --git a/_messages/th.json b/_messages/th.json index 375c9015b..56f6c3232 100644 --- a/_messages/th.json +++ b/_messages/th.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "สโนว์บ็อก" }, + "scene_snowdodgeball": { + "message": "Snow Dodgeball" + }, "scene_snowflake": { "message": "สร้างเกล็ดหิมะ" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "กำลังรอเอลฟ์ของซานต้าตนอื่น" }, + "snowdodgeball_instruction_dodge": { + "message": "คลิกที่เอลฟ์เพื่อหลบลูกบอลหิมะ" + }, + "snowdodgeball_instruction_throw": { + "message": "คลิกเพื่อปาลูกบอลหิมะ" + }, + "snowdodgeball_match_lost": { + "message": "แพ้การแข่งขัน" + }, + "snowdodgeball_match_won": { + "message": "ชนะการแข่งขัน" + }, + "snowdodgeball_stats_accuracy": { + "message": "ความแม่นยำ" + }, + "snowdodgeball_stats_hits": { + "message": "ถูกปา" + }, + "snowdodgeball_stats_throws": { + "message": "จำนวนครั้งที่ปาลูกบอลหิมะ" + }, "snowflake_choose_background": { "message": "เลือกฉากหลัง" }, diff --git a/_messages/tl.json b/_messages/tl.json index 4176f876a..bc23a6a6d 100644 --- a/_messages/tl.json +++ b/_messages/tl.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Snowbox" }, + "scene_snowdodgeball": { + "message": "Snow Dodgeball" + }, "scene_snowflake": { "message": "Code a Snowflake" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Hinihintay ang iba pa sa mga elf ni Santa" }, + "snowdodgeball_instruction_dodge": { + "message": "Mag-click sa elf para iwasan ang mga snowball" + }, + "snowdodgeball_instruction_throw": { + "message": "I-click para magbato ng mga snowball" + }, + "snowdodgeball_match_lost": { + "message": "Natalo sa laban" + }, + "snowdodgeball_match_won": { + "message": "Nanalo sa laban!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Katumpakan" + }, + "snowdodgeball_stats_hits": { + "message": "Mga Tinanggap na Hit" + }, + "snowdodgeball_stats_throws": { + "message": "Mga Naipukol na Snowball" + }, "snowflake_choose_background": { "message": "Pumili ng backdrop" }, diff --git a/_messages/uk.json b/_messages/uk.json index 4bfc14ef1..091f014de 100644 --- a/_messages/uk.json +++ b/_messages/uk.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Ліпимо зі снігу" }, + "scene_snowdodgeball": { + "message": "Гра в сніжки" + }, "scene_snowflake": { "message": "Створи сніжинку" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Чекаємо на інших ельфів Санти" }, + "snowdodgeball_instruction_dodge": { + "message": "Натискайте ельфа, щоб ухилятися від сніжок" + }, + "snowdodgeball_instruction_throw": { + "message": "Натискайте, щоб кидати сніжки" + }, + "snowdodgeball_match_lost": { + "message": "Ви програли" + }, + "snowdodgeball_match_won": { + "message": "Ви виграли!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Влучність" + }, + "snowdodgeball_stats_hits": { + "message": "Влучань у вас" + }, + "snowdodgeball_stats_throws": { + "message": "Кинутих сніжок" + }, "snowflake_choose_background": { "message": "Виберіть фон" }, diff --git a/_messages/vi.json b/_messages/vi.json index d970dfe90..09a9cdf58 100644 --- a/_messages/vi.json +++ b/_messages/vi.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "Hộp tuyết" }, + "scene_snowdodgeball": { + "message": "Bóng né tuyết" + }, "scene_snowflake": { "message": "Viết mã cho bông tuyết" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "Đang chờ thêm chú lùn của ông già Noel tham gia" }, + "snowdodgeball_instruction_dodge": { + "message": "Nhấp vào yêu tinh để né bóng tuyết" + }, + "snowdodgeball_instruction_throw": { + "message": "Nhấp để ném bóng tuyết" + }, + "snowdodgeball_match_lost": { + "message": "Thua trận" + }, + "snowdodgeball_match_won": { + "message": "Thắng trận!" + }, + "snowdodgeball_stats_accuracy": { + "message": "Độ chính xác" + }, + "snowdodgeball_stats_hits": { + "message": "Số lần bị ném trúng" + }, + "snowdodgeball_stats_throws": { + "message": "Số bóng tuyết đã ném" + }, "snowflake_choose_background": { "message": "Chọn phông nền" }, diff --git a/_messages/zh-CN.json b/_messages/zh-CN.json index e4021e4dd..02a3754f9 100644 --- a/_messages/zh-CN.json +++ b/_messages/zh-CN.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "雪盒" }, + "scene_snowdodgeball": { + "message": "躲雪球" + }, "scene_snowflake": { "message": "使用编码设计雪花" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "正在等待更多圣诞老人的精灵加入" }, + "snowdodgeball_instruction_dodge": { + "message": "点击精灵即可躲避雪球" + }, + "snowdodgeball_instruction_throw": { + "message": "点击即可投掷雪球" + }, + "snowdodgeball_match_lost": { + "message": "比赛输了" + }, + "snowdodgeball_match_won": { + "message": "比赛获胜!" + }, + "snowdodgeball_stats_accuracy": { + "message": "准确率" + }, + "snowdodgeball_stats_hits": { + "message": "被击中的次数" + }, + "snowdodgeball_stats_throws": { + "message": "丢出的雪球数" + }, "snowflake_choose_background": { "message": "选择背景" }, diff --git a/_messages/zh-HK.json b/_messages/zh-HK.json index d357d669f..42b1de330 100644 --- a/_messages/zh-HK.json +++ b/_messages/zh-HK.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "雪箱" }, + "scene_snowdodgeball": { + "message": "雪地閃避球" + }, "scene_snowflake": { "message": "使用程式碼製作雪花" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "正等待聖誕老人的其他小精靈加入" }, + "snowdodgeball_instruction_dodge": { + "message": "按一下小精靈便可避開雪球" + }, + "snowdodgeball_instruction_throw": { + "message": "按一下便可投擲雪球" + }, + "snowdodgeball_match_lost": { + "message": "你輸了" + }, + "snowdodgeball_match_won": { + "message": "你贏了!" + }, + "snowdodgeball_stats_accuracy": { + "message": "準確度" + }, + "snowdodgeball_stats_hits": { + "message": "被擊中的次數" + }, + "snowdodgeball_stats_throws": { + "message": "已投擲的雪球數量" + }, "snowflake_choose_background": { "message": "選擇背景" }, diff --git a/_messages/zh-TW.json b/_messages/zh-TW.json index da6879681..87f7672c8 100644 --- a/_messages/zh-TW.json +++ b/_messages/zh-TW.json @@ -1808,6 +1808,9 @@ "scene_snowbox": { "message": "雪箱" }, + "scene_snowdodgeball": { + "message": "雪球躲避球" + }, "scene_snowflake": { "message": "使用程式碼設計雪花" }, @@ -1973,6 +1976,27 @@ "snowball_waiting": { "message": "等待其他聖誕精靈加入中" }, + "snowdodgeball_instruction_dodge": { + "message": "輕點小精靈來閃躲雪球" + }, + "snowdodgeball_instruction_throw": { + "message": "輕點任一處即可丟雪球" + }, + "snowdodgeball_match_lost": { + "message": "你輸了" + }, + "snowdodgeball_match_won": { + "message": "你贏了!" + }, + "snowdodgeball_stats_accuracy": { + "message": "準確率" + }, + "snowdodgeball_stats_hits": { + "message": "被雪球擊中次數" + }, + "snowdodgeball_stats_throws": { + "message": "丟出雪球數" + }, "snowflake_choose_background": { "message": "選擇背景圖片" }, From 3ba97c85af90f5c4cac7c6d97b8f4dde4b4d68f4 Mon Sep 17 00:00:00 2001 From: Saransh Saini <66969478+saranshsaini@users.noreply.github.com> Date: Mon, 15 Dec 2025 22:17:53 -0800 Subject: [PATCH 02/19] Make snowdodgeball better on different screen sizes (#225) * Basic reactive * instructions reactive * trajectory fix --- static/scenes/snowdodgeball/index.css | 75 +++++++-------------- static/scenes/snowdodgeball/js/constants.js | 7 +- static/scenes/snowdodgeball/js/game.js | 54 ++++++++++++++- 3 files changed, 81 insertions(+), 55 deletions(-) diff --git a/static/scenes/snowdodgeball/index.css b/static/scenes/snowdodgeball/index.css index 94e85aaa2..787e5f463 100644 --- a/static/scenes/snowdodgeball/index.css +++ b/static/scenes/snowdodgeball/index.css @@ -56,9 +56,9 @@ body { .start-screen__title { position: absolute; - top: 80px; + top: clamp(40px, 6vh, 80px); font-family: 'Lobster', sans-serif; - font-size: 80px; + font-size: clamp(40px, 6vw, 80px); font-weight: normal; font-style: normal; color: #2d8659; @@ -75,18 +75,18 @@ body { .instructions-title { font-family: 'Google Sans', sans-serif; - font-size: 42px; + font-size: clamp(28px, 3.2vw, 42px); font-weight: normal; color: #2d8659; - margin-bottom: 30px; + margin-bottom: clamp(15px, 2.2vw, 30px); } .instruction-panels { display: flex; - gap: 20px; + gap: clamp(15px, 1.5vw, 20px); justify-content: center; align-items: flex-start; - margin-bottom: 20px; + margin-bottom: clamp(15px, 1.5vw, 20px); flex-wrap: wrap; max-width: 95vw; } @@ -96,17 +96,17 @@ body { flex-direction: column; align-items: center; background: white; - padding: 20px; - border-radius: 15px; + padding: clamp(12px, 1.5vw, 20px); + border-radius: clamp(10px, 1.2vw, 15px); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2); flex: 1 1 auto; - min-width: 200px; - max-width: 380px; + min-width: min(200px, 40vw); + max-width: min(380px, 28vw); } .instruction-panel p { font-family: 'Google Sans', sans-serif; - font-size: 18px; + font-size: clamp(14px, 1.3vw, 18px); font-weight: normal; color: #333; margin: 10px 0 0 0; @@ -115,47 +115,21 @@ body { .panel-image { width: 100%; - max-width: 350px; + max-width: min(350px, 26vw); height: auto; - border-radius: 10px; + border-radius: clamp(8px, 0.8vw, 10px); } /* Responsive adjustments */ -@media (max-width: 1400px) { - .instruction-panel { - max-width: 300px; - padding: 15px; - } - - .panel-image { - max-width: 270px; - } -} - -@media (max-width: 1000px) { - .instruction-panel { - max-width: 250px; - padding: 12px; - } - - .panel-image { - max-width: 220px; - } - - .instruction-panels { - gap: 15px; - } -} - @media (max-width: 768px) { .instruction-panels { flex-direction: column; - gap: 15px; + gap: clamp(10px, 2vw, 15px); } .instruction-panel { - max-width: 400px; - width: 90%; + max-width: 85vw; + width: 85vw; } .panel-image { @@ -165,15 +139,15 @@ body { .start-button { position: absolute; - bottom: 100px; - padding: 20px 60px; + bottom: clamp(50px, 8vh, 100px); + padding: clamp(15px, 1.5vw, 20px) clamp(40px, 4.5vw, 60px); font-family: 'Google Sans', sans-serif; - font-size: 32px; + font-size: clamp(24px, 2.4vw, 32px); font-weight: bold; color: white; background: #4a9d5f; border: none; - border-radius: 8px; + border-radius: clamp(6px, 0.6vw, 8px); cursor: pointer; box-shadow: 3px 6px 0 #2d6b3f, 8px 9px 0 rgba(0, 0, 0, 0.16); transform: translate(-3px, -6px); @@ -533,12 +507,11 @@ body { } #arena { - position: relative; + position: absolute; width: 1200px; height: 900px; - max-width: 100%; - max-height: 100%; box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); + transform-origin: 0 0; background: radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.8) 1px, transparent 1px), radial-gradient(circle at 60% 70%, rgba(255, 255, 255, 0.6) 1px, transparent 1px), @@ -573,8 +546,8 @@ body { canvas { display: block; - width: 100%; - height: 100%; + width: 1200px; + height: 900px; } /* Elf styling */ diff --git a/static/scenes/snowdodgeball/js/constants.js b/static/scenes/snowdodgeball/js/constants.js index 3429d8c3b..b64ff4268 100644 --- a/static/scenes/snowdodgeball/js/constants.js +++ b/static/scenes/snowdodgeball/js/constants.js @@ -20,7 +20,12 @@ export const Arena = { BORDER_WIDTH: 3, CENTER_LINE_COLOR: 'rgba(0, 0, 0, 0.15)', CENTER_LINE_WIDTH: 2, - OUT_OF_BOUNDS_MARGIN: 50 + OUT_OF_BOUNDS_MARGIN: 50, + // Responsive scaling + PADDING_TOP: 60, + PADDING_TOP_MOBILE: 20, + PADDING_LEFT_PERCENTAGE: 2, + ZOOM_TOUCH_DEVICE: 0.15 }; // ============================================================================= diff --git a/static/scenes/snowdodgeball/js/game.js b/static/scenes/snowdodgeball/js/game.js index 1dbe1ae6d..5ec8c9f2e 100644 --- a/static/scenes/snowdodgeball/js/game.js +++ b/static/scenes/snowdodgeball/js/game.js @@ -9,6 +9,7 @@ import { } from './constants.js'; import { LevelManager } from './level-manager.js'; import { Stats } from './stats.js'; +import isTouchDevice from '../../snowbox/js/utils/isTouchDevice.js'; export class Game { constructor(canvas, api, prepareAnimation) { @@ -58,6 +59,18 @@ export class Game { this.arenaHeight = Arena.HEIGHT; this.arenaX = 0; this.arenaY = 0; + this.ratio = Arena.WIDTH / Arena.HEIGHT; // Aspect ratio 1200/900 = 1.33 + + // Scaling properties + this.scale = 1; + this.paddingTop = Arena.PADDING_TOP; + this.paddingLeft = Arena.PADDING_LEFT_PERCENTAGE; + this.isTouchDevice = isTouchDevice(); + + // Update padding for touch devices + if (this.isTouchDevice) { + this.paddingTop = Arena.PADDING_TOP_MOBILE; + } // Spawn point management this.spawnPoints = []; @@ -74,6 +87,9 @@ export class Game { this.playerGifts = Array.from(document.querySelectorAll('.health-bar--player .gift')); this.opponentGifts = Array.from(document.querySelectorAll('.health-bar--opponent .gift')); + // Get arena DOM element for scaling + this.arenaElement = document.getElementById('arena'); + this.resize(); window.addEventListener('resize', () => this.resize()); @@ -89,7 +105,38 @@ export class Game { resize() { - // Canvas size now matches arena size + // Calculate available space + const maxHeight = window.innerHeight - this.paddingTop * 2; + const maxWidth = window.innerWidth - window.innerWidth * this.paddingLeft / 100; + + // Calculate target dimensions maintaining aspect ratio + const targetedHeight = Math.min( + this.arenaHeight * window.innerWidth / this.arenaWidth, + maxHeight + ); + const targetedWidth = Math.min(targetedHeight * this.ratio, maxWidth); + + // Calculate scale factor + this.scale = targetedWidth / this.arenaWidth; + + // Add extra zoom for touch devices + if (this.isTouchDevice) { + this.scale += Arena.ZOOM_TOUCH_DEVICE; + } + + // Apply CSS transform to scale the entire arena + this.arenaElement.style.position = 'absolute'; + this.arenaElement.style.left = '50%'; + this.arenaElement.style.top = '50%'; + this.arenaElement.style.transformOrigin = '0 0'; + + if (!this.isTouchDevice) { + this.arenaElement.style.transform = `scale(${this.scale.toFixed(3)}) translate(-50%, -50%)`; + } else { + this.arenaElement.style.transform = `scale(${this.scale.toFixed(3)}) translate(-50%, -50%) translateY(${this.paddingTop}px)`; + } + + // Canvas size remains fixed at base dimensions this.canvas.width = this.arenaWidth; this.canvas.height = this.arenaHeight; this.width = this.arenaWidth; @@ -556,8 +603,9 @@ export class Game { const arena = document.getElementById('arena'); const rect = arena.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; + // Adjust click coordinates for difference screen sizes. + const x = (e.clientX - rect.left) / this.scale; + const y = (e.clientY - rect.top) / this.scale; const arenaCenterY = this.arenaHeight / 2; const arenaBounds = { x: 0, y: 0, width: this.arenaWidth, height: this.arenaHeight }; From 2d028397786c3a6c2a6040847d5841ad2b73a9bb Mon Sep 17 00:00:00 2001 From: Saransh Saini <66969478+saranshsaini@users.noreply.github.com> Date: Sat, 20 Dec 2025 15:37:24 -0800 Subject: [PATCH 03/19] Use translations in snowdodgeball (#227) --- static/scenes/snowdodgeball/index.html | 39 +++++++++++++------------- static/scenes/snowdodgeball/js/game.js | 3 +- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/static/scenes/snowdodgeball/index.html b/static/scenes/snowdodgeball/index.html index 7c39988c0..a56e114a3 100644 --- a/static/scenes/snowdodgeball/index.html +++ b/static/scenes/snowdodgeball/index.html @@ -29,71 +29,71 @@
-

Snow Dodgeball

+

-

Instructions

+

- Click to dodge -

Click an elf to dodge snowballs

+ +

- Throw snowball -

Click to throw snowballs

+ +

- +
@@ -176,6 +176,7 @@

Match Lost

diff --git a/static/scenes/boatload/js/boat.js b/static/scenes/boatload/js/boat.js index 07420098e..d32fa264c 100644 --- a/static/scenes/boatload/js/boat.js +++ b/static/scenes/boatload/js/boat.js @@ -17,6 +17,7 @@ goog.provide('Boat'); goog.require('Constants'); +goog.require('Random'); goog.require('app.shared.pools'); /** @@ -94,8 +95,8 @@ Boat.prototype.onFrame = function(delta) { // Bubbles this.nextBubble -= delta; if (this.nextBubble <= 0) { - Bubble.pop(this.game, Math.random() * 10 - 5, this.y + this.height - 6, this.speed); - this.nextBubble = this.bubbleInterval + Math.random() * this.bubbleVariance; + Bubble.pop(this.game, Random.random() * 10 - 5, this.y + this.height - 6, this.speed); + this.nextBubble = this.bubbleInterval + Random.random() * this.bubbleVariance; } }; diff --git a/static/scenes/boatload/js/bubble.js b/static/scenes/boatload/js/bubble.js index 1ceb57f88..3e1308ce4 100644 --- a/static/scenes/boatload/js/bubble.js +++ b/static/scenes/boatload/js/bubble.js @@ -16,6 +16,7 @@ goog.provide('Bubble'); +goog.require('Random'); goog.require('app.shared.pools'); /** @@ -29,7 +30,7 @@ Bubble = function(game) { this.elem = $(''); this.elem[0].bubble = this; - var type = Math.ceil(Math.random() * 6); + var type = Math.ceil(Random.random() * 6); this.elem.addClass('bubble--' + type); this.game.bubblesElem.append(this.elem); }; diff --git a/static/scenes/boatload/js/game.js b/static/scenes/boatload/js/game.js index 854d8492e..4d73e321d 100644 --- a/static/scenes/boatload/js/game.js +++ b/static/scenes/boatload/js/game.js @@ -24,6 +24,7 @@ goog.require('Controls'); goog.require('Iceberg'); goog.require('Player'); goog.require('Present'); +goog.require('Random'); goog.require('app.shared.Coordinator'); goog.require('app.shared.Gameover'); goog.require('app.shared.LevelUp'); @@ -101,6 +102,7 @@ Game.prototype.preloadPools_ = function() { * Start the game */ Game.prototype.start = function() { + this.started = true; this.tutorial.start(); this.restart(); }; @@ -134,6 +136,9 @@ Game.prototype.restart = function() { this.paused = false; this.player.reset(); + // Initialize manual time mode if requested + this.manualTime = !!window.__MANUAL_TIME__; + // Start game this.unfreezeGame(); window.santaApp.fire('sound-trigger', 'bl_game_start'); @@ -143,18 +148,14 @@ Game.prototype.restart = function() { }; /** - * Called each frame while game is running. Calls onFrame on all entities. + * Advances the game state by delta seconds. + * @param {number} delta Seconds since last frame. */ -Game.prototype.onFrame = function() { +Game.prototype.step = function(delta) { if (!this.isPlaying) { return; } - // Calculate delta - var now = +new Date() / 1000; - var delta = now - this.lastFrame; - this.lastFrame = now; - Coordinator.onFrame(delta); // Update static entities @@ -178,9 +179,44 @@ Game.prototype.onFrame = function() { for (var x = 0, deadEntity; (deadEntity = deadEntities[x]) != null; x++) { this.entities.splice(deadEntity - x, 1); } +}; + +/** + * Manually advance game time by given number of seconds. + * @param {number} seconds Number of seconds to advance. + * @param {number=} opt_fps Granularity frames per second (default: 60). + */ +Game.prototype.advanceTime = function(seconds, opt_fps) { + var fps = opt_fps || 60; + var stepDuration = 1 / fps; + var remaining = seconds; + while (remaining > 0 && this.isPlaying) { + var dt = Math.min(stepDuration, remaining); + this.step(dt); + remaining -= dt; + } +}; + +/** + * Called each frame while game is running. Calls onFrame on all entities. + */ +Game.prototype.onFrame = function() { + if (!this.isPlaying) { + return; + } + + if (!this.manualTime) { + // Calculate delta from clock + var now = +new Date() / 1000; + var delta = now - this.lastFrame; + this.lastFrame = now; + this.step(delta); + } // Request next frame - this.requestId = window.requestAnimationFrame(this.onFrame); + if (!this.manualTime) { + this.requestId = window.requestAnimationFrame(this.onFrame); + } }; /** @@ -207,7 +243,7 @@ Game.prototype.updateBoats = function(delta) { if (!this.nextBoatType) { // Find a random type for the boat - var typeNumber = Math.ceil(Math.random() * Constants.BOATS.length); + var typeNumber = Math.ceil(Random.random() * Constants.BOATS.length); this.nextBoatType = Constants.BOATS[typeNumber - 1]; } @@ -232,7 +268,7 @@ Game.prototype.updateBoats = function(delta) { var multiply = Math.pow(Constants.BOAT_SPAWN_MULTIPLY_EACH_LEVEL, this.level); var interval = Constants.BOAT_SPAWN_BASE + Constants.BOAT_SPAWN_INTERVAL * multiply; var variance = Constants.BOAT_SPAWN_VARIANCE * multiply; - this.nextBoat = (interval - variance / 2) + Math.random() * variance; + this.nextBoat = (interval - variance / 2) + Random.random() * variance; // Make sure there is always one boat on the screen if (this.lastBoat) { @@ -251,12 +287,12 @@ Game.prototype.updateIcebergs = function(delta) { if (this.nextIceberg > 0) return; // Get a random type for the iceberg - var typeNumber = Math.ceil(Math.random() * Constants.ICEBERGS.length); + var typeNumber = Math.ceil(Random.random() * Constants.ICEBERGS.length); var type = Constants.ICEBERGS[typeNumber - 1]; // Find random X position var x = Constants.ICEBERG_X + - (Math.random() * Constants.ICEBERG_X_VARIANCE) - + (Random.random() * Constants.ICEBERG_X_VARIANCE) - (Constants.ICEBERG_X_VARIANCE / 2); var timeleft = (this.sceneSize.height - type.height) / type.speed; @@ -279,7 +315,7 @@ Game.prototype.updateIcebergs = function(delta) { var multiply = Math.pow(Constants.ICEBERG_SPAWN_MULTIPLY_EACH_LEVEL, this.level); var interval = Constants.ICEBERG_SPAWN_BASE + Constants.ICEBERG_SPAWN_INTERVAL * multiply; var variance = Constants.ICEBERG_SPAWN_VARIANCE * multiply; - this.nextIceberg = (interval - variance / 2) + Math.random() * variance; + this.nextIceberg = (interval - variance / 2) + Random.random() * variance; }; /** @@ -383,7 +419,9 @@ Game.prototype.unfreezeGame = function() { this.isPlaying = true; this.lastFrame = +new Date() / 1000; - this.requestId = window.requestAnimationFrame(this.onFrame); + if (!this.manualTime) { + this.requestId = window.requestAnimationFrame(this.onFrame); + } } }; diff --git a/static/scenes/boatload/js/present.js b/static/scenes/boatload/js/present.js index 196be172f..23d60dcbe 100644 --- a/static/scenes/boatload/js/present.js +++ b/static/scenes/boatload/js/present.js @@ -17,6 +17,7 @@ goog.provide('Present'); goog.require('Constants'); +goog.require('Random'); goog.require('app.shared.pools'); goog.require('app.shared.utils'); @@ -33,7 +34,7 @@ Present = function(game) { '' + '
'); - var type = Math.ceil(Math.random() * 3); + var type = Math.ceil(Random.random() * 3); this.elem.addClass('present--' + type); this.game.presentsElem.append(this.elem); diff --git a/static/scenes/runner/index.html b/static/scenes/runner/index.html index c01f1e300..25571956b 100644 --- a/static/scenes/runner/index.html +++ b/static/scenes/runner/index.html @@ -193,6 +193,18 @@ const game = new Game(document.getElementById('module-runner')); + if (window.__MANUAL_TIME__) { + game.manualTime = true; + window.__TEST_CONTROL__ = { + advanceTime: (seconds) => { + if (!game.isPlaying && !game.started) { + game.start(); + } + game.advanceTime(seconds); + }, + }; + } + api.addEventListener('pause', (ev) => game.pause()); api.addEventListener('resume', (ev) => game.resume()); api.addEventListener('restart', (ev) => game.restart()); @@ -203,8 +215,10 @@ sound: [], }); - api.ready(async () => { - game.start(); + api.ready(() => { + if (!game.started) { + game.start(); + } }); diff --git a/static/scenes/runner/js/boost.js b/static/scenes/runner/js/boost.js index 6ae85d0c0..5a1bc06cc 100644 --- a/static/scenes/runner/js/boost.js +++ b/static/scenes/runner/js/boost.js @@ -17,6 +17,7 @@ goog.provide('app.Boost'); goog.require('Constants'); +goog.require('Random'); goog.require('app.shared.Coordinator'); goog.require('app.shared.pools'); @@ -58,7 +59,7 @@ app.Boost.prototype.onInit = function(startX, startY) { this.dead = false; this.collected = false; - var type = Constants.BOOSTS[Math.floor(Math.random() * + var type = Constants.BOOSTS[Math.floor(Random.random() * Constants.BOOSTS.length)]; this.elem.addClass(type.css); diff --git a/static/scenes/runner/js/game.js b/static/scenes/runner/js/game.js index d588085c3..5d7b67e70 100644 --- a/static/scenes/runner/js/game.js +++ b/static/scenes/runner/js/game.js @@ -17,6 +17,7 @@ goog.provide('app.Game'); goog.require('Constants'); +goog.require('Random'); goog.require('app.Boost'); goog.require('app.Controls'); goog.require('app.Canvas'); @@ -68,11 +69,11 @@ app.Game = function(context) { this.scale = 1; this.watchSceneSize_(); + this.manualTime = !!window.__MANUAL_TIME__; + // Cache a bound onFrame since we need it each frame. this.onFrame = this.onFrame.bind(this); - // this.updateLevel_ = this.updateLevel_.bind(this); - this.preloadPools_(); }; @@ -104,17 +105,19 @@ app.Game.prototype.preloadPools_ = function() { }; /** - * Starts the game. + * Start the game. * @export */ app.Game.prototype.start = function() { - this.tutorial.start(); this.started = true; this.restart(); + + this.tutorial.start(); }; /** - * Reset and restart the game. + * Restart the game. + * @export */ app.Game.prototype.restart = function() { // Cleanup last game @@ -208,7 +211,9 @@ app.Game.prototype.unfreezeGame = function() { this.isPlaying = true; this.lastFrame = +new Date() / 1000; - this.requestId = window.requestAnimationFrame(this.onFrame); + if (!this.manualTime) { + this.requestId = window.requestAnimationFrame(this.onFrame); + } } }; @@ -225,8 +230,7 @@ app.Game.prototype.gameover = function() { score: this.scoreboard.score, level: 1 /* level */, timePlayed: new Date - this.gameStartTime - }) -; + }); }; /** @@ -254,17 +258,14 @@ app.Game.prototype.dispose = function() { }; /** - * Called each frame. + * Advances the game state by delta seconds. + * @param {number} delta Seconds since last frame. */ -app.Game.prototype.onFrame = function() { +app.Game.prototype.step = function(delta) { if (!this.isPlaying) { return; } - // Calculate delta - var now = +new Date() / 1000, - delta = now - this.lastFrame; - this.lastFrame = now; this.timePassed += delta; var second = Math.floor(this.timePassed); if (this.level == 0 && second >= 28 && !this.updatingLevel) { @@ -291,9 +292,6 @@ app.Game.prototype.onFrame = function() { this.entitiesLayerElem_ .css('transform', 'translate3d(' + -this.distanceTraveled + 'px, 0, 0)'); - var groundWidth = parseInt(this.groundElem_.css('width')); - var groundWidthScaled = groundWidth * this.scale; - // Update entities and track which are dead. var deadEntities = []; for (var i = 0, entity; entity = this.entities[i]; i++) { @@ -308,8 +306,39 @@ app.Game.prototype.onFrame = function() { for (var i = 0, deadEntity; (deadEntity = deadEntities[i]) != null; i++) { this.entities.splice(deadEntity - i, 1); } +}; + +/** + * Advances manual time by duration seconds. + * @param {number} duration + */ +app.Game.prototype.advanceTime = function(duration) { + var maxStepSize = 1; + var remaining = duration; + while (remaining > 0 && this.isPlaying) { + var dt = Math.min(maxStepSize, remaining); + this.step(dt); + remaining -= dt; + } +}; + +/** + * Called each frame. + */ +app.Game.prototype.onFrame = function() { + if (!this.isPlaying) { + return; + } - this.requestId = window.requestAnimationFrame(this.onFrame); + var now = +new Date() / 1000, + delta = now - this.lastFrame; + this.lastFrame = now; + + this.step(delta); + + if (!this.manualTime) { + this.requestId = window.requestAnimationFrame(this.onFrame); + } }; /** @@ -324,16 +353,23 @@ app.Game.prototype.updateLevel_ = function(level) { for (var i=0; i<6; i++) { game.stripesElem_.append(game.stripes_); } - this.stripeTimer = new app.Timer(function() { - game.gameElem_.addClass('changing-level'); - }, 200); - this.levelTimer = new app.Timer(function() { - game.gameElem_.removeClass('changing-level'); + if (this.manualTime) { game.level = level; game.setLevelClass(level); game.speed = Constants.GAME_BASE_SPEED * Constants.GAME_LEVEL_SPEED[level]; game.updatingLevel = false; - }, 2000); + } else { + this.stripeTimer = new app.Timer(function() { + game.gameElem_.addClass('changing-level'); + }, 200); + this.levelTimer = new app.Timer(function() { + game.gameElem_.removeClass('changing-level'); + game.level = level; + game.setLevelClass(level); + game.speed = Constants.GAME_BASE_SPEED * Constants.GAME_LEVEL_SPEED[level]; + game.updatingLevel = false; + }, 2000); + } }; /** @@ -348,7 +384,7 @@ app.Game.prototype.watchSceneSize_ = function() { var updateSize = function() { game.scale = 1; - game.visibleWidth = gameElem.width() * 1.5; + game.visibleWidth = (gameElem.width() || 1000) * 1.5; }; updateSize(); @@ -363,7 +399,7 @@ app.Game.prototype.updateEntities = function() { var pos = this.lastEntityPos + Constants.GAME_ENTITY_SPACING; if (pos < this.finishLinePos - Constants.FINISH_LINE_BUFFER && this.lastEntityPos < this.distanceTraveled + this.visibleWidth) { - var random = Math.random(); + var random = Random.random(); if (random < 0.2) { this.lastEntityPos = this.addObstacle(pos, 0); } else if (random < 0.5) { @@ -372,9 +408,9 @@ app.Game.prototype.updateEntities = function() { this.lastEntityPos = this.addPlatforms(pos); } else if (random < 0.9) { this.lastEntityPos = this.addBoost(pos); - }else { + } else { // Leave some space empty sometimes. - this.lastEntityPos += Math.floor(Math.random() * 1000); + this.lastEntityPos += Math.floor(Random.random() * 1000); } } }; @@ -385,12 +421,12 @@ app.Game.prototype.updateEntities = function() { * @return {number} The position after the last added platform. */ app.Game.prototype.addPlatforms = function(pos) { - var numShort = Math.ceil(Math.random() * 3); - var numTall = Math.floor(Math.random() * 3); + var numShort = Math.ceil(Random.random() * 3); + var numTall = Math.floor(Random.random() * 3); var woodsy = false; var i; - if (Math.random() < 0.25) { + if (Random.random() < 0.25) { woodsy = true; } @@ -420,7 +456,7 @@ app.Game.prototype.addPlatform = function(pos, level, woodsy, var platform = app.Platform.pop(this, pos, level, woodsy); this.entities.push(platform); - var random = Math.random(); + var random = Random.random(); if (random < 0.25) { this.addPresents(pos, platform, (level + 1) * Constants.PLATFORM_HEIGHT + 75); @@ -428,7 +464,7 @@ app.Game.prototype.addPlatform = function(pos, level, woodsy, this.addObstacle(pos + platform.width * .66, level + 1); } - if (Math.random() < 0.25 && !opt_noExtraPresents) { + if (Random.random() < 0.25 && !opt_noExtraPresents) { this.addPresents(pos, platform, 75); } @@ -450,7 +486,7 @@ app.Game.prototype.addObstacle = function(pos, level) { var obstacle = app.Obstacle.pop(this, pos, level); this.entities.push(obstacle); - if (Math.random() < 0.5) { + if (Random.random() < 0.5) { var levelHeight = this.getPresentHeightForLevel(level + 1); this.addPresents(pos, obstacle, obstacle.presentsHeight || levelHeight); } @@ -468,7 +504,7 @@ app.Game.prototype.addObstacle = function(pos, level) { * @return {number} The position after the last added present. */ app.Game.prototype.addPresents = function(pos, opt_entity, opt_height) { - var height = opt_height || Math.floor(Math.random() * 500 + 75); + var height = opt_height || Math.floor(Random.random() * 500 + 75); if (opt_entity) { var presentSpacing = 75; @@ -495,8 +531,8 @@ app.Game.prototype.addPresents = function(pos, opt_entity, opt_height) { this.entities.push(present); } } else { - var numPresents = Math.ceil(Math.random() * 3 + 2); - var numRows = Math.ceil(Math.random() * 4); + var numPresents = Math.ceil(Random.random() * 3 + 2); + var numRows = Math.ceil(Random.random() * 4); var presentSpacing = 150; for (var i = 0; i < numPresents; i++) { @@ -522,7 +558,7 @@ app.Game.prototype.addBoost = function(pos) { if (this.lastBoostPos > pos - Constants.PX_BETWEEN_BOOSTS) { return this.lastEntityPos; } - var boost = app.Boost.pop(this, pos, Math.floor(Math.random() * 500 + 75)); + var boost = app.Boost.pop(this, pos, Math.floor(Random.random() * 500 + 75)); this.entities.push(boost); this.lastBoostPos = pos + boost.width; diff --git a/static/scenes/runner/js/obstacle.js b/static/scenes/runner/js/obstacle.js index c62424487..92dd6f627 100644 --- a/static/scenes/runner/js/obstacle.js +++ b/static/scenes/runner/js/obstacle.js @@ -17,6 +17,7 @@ goog.provide('app.Obstacle'); goog.require('Constants'); +goog.require('Random'); goog.require('app.shared.pools'); /** @@ -51,7 +52,7 @@ app.Obstacle.prototype.onInit = function(startX, level) { var obstacleArray = level > 0 ? Constants.OBSTACLES_PLATFORM : Constants.OBSTACLES_GROUND; var type = obstacleArray[ - Math.floor(Math.random() * obstacleArray.length)]; + Math.floor(Random.random() * obstacleArray.length)]; this.elem.addClass(type.css); this.x = startX; diff --git a/static/scenes/runner/js/platform.js b/static/scenes/runner/js/platform.js index e0037ce12..69f9be1c3 100644 --- a/static/scenes/runner/js/platform.js +++ b/static/scenes/runner/js/platform.js @@ -17,6 +17,7 @@ goog.provide('app.Platform'); goog.require('Constants'); +goog.require('Random'); goog.require('app.shared.pools'); /** @@ -52,7 +53,7 @@ app.Platform.prototype.onInit = function(startX, level, opt_woodsy) { var platformArray = level > 0 ? Constants.PLATFORMS_TALL : Constants.PLATFORMS_SHORT; var index = opt_woodsy ? platformArray.length - 1 : - Math.floor(Math.random() * (platformArray.length - 1)); + Math.floor(Random.random() * (platformArray.length - 1)); var type = platformArray[index]; this.elem.addClass(type.css); diff --git a/static/scenes/runner/js/present.js b/static/scenes/runner/js/present.js index 3ab9eaa96..fa28525de 100644 --- a/static/scenes/runner/js/present.js +++ b/static/scenes/runner/js/present.js @@ -17,6 +17,7 @@ goog.provide('app.Present'); goog.require('Constants'); +goog.require('Random'); goog.require('app.shared.pools'); /** @@ -52,11 +53,11 @@ app.Present.prototype.onInit = function(startX, startY) { this.collected = false; var type; - if (Math.random() < .03) { - type = Constants.TREATS[Math.floor(Math.random() * + if (Random.random() < .03) { + type = Constants.TREATS[Math.floor(Random.random() * Constants.TREATS.length)]; } else { - type = Constants.PRESENTS[Math.floor(Math.random() * + type = Constants.PRESENTS[Math.floor(Random.random() * Constants.PRESENTS.length)]; } this.elem.addClass(type.css); diff --git a/tests/boatload.spec.js b/tests/boatload.spec.js new file mode 100644 index 000000000..c38be8e1c --- /dev/null +++ b/tests/boatload.spec.js @@ -0,0 +1,139 @@ +const { test, expect } = require('@playwright/test'); +const { expectGameOverOverlay } = require('./helpers'); + +function getHostUrl(baseURL) { + if (baseURL && !baseURL.includes('localhost') && !baseURL.includes('127.0.0.1')) { + return `${baseURL}/boatload.html`; + } + return 'http://localhost:8000/boatload.html'; +} + +async function advanceTime(page, seconds) { + const frameLoc = page.frameLocator('iframe[src*="boatload"]'); + await frameLoc.locator('#module-boatload').evaluate((_, secs) => { + window.__TEST_CONTROL__.advanceTime(secs); + }, seconds); +} + +test.describe('Boatload Game', () => { + test.beforeEach(async ({ page, baseURL }) => { + await page.addInitScript(() => { + window.__MANUAL_TIME__ = true; + }); + + const hostUrl = getHostUrl(baseURL); + await page.goto(hostUrl, { waitUntil: 'domcontentloaded' }); + + // Target the actual scene iframe + const frameLoc = page.frameLocator('iframe[src*="boatload"]'); + const gameElem = frameLoc.locator('#module-boatload'); + await expect(gameElem).toBeVisible({ timeout: 30000 }); + + // Ensure __TEST_CONTROL__ is defined or fail immediately + await gameElem.evaluate(async () => { + for (let i = 0; i < 100; i++) { + if (window.__TEST_CONTROL__) break; + await new Promise(r => setTimeout(r, 100)); + } + if (!window.__TEST_CONTROL__) { + throw new Error('window.__TEST_CONTROL__ is not defined'); + } + if (window.Random && window.Random.setSeed) { + window.Random.setSeed('test1234'); + } + }); + }); + + test('should load the hosted game automatically', async ({ page }) => { + const frameLoc = page.frameLocator('iframe[src*="boatload"]'); + const gameElem = frameLoc.locator('#module-boatload'); + await expect(gameElem).toBeVisible({ timeout: 30000 }); + + // Assert game is active (not frozen) + const frozenElem = frameLoc.locator('#module-boatload.frozen'); + await expect(frozenElem).not.toBeAttached(); + }); + + test('should end after 60 seconds', async ({ page }) => { + const frameLoc = page.frameLocator('iframe[src*="boatload"]'); + + await advanceTime(page, 65); + + // Assert game stopped and module element received .frozen class + const frozenElem = frameLoc.locator('#module-boatload.frozen'); + await expect(frozenElem).toBeAttached(); + + // Assert gameover overlay is visible with text, play again, and home buttons + await expectGameOverOverlay(page); + }); + + test('should update score when a present lands in a boat', async ({ page }) => { + const frameLoc = page.frameLocator('iframe[src*="boatload"]'); + + await advanceTime(page, 0.1); + + // Drop present via user interaction + await page.locator('iframe[src*="boatload"]').focus(); + await page.keyboard.press('Space'); + + await advanceTime(page, 3); + + // Verify score sign DOM element is present + const scoreSign = frameLoc.locator('.score-sign'); + await expect(scoreSign).toBeAttached(); + }); + + test('should block presents with an iceberg', async ({ page }) => { + const frameLoc = page.frameLocator('iframe[src*="boatload"]'); + + await advanceTime(page, 0.1); + + // Verify icebergs layer element exists + const icebergsElem = frameLoc.locator('.icebergs'); + await expect(icebergsElem).toBeAttached(); + + // Drop present over icebergs layer + await page.locator('iframe[src*="boatload"]').focus(); + await page.keyboard.press('Space'); + await advanceTime(page, 0.5); + + // Verify presents layer element exists + const presentsElem = frameLoc.locator('.presents'); + await expect(presentsElem).toBeAttached(); + }); + + test('should show score screen at the end of the game', async ({ page }) => { + const frameLoc = page.frameLocator('iframe[src*="boatload"]'); + + await advanceTime(page, 65); + + // Assert game is frozen and score screen / gameover overlay is rendered + const frozenElem = frameLoc.locator('#module-boatload.frozen'); + await expect(frozenElem).toBeAttached(); + + const scoreSign = frameLoc.locator('.score-sign'); + await expect(scoreSign).toBeAttached(); + + await expectGameOverOverlay(page); + }); + + test('should be able to be restarted', async ({ page }) => { + const frameLoc = page.frameLocator('iframe[src*="boatload"]'); + const gameElem = frameLoc.locator('#module-boatload'); + + // End game first + await advanceTime(page, 65); + const frozenElem = frameLoc.locator('#module-boatload.frozen'); + await expect(frozenElem).toBeAttached(); + await expectGameOverOverlay(page); + + // Dispatch restart event + await gameElem.evaluate(() => { + const event = new CustomEvent('restart'); + window.dispatchEvent(event); + }); + + // Assert game module unfreezes (loses .frozen class) + await expect(frozenElem).not.toBeAttached(); + }); +}); diff --git a/tests/helpers.js b/tests/helpers.js new file mode 100644 index 000000000..836c21494 --- /dev/null +++ b/tests/helpers.js @@ -0,0 +1,28 @@ +const { expect } = require('@playwright/test'); + +/** + * Asserts that the santa-overlay game over screen is visible on the page + * with "Game Over" text, Play Again button, and Home button. + * @param {import('@playwright/test').Page} page + */ +async function expectGameOverOverlay(page) { + const overlay = page.locator('santa-overlay'); + await expect(overlay).toBeVisible({ timeout: 10000 }); + + // Assert "Game Over" header text in hero container + const heading = overlay.locator('.hero h1'); + await expect(heading).toBeVisible(); + await expect(heading).toHaveText(/game\s*over/i); + + // Assert Play Again button is visible + const playAgainBtn = overlay.locator('#playagainButton'); + await expect(playAgainBtn).toBeVisible(); + + // Assert Home button is visible + const homeBtn = overlay.locator('santa-button[data-action="home"]'); + await expect(homeBtn).toBeVisible(); +} + +module.exports = { + expectGameOverOverlay, +}; diff --git a/tests/homepage.spec.js b/tests/homepage.spec.js new file mode 100644 index 000000000..ffd5b21c3 --- /dev/null +++ b/tests/homepage.spec.js @@ -0,0 +1,36 @@ +const { test, expect } = require('@playwright/test'); + +test.describe('Home Page', () => { + test('should load the home page successfully', async ({ page }) => { + const response = await page.goto('/'); + + // Ensure HTTP response is successful + expect(response).not.toBeNull(); + expect(response.ok()).toBeTruthy(); + + // Ensure body element is present in DOM + await expect(page.locator('body')).toBeAttached(); + + // Verify main page loader or app element is present + const loader = page.locator('.loader, santa-app, #santa-app').first(); + await expect(loader).toBeAttached(); + }); + + test('should have valid title or document structure', async ({ page }) => { + await page.goto('/'); + + // Validate page has HTML structure + const body = page.locator('body'); + await expect(body).toBeAttached(); + + // Verify title if set or fallback to data-title attribute check + const title = await page.title(); + if (title) { + expect(title.toLowerCase()).toContain('santa'); + } else { + // In local dev server without static release build, data-title placeholder attribute exists + const titleAttr = await page.locator('title').getAttribute('data-title'); + expect(titleAttr).not.toBeNull(); + } + }); +}); diff --git a/tests/runner.spec.js b/tests/runner.spec.js new file mode 100644 index 000000000..29653919f --- /dev/null +++ b/tests/runner.spec.js @@ -0,0 +1,182 @@ +const { test, expect } = require('@playwright/test'); +const { expectGameOverOverlay } = require('./helpers'); + +function getHostUrl(baseURL) { + if (baseURL && !baseURL.includes('localhost') && !baseURL.includes('127.0.0.1')) { + return `${baseURL}/runner.html`; + } + return 'http://localhost:8000/runner.html'; +} + +async function advanceTime(page, seconds) { + const frameLoc = page.frameLocator('iframe[src*="scenes/runner"]'); + await frameLoc.locator('#module-runner').evaluate((_, secs) => { + window.__TEST_CONTROL__.advanceTime(secs); + }, seconds); +} + +test.describe('Runner Game', () => { + test.beforeEach(async ({ page, baseURL }) => { + await page.addInitScript(() => { + window.__MANUAL_TIME__ = true; + }); + + const hostUrl = getHostUrl(baseURL); + await page.goto(hostUrl, { waitUntil: 'domcontentloaded' }); + + // Target active scene iframe + const frameLoc = page.frameLocator('iframe[src*="scenes/runner"]'); + const gameElem = frameLoc.locator('#module-runner'); + await expect(gameElem).toBeAttached({ timeout: 30000 }); + + // Ensure __TEST_CONTROL__ is defined or fail immediately + await gameElem.evaluate(async () => { + for (let i = 0; i < 100; i++) { + if (window.__TEST_CONTROL__) break; + await new Promise(r => setTimeout(r, 100)); + } + if (!window.__TEST_CONTROL__) { + throw new Error('window.__TEST_CONTROL__ is not defined'); + } + if (window.Random && window.Random.setSeed) { + window.Random.setSeed('test1234'); + } + }); + }); + + test('should load the hosted runner game automatically and start tutorial', async ({ page }) => { + const tutorialElem = page.locator('santa-gameloader santa-tutorial, santa-tutorial'); + await expect(tutorialElem).toBeAttached({ timeout: 30000 }); + + const frameLoc = page.frameLocator('iframe[src*="scenes/runner"]'); + const frozenGameElem = frameLoc.locator('#module-runner .game.frozen'); + await expect(frozenGameElem).not.toBeAttached(); + }); + + test('should allow character to double jump but not triple jump', async ({ page }) => { + const frameLoc = page.frameLocator('iframe[src*="scenes/runner"]'); + const playerElem = frameLoc.locator('.reindeer'); + const iframeElem = page.locator('iframe[src*="scenes/runner"]'); + + await advanceTime(page, 0.1); + + const getPlayerTransformY = async () => { + return await playerElem.evaluate((el) => { + const transform = el.style.transform || ''; + const match = transform.match(/translate3d\([^,]+,\s*(-?\d+(?:\.\d+)?)px/); + return match ? parseFloat(match[1]) : 0; + }); + }; + + const initialY = await getPlayerTransformY(); + + await iframeElem.focus(); + await page.keyboard.press('Space'); + await advanceTime(page, 0.05); + const jump1Y = await getPlayerTransformY(); + + await page.keyboard.press('Space'); + await advanceTime(page, 0.05); + const jump2Y = await getPlayerTransformY(); + + await advanceTime(page, 0.2); + const apexY = await getPlayerTransformY(); + + await advanceTime(page, 0.2); + const landingY = await getPlayerTransformY(); + + expect(jump1Y).toBeLessThan(initialY); + expect(jump2Y).toBeLessThan(jump1Y); + expect(apexY).toBeLessThanOrEqual(jump2Y); + expect(landingY).toBeLessThan(apexY); + }); + + test('should increase game speed over time as levels advance', async ({ page }) => { + const frameLoc = page.frameLocator('iframe[src*="scenes/runner"]'); + const entitiesLayer = frameLoc.locator('.entities-layer'); + + const getLayerX = async () => { + return await entitiesLayer.evaluate((el) => { + const transform = el.style.transform || ''; + const match = transform.match(/translate3d\((-?\d+(?:\.\d+)?)px/); + return match ? parseFloat(match[1]) : 0; + }); + }; + + const x0 = await getLayerX(); + await advanceTime(page, 1); + const x1 = await getLayerX(); + const distLevel0 = Math.abs(x1 - x0); + + await advanceTime(page, 30); + + const x2 = await getLayerX(); + await advanceTime(page, 1); + const x3 = await getLayerX(); + const distLevel1 = Math.abs(x3 - x2); + + expect(distLevel1).toBeGreaterThan(distLevel0); + }); + + test('should update score when presents are collected', async ({ page }) => { + const frameLoc = page.frameLocator('iframe[src*="scenes/runner"]'); + await advanceTime(page, 15); + const entitiesLayer = frameLoc.locator('.entities-layer'); + await expect(entitiesLayer).toBeAttached(); + }); + + test('should handle obstacle collision', async ({ page }) => { + const frameLoc = page.frameLocator('iframe[src*="scenes/runner"]'); + await advanceTime(page, 5); + const hitCloud = frameLoc.locator('.hit-cloud'); + await expect(hitCloud).toBeAttached(); + }); + + test('should activate boost powerup when collected', async ({ page }) => { + const frameLoc = page.frameLocator('iframe[src*="scenes/runner"]'); + await advanceTime(page, 10); + const boosts = frameLoc.locator('.boosts'); + await expect(boosts).toBeAttached(); + }); + + test('should activate magnet powerup when collected', async ({ page }) => { + const frameLoc = page.frameLocator('iframe[src*="scenes/runner"]'); + await advanceTime(page, 10); + const magnet = frameLoc.locator('.magnet'); + await expect(magnet).toBeAttached(); + }); + + test('should end game and show gameover screen when timer expires', async ({ page }) => { + const frameLoc = page.frameLocator('iframe[src*="scenes/runner"]'); + + // Runner initial countdown is 120s + await advanceTime(page, 130); + + // Assert game element receives .frozen class on gameover + const frozenGameElem = frameLoc.locator('#module-runner .game.frozen'); + await expect(frozenGameElem).toBeAttached(); + + // Assert gameover overlay is visible with text, play again, and home buttons + await expectGameOverOverlay(page); + }); + + test('should restart game when restart event is triggered', async ({ page }) => { + const frameLoc = page.frameLocator('iframe[src*="scenes/runner"]'); + const gameElem = frameLoc.locator('#module-runner'); + + // End game first (Runner countdown is 120s) + await advanceTime(page, 130); + const frozenGameElem = frameLoc.locator('#module-runner .game.frozen'); + await expect(frozenGameElem).toBeAttached(); + await expectGameOverOverlay(page); + + // Dispatch restart event + await gameElem.evaluate(() => { + const event = new CustomEvent('restart'); + window.dispatchEvent(event); + }); + + // Assert game unfreezes (loses .frozen class) + await expect(frozenGameElem).not.toBeAttached(); + }); +}); From c107f2936f08f61c59eff8cd60dce5344b6c2daf Mon Sep 17 00:00:00 2001 From: Jez Swanson Date: Mon, 31 Aug 2026 07:57:41 -0700 Subject: [PATCH 16/19] Playwright followup (#250) * Follow up to playwright tests * Add isReady synchronization to boatload and runner test controls * Improvements to runner tests. BUT these include some cheating (spawning power ups) * Click play again button on game over overlay in restart tests * Improve tests for collecting powerups --- static/scenes/_shared/js/random.js | 3 + static/scenes/boatload/index.html | 2 + static/scenes/boatload/js/game.js | 23 ++-- static/scenes/runner/index.html | 3 + static/scenes/runner/js/game.js | 14 +-- tests/boatload.spec.js | 17 ++- tests/runner.spec.js | 183 ++++++++++++++++++++++++----- 7 files changed, 183 insertions(+), 62 deletions(-) diff --git a/static/scenes/_shared/js/random.js b/static/scenes/_shared/js/random.js index 28c9699a7..21387b343 100644 --- a/static/scenes/_shared/js/random.js +++ b/static/scenes/_shared/js/random.js @@ -47,3 +47,6 @@ Random.random = function() { } return Random.generator_(); }; + +window['Random'] = Random; + diff --git a/static/scenes/boatload/index.html b/static/scenes/boatload/index.html index ee60fbd0b..cf2309568 100644 --- a/static/scenes/boatload/index.html +++ b/static/scenes/boatload/index.html @@ -100,6 +100,7 @@ } game.advanceTime(seconds); }, + isReady: () => !!window.__SCENE_READY__, }; } @@ -117,6 +118,7 @@ if (!game.started) { game.start(); } + window.__SCENE_READY__ = true; }); diff --git a/static/scenes/boatload/js/game.js b/static/scenes/boatload/js/game.js index 4d73e321d..daa5e59ef 100644 --- a/static/scenes/boatload/js/game.js +++ b/static/scenes/boatload/js/game.js @@ -184,10 +184,9 @@ Game.prototype.step = function(delta) { /** * Manually advance game time by given number of seconds. * @param {number} seconds Number of seconds to advance. - * @param {number=} opt_fps Granularity frames per second (default: 60). */ -Game.prototype.advanceTime = function(seconds, opt_fps) { - var fps = opt_fps || 60; +Game.prototype.advanceTime = function(seconds) { + var fps = 60; var stepDuration = 1 / fps; var remaining = seconds; while (remaining > 0 && this.isPlaying) { @@ -201,22 +200,18 @@ Game.prototype.advanceTime = function(seconds, opt_fps) { * Called each frame while game is running. Calls onFrame on all entities. */ Game.prototype.onFrame = function() { - if (!this.isPlaying) { + if (!this.isPlaying || this.manualTime) { return; } - if (!this.manualTime) { - // Calculate delta from clock - var now = +new Date() / 1000; - var delta = now - this.lastFrame; - this.lastFrame = now; - this.step(delta); - } + // Calculate delta from clock + var now = +new Date() / 1000; + var delta = now - this.lastFrame; + this.lastFrame = now; + this.step(delta); // Request next frame - if (!this.manualTime) { - this.requestId = window.requestAnimationFrame(this.onFrame); - } + this.requestId = window.requestAnimationFrame(this.onFrame); }; /** diff --git a/static/scenes/runner/index.html b/static/scenes/runner/index.html index 25571956b..fe61a2dc7 100644 --- a/static/scenes/runner/index.html +++ b/static/scenes/runner/index.html @@ -196,12 +196,14 @@ if (window.__MANUAL_TIME__) { game.manualTime = true; window.__TEST_CONTROL__ = { + game, advanceTime: (seconds) => { if (!game.isPlaying && !game.started) { game.start(); } game.advanceTime(seconds); }, + isReady: () => !!window.__SCENE_READY__, }; } @@ -219,6 +221,7 @@ if (!game.started) { game.start(); } + window.__SCENE_READY__ = true; }); diff --git a/static/scenes/runner/js/game.js b/static/scenes/runner/js/game.js index 5d7b67e70..3099ca67b 100644 --- a/static/scenes/runner/js/game.js +++ b/static/scenes/runner/js/game.js @@ -178,7 +178,7 @@ app.Game.prototype.pause = function() { * Resume the game. */ app.Game.prototype.resume = function() { - if (this.started) { + if (this.started && this.paused) { this.paused = false; this.unfreezeGame(); } @@ -313,10 +313,11 @@ app.Game.prototype.step = function(delta) { * @param {number} duration */ app.Game.prototype.advanceTime = function(duration) { - var maxStepSize = 1; + var fps = 60; + var stepDuration = 1 / fps; var remaining = duration; while (remaining > 0 && this.isPlaying) { - var dt = Math.min(maxStepSize, remaining); + var dt = Math.min(stepDuration, remaining); this.step(dt); remaining -= dt; } @@ -326,19 +327,16 @@ app.Game.prototype.advanceTime = function(duration) { * Called each frame. */ app.Game.prototype.onFrame = function() { - if (!this.isPlaying) { + if (!this.isPlaying || this.manualTime) { return; } var now = +new Date() / 1000, delta = now - this.lastFrame; this.lastFrame = now; - this.step(delta); - if (!this.manualTime) { - this.requestId = window.requestAnimationFrame(this.onFrame); - } + this.requestId = window.requestAnimationFrame(this.onFrame); }; /** diff --git a/tests/boatload.spec.js b/tests/boatload.spec.js index c38be8e1c..d42dc084a 100644 --- a/tests/boatload.spec.js +++ b/tests/boatload.spec.js @@ -29,18 +29,19 @@ test.describe('Boatload Game', () => { const gameElem = frameLoc.locator('#module-boatload'); await expect(gameElem).toBeVisible({ timeout: 30000 }); - // Ensure __TEST_CONTROL__ is defined or fail immediately + // Ensure __TEST_CONTROL__ is defined and ready or fail immediately await gameElem.evaluate(async () => { for (let i = 0; i < 100; i++) { - if (window.__TEST_CONTROL__) break; + if (window.__TEST_CONTROL__ && window.__TEST_CONTROL__.isReady()) break; await new Promise(r => setTimeout(r, 100)); } if (!window.__TEST_CONTROL__) { throw new Error('window.__TEST_CONTROL__ is not defined'); } - if (window.Random && window.Random.setSeed) { - window.Random.setSeed('test1234'); + if (!window.Random || !window.Random.setSeed) { + throw new Error('window.Random.setSeed is not defined'); } + window.Random.setSeed('test1234'); }); }); @@ -127,11 +128,9 @@ test.describe('Boatload Game', () => { await expect(frozenElem).toBeAttached(); await expectGameOverOverlay(page); - // Dispatch restart event - await gameElem.evaluate(() => { - const event = new CustomEvent('restart'); - window.dispatchEvent(event); - }); + // Click play again on game over overlay + const playAgainBtn = page.locator('santa-overlay #playagainButton'); + await playAgainBtn.click(); // Assert game module unfreezes (loses .frozen class) await expect(frozenElem).not.toBeAttached(); diff --git a/tests/runner.spec.js b/tests/runner.spec.js index 29653919f..c4784b311 100644 --- a/tests/runner.spec.js +++ b/tests/runner.spec.js @@ -29,18 +29,19 @@ test.describe('Runner Game', () => { const gameElem = frameLoc.locator('#module-runner'); await expect(gameElem).toBeAttached({ timeout: 30000 }); - // Ensure __TEST_CONTROL__ is defined or fail immediately + // Ensure __TEST_CONTROL__ is defined and ready or fail immediately await gameElem.evaluate(async () => { - for (let i = 0; i < 100; i++) { - if (window.__TEST_CONTROL__) break; + for (let i = 0; i < 300; i++) { + if (window.__TEST_CONTROL__ && window.__TEST_CONTROL__.isReady()) break; await new Promise(r => setTimeout(r, 100)); } if (!window.__TEST_CONTROL__) { throw new Error('window.__TEST_CONTROL__ is not defined'); } - if (window.Random && window.Random.setSeed) { - window.Random.setSeed('test1234'); + if (!window.Random || !window.Random.setSeed) { + throw new Error('window.Random.setSeed is not defined'); } + window.Random.setSeed('test1234'); }); }); @@ -69,26 +70,47 @@ test.describe('Runner Game', () => { }; const initialY = await getPlayerTransformY(); + expect(initialY).toBe(0); await iframeElem.focus(); + + // 1st Jump: Player jumps off the ground await page.keyboard.press('Space'); - await advanceTime(page, 0.05); + await advanceTime(page, 0.1); const jump1Y = await getPlayerTransformY(); + expect(jump1Y).toBeLessThan(initialY); + // 2nd Jump (Double jump): Player jumps again mid-air and gains height await page.keyboard.press('Space'); - await advanceTime(page, 0.05); + await advanceTime(page, 0.1); const jump2Y = await getPlayerTransformY(); + expect(jump2Y).toBeLessThan(jump1Y); - await advanceTime(page, 0.2); - const apexY = await getPlayerTransformY(); + // Let the player reach apex and begin descending + await advanceTime(page, 0.6); + const midAirFallingY = await getPlayerTransformY(); + expect(midAirFallingY).toBeLessThan(0); // still in mid-air - await advanceTime(page, 0.2); - const landingY = await getPlayerTransformY(); + // 3rd Jump attempt (Triple jump): Press Space a third time while still mid-air + await page.keyboard.press('Space'); + await advanceTime(page, 0.1); + const postTripleJumpY = await getPlayerTransformY(); - expect(jump1Y).toBeLessThan(initialY); - expect(jump2Y).toBeLessThan(jump1Y); - expect(apexY).toBeLessThanOrEqual(jump2Y); - expect(landingY).toBeLessThan(apexY); + // Triple jump is NOT permitted, so the player must continue falling downward (y increases toward 0) + // rather than jumping upwards (which would make y more negative than midAirFallingY) + expect(postTripleJumpY).toBeGreaterThan(midAirFallingY); + expect(postTripleJumpY).toBeLessThan(0); + + // Advance time until the player lands back on the ground + await advanceTime(page, 0.5); + const landedY = await getPlayerTransformY(); + expect(landedY).toBe(0); + + // Once grounded, jump counter resets and jumping is permitted again + await page.keyboard.press('Space'); + await advanceTime(page, 0.1); + const postLandingJumpY = await getPlayerTransformY(); + expect(postLandingJumpY).toBeLessThan(0); }); test('should increase game speed over time as levels advance', async ({ page }) => { @@ -120,30 +142,131 @@ test.describe('Runner Game', () => { test('should update score when presents are collected', async ({ page }) => { const frameLoc = page.frameLocator('iframe[src*="scenes/runner"]'); + const gameElem = frameLoc.locator('#module-runner'); + + // Score is initially 0 and no presents have been collected yet + const initialScore = await gameElem.evaluate(() => { + return window.__TEST_CONTROL__.game.scoreboard.score; + }); + expect(initialScore).toBe(0); + + const collectedPresents = frameLoc.locator('.present--collected'); + await expect(collectedPresents).not.toBeAttached(); + + // Advance time so the player runs into and collects presents along the track await advanceTime(page, 15); - const entitiesLayer = frameLoc.locator('.entities-layer'); - await expect(entitiesLayer).toBeAttached(); + + // Assert that presents have been collected in the scene + await expect(collectedPresents.first()).toBeAttached(); + const count = await collectedPresents.count(); + expect(count).toBeGreaterThan(0); + + // Assert that scoreboard score within runner game updated + const finalScore = await gameElem.evaluate(() => { + return window.__TEST_CONTROL__.game.scoreboard.score; + }); + expect(finalScore).toBeGreaterThan(initialScore); + + // Assert that the score badge on the main page is updated with the new score + const badgeScore = await page.locator('santa-badge').evaluate((el) => el.score); + expect(badgeScore).toBe(finalScore); }); test('should handle obstacle collision', async ({ page }) => { const frameLoc = page.frameLocator('iframe[src*="scenes/runner"]'); - await advanceTime(page, 5); - const hitCloud = frameLoc.locator('.hit-cloud'); - await expect(hitCloud).toBeAttached(); + const gameElem = frameLoc.locator('#module-runner'); + const reindeerElem = frameLoc.locator('.reindeer'); + const hitCloudElem = frameLoc.locator('.hit-cloud'); + + // Initially, reindeer is running normally and hit-cloud is hidden + await expect(reindeerElem).toHaveClass(/reindeer--run/); + await expect(reindeerElem).not.toHaveClass(/reindeer--collision/); + await expect(hitCloudElem).toHaveClass(/hidden/); + + // At t=4s (with seed 'test1234'), the reindeer collides with an obstacle + await advanceTime(page, 4); + + // Assert reindeer enters collision state and hit cloud is activated + await expect(reindeerElem).toHaveClass(/reindeer--collision/); + await expect(hitCloudElem).not.toHaveClass(/hidden/); + await expect(frameLoc.locator('.hit-cloud__inner--active')).toBeAttached(); + + const isCollisionState = await gameElem.evaluate(() => { + // REINDEER_STATE_COLLISION = 3 + return window.__TEST_CONTROL__.game.player.state === 3; + }); + expect(isCollisionState).toBe(true); + + // After collision duration (2s), reindeer recovers and returns to running state + await advanceTime(page, 2.5); + await expect(reindeerElem).toHaveClass(/reindeer--run/); + await expect(reindeerElem).not.toHaveClass(/reindeer--collision/); + await expect(hitCloudElem).toHaveClass(/hidden/); }); test('should activate boost powerup when collected', async ({ page }) => { const frameLoc = page.frameLocator('iframe[src*="scenes/runner"]'); - await advanceTime(page, 10); - const boosts = frameLoc.locator('.boosts'); - await expect(boosts).toBeAttached(); + const gameElem = frameLoc.locator('#module-runner'); + const iframeElem = page.locator('iframe[src*="scenes/runner"]'); + + // Initially no boosts have been collected + const collectedBoost = frameLoc.locator('.boost--collected'); + await expect(collectedBoost).not.toBeAttached(); + + // Advance time towards naturally spawned time boost (at x=30278, y=-244) + await advanceTime(page, 69.0); + + const initialCountdown = await gameElem.evaluate(() => { + return window.__TEST_CONTROL__.game.scoreboard.countdown; + }); + + // Jump using native keyboard input to collect the time boost + await iframeElem.focus(); + await page.keyboard.press('Space'); + await advanceTime(page, 1.5); + + // Assert that the boost is marked collected in the DOM + await expect(collectedBoost.first()).toBeAttached(); + + // Assert that the time boost added 10 seconds to the countdown + const newCountdown = await gameElem.evaluate(() => { + return window.__TEST_CONTROL__.game.scoreboard.countdown; + }); + // With 1.5 seconds of game time elapsed and +10s boost added, newCountdown > initialCountdown + expect(newCountdown).toBeGreaterThan(initialCountdown); }); test('should activate magnet powerup when collected', async ({ page }) => { const frameLoc = page.frameLocator('iframe[src*="scenes/runner"]'); - await advanceTime(page, 10); - const magnet = frameLoc.locator('.magnet'); - await expect(magnet).toBeAttached(); + const gameElem = frameLoc.locator('#module-runner'); + const magnetElem = frameLoc.locator('.magnet'); + const iframeElem = page.locator('iframe[src*="scenes/runner"]'); + + // Initially magnet powerup is not active + await expect(magnetElem).not.toHaveClass(/magnet--active/); + const initialMagnetMode = await gameElem.evaluate(() => window.__TEST_CONTROL__.game.magnetMode); + expect(initialMagnetMode).toBe(false); + + // Advance time towards naturally spawned magnet boost (at x=11778, y=-589) + await advanceTime(page, 30.4); + + // Perform double jump using native keyboard input to collect the high-altitude magnet boost + await iframeElem.focus(); + await page.keyboard.press('Space'); + await advanceTime(page, 0.3); + await page.keyboard.press('Space'); + await advanceTime(page, 0.8); + + // Assert magnet powerup activated in the UI and game state + await expect(magnetElem).toHaveClass(/magnet--active/); + const magnetModeActive = await gameElem.evaluate(() => window.__TEST_CONTROL__.game.magnetMode); + expect(magnetModeActive).toBe(true); + + // Advance time past the magnet duration (20s) and assert it deactivates + await advanceTime(page, 21); + await expect(magnetElem).not.toHaveClass(/magnet--active/); + const magnetModeEnded = await gameElem.evaluate(() => window.__TEST_CONTROL__.game.magnetMode); + expect(magnetModeEnded).toBe(false); }); test('should end game and show gameover screen when timer expires', async ({ page }) => { @@ -170,11 +293,9 @@ test.describe('Runner Game', () => { await expect(frozenGameElem).toBeAttached(); await expectGameOverOverlay(page); - // Dispatch restart event - await gameElem.evaluate(() => { - const event = new CustomEvent('restart'); - window.dispatchEvent(event); - }); + // Click play again on game over overlay + const playAgainBtn = page.locator('santa-overlay #playagainButton'); + await playAgainBtn.click(); // Assert game unfreezes (loses .frozen class) await expect(frozenGameElem).not.toBeAttached(); From 3ac9b8b48a31abf265c7b84e06cc7fbd40b8491c Mon Sep 17 00:00:00 2001 From: Jez Swanson Date: Fri, 28 Aug 2026 14:31:59 -0700 Subject: [PATCH 17/19] Add in-memory caching for compiled closure scenes and enable it for Playwright tests Adds --cache flag to the dev server, which defaults to false, but is enabled for tests. --- build/compile-scene.js | 15 +++++++++++++-- playwright.config.js | 4 ++-- santa-vfs.js | 3 ++- serve.js | 6 ++++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/build/compile-scene.js b/build/compile-scene.js index d395a4d1a..91bdc5214 100644 --- a/build/compile-scene.js +++ b/build/compile-scene.js @@ -93,12 +93,19 @@ function invokeCompiler(compiler) { } +const sceneCache = new Map(); + /** * @param {string} sceneName * @param {boolean=} compile + * @param {boolean=} cache * @return {{code: string, map: !Object}} */ -module.exports = async function compile(sceneName, compile=true) { +module.exports = async function compile(sceneName, compile=true, cache=false) { + const cacheKey = `${sceneName}-${compile}`; + if (cache && sceneCache.has(cacheKey)) { + return sceneCache.get(cacheKey); + } const compilerSrc = [ 'build/transpile/export.js', 'static/scenes/_shared/js', @@ -171,7 +178,11 @@ module.exports = async function compile(sceneName, compile=true) { map.sources.push(`static/scenes/${sceneName}/js`, `static/scenes/_shared/js`); map.sourcesContent.push(null, null); - return {code, map}; + const result = {code, map}; + if (cache) { + sceneCache.set(cacheKey, result); + } + return result; } finally { sourceMapTemp.removeCallback(); } diff --git a/playwright.config.js b/playwright.config.js index 02b86d659..7539f41ef 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -26,8 +26,8 @@ module.exports = defineConfig({ ], webServer: isLocal ? { - command: 'npm start', - url: 'http://localhost:8000', + command: `node ./serve.js --cache`, + url: baseURL, reuseExistingServer: !process.env.CI, timeout: 120 * 1000, } diff --git a/santa-vfs.js b/santa-vfs.js index 70e4e1b4c..772645dde 100644 --- a/santa-vfs.js +++ b/santa-vfs.js @@ -130,6 +130,7 @@ module.exports = (staticScope, options) => { options = Object.assign({ lang: null, compile: true, + cache: false, config: {}, }, options); @@ -193,7 +194,7 @@ module.exports = (staticScope, options) => { throw new Error(`unsupported Closure flags: ${flags}`); } - const {code, map} = await compileScene(sceneName, options.compile); + const {code, map} = await compileScene(sceneName, options.compile, options.cache); return {code, map}; }, }; diff --git a/serve.js b/serve.js index 72d049005..ba5723cb4 100755 --- a/serve.js +++ b/serve.js @@ -63,6 +63,11 @@ const yargs = require('yargs') default: true, describe: 'Compile Closure scenes', }) + .option('cache', { + type: 'boolean', + default: false, + describe: 'Cache compiled Closure scenes in memory', + }) .argv; /** @@ -106,6 +111,7 @@ const config = { async function serve() { const vfs = santaVfs(config.staticScope, { compile: yargs.compile, + cache: yargs.cache, lang: yargs.lang, config, }); From 0dac3dba2cf96a1e158616b5c1f40362ffaeebe4 Mon Sep 17 00:00:00 2001 From: Jez Swanson Date: Fri, 28 Aug 2026 14:32:34 -0700 Subject: [PATCH 18/19] Improve getHostUrl to respect custom baseURL ports --- tests/boatload.spec.js | 5 +---- tests/runner.spec.js | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/boatload.spec.js b/tests/boatload.spec.js index d42dc084a..4c2b19163 100644 --- a/tests/boatload.spec.js +++ b/tests/boatload.spec.js @@ -2,10 +2,7 @@ const { test, expect } = require('@playwright/test'); const { expectGameOverOverlay } = require('./helpers'); function getHostUrl(baseURL) { - if (baseURL && !baseURL.includes('localhost') && !baseURL.includes('127.0.0.1')) { - return `${baseURL}/boatload.html`; - } - return 'http://localhost:8000/boatload.html'; + return `${baseURL || 'http://localhost:8000'}/boatload.html`; } async function advanceTime(page, seconds) { diff --git a/tests/runner.spec.js b/tests/runner.spec.js index c4784b311..f158f0f31 100644 --- a/tests/runner.spec.js +++ b/tests/runner.spec.js @@ -2,10 +2,7 @@ const { test, expect } = require('@playwright/test'); const { expectGameOverOverlay } = require('./helpers'); function getHostUrl(baseURL) { - if (baseURL && !baseURL.includes('localhost') && !baseURL.includes('127.0.0.1')) { - return `${baseURL}/runner.html`; - } - return 'http://localhost:8000/runner.html'; + return `${baseURL || 'http://localhost:8000'}/runner.html`; } async function advanceTime(page, seconds) { From f6209a432f9bdb0edace64be994c48f0261e7b2a Mon Sep 17 00:00:00 2001 From: Jez Swanson Date: Fri, 28 Aug 2026 14:33:31 -0700 Subject: [PATCH 19/19] Configure playwright webServer to port 8005 and fix port coercion in serve.js --- playwright.config.js | 6 ++++-- serve.js | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/playwright.config.js b/playwright.config.js index 7539f41ef..0e80f8f7c 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,6 +1,8 @@ const { defineConfig, devices } = require('@playwright/test'); -const baseURL = process.env.BASE_URL || 'http://localhost:8000'; +const baseURL = process.env.BASE_URL || `http://localhost:${process.env.PORT || 8005}`; +const urlObj = new URL(baseURL); +const port = process.env.PORT || urlObj.port || 8005; const isLocal = baseURL.includes('localhost') || baseURL.includes('127.0.0.1'); module.exports = defineConfig({ @@ -26,7 +28,7 @@ module.exports = defineConfig({ ], webServer: isLocal ? { - command: `node ./serve.js --cache`, + command: `node ./serve.js --port ${port} --cache`, url: baseURL, reuseExistingServer: !process.env.CI, timeout: 120 * 1000, diff --git a/serve.js b/serve.js index ba5723cb4..f06cef617 100755 --- a/serve.js +++ b/serve.js @@ -35,7 +35,10 @@ const yargs = require('yargs') .option('port', { alias: 'p', type: 'number', - default: process.env.PORT || 8000, + default: Number(process.env.PORT) || 8000, + coerce(v) { + return Number(v) || 8000; + }, describe: 'Static port', }) .option('all', {