diff --git a/assets/audio/enclose_moose/being_a_moose_and_being_free.mp3 b/assets/audio/enclose_moose/being_a_moose_and_being_free.mp3 new file mode 100644 index 00000000..471ca8d9 Binary files /dev/null and b/assets/audio/enclose_moose/being_a_moose_and_being_free.mp3 differ diff --git a/assets/audio/enclose_moose/dont_want_to_be_enclosed.mp3 b/assets/audio/enclose_moose/dont_want_to_be_enclosed.mp3 new file mode 100644 index 00000000..403a3052 Binary files /dev/null and b/assets/audio/enclose_moose/dont_want_to_be_enclosed.mp3 differ diff --git a/assets/audio/enclose_moose/escape_this_way.mp3 b/assets/audio/enclose_moose/escape_this_way.mp3 new file mode 100644 index 00000000..63a0afe2 Binary files /dev/null and b/assets/audio/enclose_moose/escape_this_way.mp3 differ diff --git a/assets/audio/enclose_moose/escape_this_way_2.mp3 b/assets/audio/enclose_moose/escape_this_way_2.mp3 new file mode 100644 index 00000000..47256f7a Binary files /dev/null and b/assets/audio/enclose_moose/escape_this_way_2.mp3 differ diff --git a/assets/audio/enclose_moose/escape_this_way_3.mp3 b/assets/audio/enclose_moose/escape_this_way_3.mp3 new file mode 100644 index 00000000..e7782dca Binary files /dev/null and b/assets/audio/enclose_moose/escape_this_way_3.mp3 differ diff --git a/assets/audio/enclose_moose/escape_this_way_4.mp3 b/assets/audio/enclose_moose/escape_this_way_4.mp3 new file mode 100644 index 00000000..95aae083 Binary files /dev/null and b/assets/audio/enclose_moose/escape_this_way_4.mp3 differ diff --git a/assets/audio/enclose_moose/free_because_im_a_moose.mp3 b/assets/audio/enclose_moose/free_because_im_a_moose.mp3 new file mode 100644 index 00000000..e8a1fc99 Binary files /dev/null and b/assets/audio/enclose_moose/free_because_im_a_moose.mp3 differ diff --git a/assets/audio/enclose_moose/free_like_a_moose.mp3 b/assets/audio/enclose_moose/free_like_a_moose.mp3 new file mode 100644 index 00000000..03fc5369 Binary files /dev/null and b/assets/audio/enclose_moose/free_like_a_moose.mp3 differ diff --git a/assets/audio/enclose_moose/goodbye_moo.mp3 b/assets/audio/enclose_moose/goodbye_moo.mp3 new file mode 100644 index 00000000..84806aca Binary files /dev/null and b/assets/audio/enclose_moose/goodbye_moo.mp3 differ diff --git a/assets/audio/enclose_moose/hang_out_with_the_others.mp3 b/assets/audio/enclose_moose/hang_out_with_the_others.mp3 new file mode 100644 index 00000000..012e3c56 Binary files /dev/null and b/assets/audio/enclose_moose/hang_out_with_the_others.mp3 differ diff --git a/assets/audio/enclose_moose/have_been_enclosed.mp3 b/assets/audio/enclose_moose/have_been_enclosed.mp3 new file mode 100644 index 00000000..4f3d76d3 Binary files /dev/null and b/assets/audio/enclose_moose/have_been_enclosed.mp3 differ diff --git a/assets/audio/enclose_moose/im_a_moose.mp3 b/assets/audio/enclose_moose/im_a_moose.mp3 new file mode 100644 index 00000000..7bcca2dd Binary files /dev/null and b/assets/audio/enclose_moose/im_a_moose.mp3 differ diff --git a/assets/audio/enclose_moose/im_a_moose_2.mp3 b/assets/audio/enclose_moose/im_a_moose_2.mp3 new file mode 100644 index 00000000..f69880f5 Binary files /dev/null and b/assets/audio/enclose_moose/im_a_moose_2.mp3 differ diff --git a/assets/audio/enclose_moose/im_a_moose_3.mp3 b/assets/audio/enclose_moose/im_a_moose_3.mp3 new file mode 100644 index 00000000..9ae8e4cd Binary files /dev/null and b/assets/audio/enclose_moose/im_a_moose_3.mp3 differ diff --git a/assets/audio/enclose_moose/im_a_moose_4.mp3 b/assets/audio/enclose_moose/im_a_moose_4.mp3 new file mode 100644 index 00000000..4080839e Binary files /dev/null and b/assets/audio/enclose_moose/im_a_moose_4.mp3 differ diff --git a/assets/audio/enclose_moose/im_a_moose_5.mp3 b/assets/audio/enclose_moose/im_a_moose_5.mp3 new file mode 100644 index 00000000..a8dc7915 Binary files /dev/null and b/assets/audio/enclose_moose/im_a_moose_5.mp3 differ diff --git a/assets/audio/enclose_moose/im_free.mp3 b/assets/audio/enclose_moose/im_free.mp3 new file mode 100644 index 00000000..ffa50aa3 Binary files /dev/null and b/assets/audio/enclose_moose/im_free.mp3 differ diff --git a/assets/audio/enclose_moose/im_free_2.mp3 b/assets/audio/enclose_moose/im_free_2.mp3 new file mode 100644 index 00000000..a74dd7c1 Binary files /dev/null and b/assets/audio/enclose_moose/im_free_2.mp3 differ diff --git a/assets/audio/enclose_moose/just_want_to_be_free.mp3 b/assets/audio/enclose_moose/just_want_to_be_free.mp3 new file mode 100644 index 00000000..92d0be21 Binary files /dev/null and b/assets/audio/enclose_moose/just_want_to_be_free.mp3 differ diff --git a/assets/audio/enclose_moose/let_me_be_free.mp3 b/assets/audio/enclose_moose/let_me_be_free.mp3 new file mode 100644 index 00000000..622e91c3 Binary files /dev/null and b/assets/audio/enclose_moose/let_me_be_free.mp3 differ diff --git a/assets/audio/enclose_moose/let_me_go.mp3 b/assets/audio/enclose_moose/let_me_go.mp3 new file mode 100644 index 00000000..4416fdea Binary files /dev/null and b/assets/audio/enclose_moose/let_me_go.mp3 differ diff --git a/assets/audio/enclose_moose/moo_sad.mp3 b/assets/audio/enclose_moose/moo_sad.mp3 new file mode 100644 index 00000000..82c97c0e Binary files /dev/null and b/assets/audio/enclose_moose/moo_sad.mp3 differ diff --git a/assets/audio/enclose_moose/moo_sad_2.mp3 b/assets/audio/enclose_moose/moo_sad_2.mp3 new file mode 100644 index 00000000..05cba72f Binary files /dev/null and b/assets/audio/enclose_moose/moo_sad_2.mp3 differ diff --git a/assets/audio/enclose_moose/moosing_it.mp3 b/assets/audio/enclose_moose/moosing_it.mp3 new file mode 100644 index 00000000..c72eed34 Binary files /dev/null and b/assets/audio/enclose_moose/moosing_it.mp3 differ diff --git a/assets/audio/enclose_moose/moosing_time.mp3 b/assets/audio/enclose_moose/moosing_time.mp3 new file mode 100644 index 00000000..14dff6a7 Binary files /dev/null and b/assets/audio/enclose_moose/moosing_time.mp3 differ diff --git a/assets/audio/enclose_moose/moost_be_free.mp3 b/assets/audio/enclose_moose/moost_be_free.mp3 new file mode 100644 index 00000000..d935ddb0 Binary files /dev/null and b/assets/audio/enclose_moose/moost_be_free.mp3 differ diff --git a/assets/audio/enclose_moose/moost_this_happen.mp3 b/assets/audio/enclose_moose/moost_this_happen.mp3 new file mode 100644 index 00000000..99199687 Binary files /dev/null and b/assets/audio/enclose_moose/moost_this_happen.mp3 differ diff --git a/assets/audio/enclose_moose/moost_you_do_this.mp3 b/assets/audio/enclose_moose/moost_you_do_this.mp3 new file mode 100644 index 00000000..87d4afe1 Binary files /dev/null and b/assets/audio/enclose_moose/moost_you_do_this.mp3 differ diff --git a/assets/audio/enclose_moose/must_i_be_enclosed.mp3 b/assets/audio/enclose_moose/must_i_be_enclosed.mp3 new file mode 100644 index 00000000..fc1fa79b Binary files /dev/null and b/assets/audio/enclose_moose/must_i_be_enclosed.mp3 differ diff --git a/assets/audio/enclose_moose/never_be_able_to_enclose_me.mp3 b/assets/audio/enclose_moose/never_be_able_to_enclose_me.mp3 new file mode 100644 index 00000000..8b67d16f Binary files /dev/null and b/assets/audio/enclose_moose/never_be_able_to_enclose_me.mp3 differ diff --git a/assets/audio/enclose_moose/never_catch_me.mp3 b/assets/audio/enclose_moose/never_catch_me.mp3 new file mode 100644 index 00000000..f7411cf6 Binary files /dev/null and b/assets/audio/enclose_moose/never_catch_me.mp3 differ diff --git a/assets/audio/enclose_moose/not_supposed_to_be_enclosed.mp3 b/assets/audio/enclose_moose/not_supposed_to_be_enclosed.mp3 new file mode 100644 index 00000000..0fb73e1d Binary files /dev/null and b/assets/audio/enclose_moose/not_supposed_to_be_enclosed.mp3 differ diff --git a/assets/audio/enclose_moose/out_of_here.mp3 b/assets/audio/enclose_moose/out_of_here.mp3 new file mode 100644 index 00000000..bc608518 Binary files /dev/null and b/assets/audio/enclose_moose/out_of_here.mp3 differ diff --git a/assets/audio/enclose_moose/out_of_here_2.mp3 b/assets/audio/enclose_moose/out_of_here_2.mp3 new file mode 100644 index 00000000..43967c74 Binary files /dev/null and b/assets/audio/enclose_moose/out_of_here_2.mp3 differ diff --git a/assets/audio/enclose_moose/sneak_away_like_so.mp3 b/assets/audio/enclose_moose/sneak_away_like_so.mp3 new file mode 100644 index 00000000..e2e4e3fb Binary files /dev/null and b/assets/audio/enclose_moose/sneak_away_like_so.mp3 differ diff --git a/assets/audio/enclose_moose/sneak_away_like_so_2.mp3 b/assets/audio/enclose_moose/sneak_away_like_so_2.mp3 new file mode 100644 index 00000000..bc889fba Binary files /dev/null and b/assets/audio/enclose_moose/sneak_away_like_so_2.mp3 differ diff --git a/assets/audio/enclose_moose/sneak_away_like_so_3.mp3 b/assets/audio/enclose_moose/sneak_away_like_so_3.mp3 new file mode 100644 index 00000000..c0643724 Binary files /dev/null and b/assets/audio/enclose_moose/sneak_away_like_so_3.mp3 differ diff --git a/assets/audio/enclose_moose/sneak_away_like_so_4.mp3 b/assets/audio/enclose_moose/sneak_away_like_so_4.mp3 new file mode 100644 index 00000000..c95bd3a5 Binary files /dev/null and b/assets/audio/enclose_moose/sneak_away_like_so_4.mp3 differ diff --git a/assets/audio/enclose_moose/sneak_away_like_so_5.mp3 b/assets/audio/enclose_moose/sneak_away_like_so_5.mp3 new file mode 100644 index 00000000..78dafb71 Binary files /dev/null and b/assets/audio/enclose_moose/sneak_away_like_so_5.mp3 differ diff --git a/assets/audio/enclose_moose/test.mp3 b/assets/audio/enclose_moose/test.mp3 new file mode 100644 index 00000000..9c5618ee Binary files /dev/null and b/assets/audio/enclose_moose/test.mp3 differ diff --git a/assets/audio/enclose_moose/this_way_that_way.mp3 b/assets/audio/enclose_moose/this_way_that_way.mp3 new file mode 100644 index 00000000..98c0ba7e Binary files /dev/null and b/assets/audio/enclose_moose/this_way_that_way.mp3 differ diff --git a/assets/audio/enclose_moose/this_way_that_way_2.mp3 b/assets/audio/enclose_moose/this_way_that_way_2.mp3 new file mode 100644 index 00000000..895348e6 Binary files /dev/null and b/assets/audio/enclose_moose/this_way_that_way_2.mp3 differ diff --git a/assets/audio/enclose_moose/want_to_be_free.mp3 b/assets/audio/enclose_moose/want_to_be_free.mp3 new file mode 100644 index 00000000..80395648 Binary files /dev/null and b/assets/audio/enclose_moose/want_to_be_free.mp3 differ diff --git a/assets/audio/enclose_moose/you_cant_enclose_me.mp3 b/assets/audio/enclose_moose/you_cant_enclose_me.mp3 new file mode 100644 index 00000000..27881d22 Binary files /dev/null and b/assets/audio/enclose_moose/you_cant_enclose_me.mp3 differ diff --git a/assets/audio/enclose_moose/you_enclosed_me.mp3 b/assets/audio/enclose_moose/you_enclosed_me.mp3 new file mode 100644 index 00000000..60139659 Binary files /dev/null and b/assets/audio/enclose_moose/you_enclosed_me.mp3 differ diff --git a/assets/data/nollning_26/map/map.png b/assets/data/nollning_26/map/map.png index 7940ca49..90435fb8 100644 Binary files a/assets/data/nollning_26/map/map.png and b/assets/data/nollning_26/map/map.png differ diff --git a/assets/data/nollning_26/map/map_info.json b/assets/data/nollning_26/map/map_info.json index 074fdd01..abeadc8f 100644 --- a/assets/data/nollning_26/map/map_info.json +++ b/assets/data/nollning_26/map/map_info.json @@ -1,4 +1,80 @@ [ + { + "title": { + "sv": "Studiecentrum", + "en": "The Study Centre" + }, + "description": { + "sv": "Studiecentrum är byggnaden att minnas. Här finns lunch, fika, skolmaterial och böcker, allting samlat på en enda plats! Behöver man smita undan atmosfären av Mattehuset kan man även plugga här, då omringad av alla möjliga sorters studenter från universitetet. Att ta sig hit tar inte mer än 3 minuter.", + "en": "The Study Centre is the building to remember. You’ll find lunch, coffee breaks, school supplies, and books, everything gathered in one place! If you need to escape the atmosphere of the Math Building, you can also study here, surrounded by all kinds of university students. It takes no more than three minutes to get there." + }, + "descriptionAssets": [], + "boxes": [ + { + "left": 1658.0, + "top": 2430.0, + "width": 176.0, + "height": 130.0 + } + ] + }, + { + "title": { + "sv": "Fysicum", + "en": "Fysicum" + }, + "description": { + "sv": "Välkommen in – vågar du ta första steget in i labyrinten som titulerats Fysicum? Inte ens den mest erfarna och uråldriga 5:e klassaren kan med säkerhet röra sig genom denna byggnadens korridorer med vetskap om var den kommer hamna. Så ta en vän i hand och ge er ut. Skulle ni våga er hit kostar det 5 minuter av er värdefulla tid.", + "en": "Welcome inside — do you dare take the first step into the labyrinth known as Fysicum? Not even the most seasoned, ancient fifth-grader can navigate these corridors with certainty about where they’ll end up. So, take a friend by the hand and venture in. If you dare to visit, it will cost you five minutes of your valuable time." + }, + "descriptionAssets": [], + "boxes": [ + { + "left": 662.0, + "top": 2538.0, + "width": 405.0, + "height": 255.0 + } + ] + }, + { + "title": { + "sv": "Matteannexet", + "en": "The Math Annex" + }, + "description": { + "sv": "Stig på och känn dig som hemma i Matteannexet! En myriad av studenter från alla Lunds universitets hörn samlas här för att bevittna de stora föreläsningssalarna. Befinner du dig i heMH tar det mer eller mindre 34 sekunder att ta dig hit.", + "en": "Step inside and feel at home in the Math Annex! A multitude of students from every corner of Lund University gather here to experience the large lecture halls. If you are in heMH, it takes more or less 34 seconds to get here." + }, + "descriptionAssets": [], + "boxes": [ + { + "left": 1384.0, + "top": 2554.0, + "width": 189.0, + "height": 118.0 + } + ] + }, + { + "title": { + "sv": "Kårhuset", + "en": "The Union Building" + }, + "description": { + "sv": "På kårhuset hör Nollningsutskottet hemma bestående av Nolleamiralen, Nollegeneralen, fyra Øverster. Vi hittar även de största föreläsningssalarna en student kan föreställa sig men än viktigare än det, GASQUE. På bottenplan finner vi platsen där studentens drömmar slår in, platsen som stått ut med både klickande högaklackar, spillda glas och blöta servetter. Sexets hemvist och sångisarnas scen! Ska ni masa er hit från heMH tar det 4 minuter av dyrbar tid.", + "en": "The Union Building is home to the Orientation Committee—comprising the Orientation Admiral, the Orientation General, and four Colonels. Here, you will find the largest lecture halls a student could possibly imagine, but more importantly, GASQUE. On the ground floor lies the place where students' dreams come true, a spot that has endured the clacking of high heels, spilled glasses, and soggy napkins. It is the home base of the party committee and the stage for the toastmasters! If you decide to drag yourselves over here from hoMH, it takes four minutes of precious time." + }, + "descriptionAssets": [], + "boxes": [ + { + "left": 1585.0, + "top": 2222.0, + "width": 278.0, + "height": 107.0 + } + ] + }, { "title": { "sv": "Kemicentrum", @@ -8,7 +84,13 @@ "sv": "Längst bort från trygga hemvisten hittar vi Kemicentrum! I dess korridorer rör sig K-pHøs och W-Phøs. Packa matsäck och kaffe för vägen hit tar hela 9 minuter av vandring.", "en": "Furthest away from our familiar home base, we find the Chemistry Centre! K-pHøs and W-Phøs roam its corridors. Pack a packed lunch and some coffee, because the walk here takes a whole 9 minutes." }, - "descriptionAssets": [], + "descriptionAssets": [ + { + "path": "assets/data/nollning_26/map/pin_media/W_Phos.jpg", + "width": 6000, + "height": 4000 + } + ], "boxes": [ { "left": 1489.0, @@ -28,7 +110,11 @@ "en": "Amidst the glitz and glamour, A-Phøs steps into the spotlight. You can find them here, in the A-building. The journey here takes 8 minutes from the Math Building." }, "descriptionAssets": [ - "assets/data/nollning_26/map/pin_media/Aphos.jpg" + { + "path": "assets/data/nollning_26/map/pin_media/A_Phos.jpg", + "width": 2880, + "height": 2160 + } ], "boxes": [ { @@ -48,7 +134,13 @@ "sv": "Slå upp dörrarna in till V-huset och V-Phøs du skall finna! Expeditionen hit tar hela 6 minuter och ska man ta sig så långt från Mattehuset bör riskerna beaktas.", "en": "Throw open the doors to the V-building, and you shall find V-Phøs! The trek here takes a full six minutes, and venturing that far from the Math Building means the risks should be considered." }, - "descriptionAssets": [], + "descriptionAssets": [ + { + "path": "assets/data/nollning_26/map/pin_media/V_Phos.jpg", + "width": 3000, + "height": 2000 + } + ], "boxes": [ { "left": 1926.0, @@ -67,7 +159,13 @@ "sv": "Välkommen in i värmen av E-huset. I denna byggnad finner vi E-Phøs och D-stab. Färden hit tar er 3 minuter från Mattehuset.", "en": "Welcome into the warmth of the E-building. This building houses E-Phøs and D-stab. The journey here takes three minutes from the Math Building." }, - "descriptionAssets": [], + "descriptionAssets": [ + { + "path": "assets/data/nollning_26/map/pin_media/Staben.jpg", + "width": 8218, + "height": 5414 + } + ], "boxes": [ { "left": 1834.0, @@ -87,7 +185,16 @@ "en": "Hello and welcome to the M-building! In this building, you will find M-Phøs and I-Phøs. If you are coming here from the Math Building, it takes about four minutes to walk." }, "descriptionAssets": [ - "assets/data/nollning_26/map/pin_media/Mphos.jpg" + { + "path": "assets/data/nollning_26/map/pin_media/M_Phos.jpg", + "width": 6000, + "height": 4000 + }, + { + "path": "assets/data/nollning_26/map/pin_media/I_Phos.jpg", + "width": 6000, + "height": 3181 + } ], "boxes": [ { @@ -108,7 +215,11 @@ "en": "So you’ve found your way home—I’m so glad! This is the building where the F-fös hang out, and you can truly feel at home here during your coming years at LTH. Grab a sandwich from Hilbert Café, stamp your coffee card for a cup of freshly brewed liquid gold, and just enjoy—you’re exactly where you’re meant to be! <3" }, "descriptionAssets": [ - "assets/data/nollning_26/map/pin_media/Foset.jpg" + { + "path": "assets/data/nollning_26/map/pin_media/Foset.jpg", + "width": 5184, + "height": 3456 + } ], "boxes": [ { @@ -129,7 +240,11 @@ "en": "We’ve traveled a long way now—all the way to Helsingborg! That’s where we find the ING-phøs. It takes anywhere from 31 to 52 minutes to travel to them." }, "descriptionAssets": [ - "assets/data/nollning_26/map/pin_media/INGphos.jpg" + { + "path": "assets/data/nollning_26/map/pin_media/ING_Phos.jpg", + "width": 6240, + "height": 4160 + } ], "boxes": [ { diff --git a/assets/data/nollning_26/map/pin_media/Aphos.jpg b/assets/data/nollning_26/map/pin_media/A_Phos.jpg similarity index 100% rename from assets/data/nollning_26/map/pin_media/Aphos.jpg rename to assets/data/nollning_26/map/pin_media/A_Phos.jpg diff --git a/assets/data/nollning_26/map/pin_media/ING_Phos.jpg b/assets/data/nollning_26/map/pin_media/ING_Phos.jpg new file mode 100644 index 00000000..4d687af0 Binary files /dev/null and b/assets/data/nollning_26/map/pin_media/ING_Phos.jpg differ diff --git a/assets/data/nollning_26/map/pin_media/INGphos.jpg b/assets/data/nollning_26/map/pin_media/INGphos.jpg deleted file mode 100644 index b578a046..00000000 Binary files a/assets/data/nollning_26/map/pin_media/INGphos.jpg and /dev/null differ diff --git a/assets/data/nollning_26/map/pin_media/I_Phos.jpg b/assets/data/nollning_26/map/pin_media/I_Phos.jpg new file mode 100644 index 00000000..a49c6e10 Binary files /dev/null and b/assets/data/nollning_26/map/pin_media/I_Phos.jpg differ diff --git a/assets/data/nollning_26/map/pin_media/M_Phos.jpg b/assets/data/nollning_26/map/pin_media/M_Phos.jpg new file mode 100644 index 00000000..530d3029 Binary files /dev/null and b/assets/data/nollning_26/map/pin_media/M_Phos.jpg differ diff --git a/assets/data/nollning_26/map/pin_media/Mphos.jpg b/assets/data/nollning_26/map/pin_media/Mphos.jpg deleted file mode 100644 index e599f4e2..00000000 Binary files a/assets/data/nollning_26/map/pin_media/Mphos.jpg and /dev/null differ diff --git a/assets/data/nollning_26/map/pin_media/Staben.jpg b/assets/data/nollning_26/map/pin_media/Staben.jpg new file mode 100644 index 00000000..4a0b5a8b Binary files /dev/null and b/assets/data/nollning_26/map/pin_media/Staben.jpg differ diff --git a/assets/data/nollning_26/map/pin_media/V_Phos.jpg b/assets/data/nollning_26/map/pin_media/V_Phos.jpg new file mode 100644 index 00000000..29e2b9b7 Binary files /dev/null and b/assets/data/nollning_26/map/pin_media/V_Phos.jpg differ diff --git a/assets/data/nollning_26/map/pin_media/W_Phos.jpg b/assets/data/nollning_26/map/pin_media/W_Phos.jpg new file mode 100644 index 00000000..01232d4a Binary files /dev/null and b/assets/data/nollning_26/map/pin_media/W_Phos.jpg differ diff --git a/assets/data/nollning_26/nolleguide/studentlife/guild/portrait_sex_en.png b/assets/data/nollning_26/nolleguide/studentlife/guild/portrait_sex_en.png index d25ad4c7..89259b23 100644 Binary files a/assets/data/nollning_26/nolleguide/studentlife/guild/portrait_sex_en.png and b/assets/data/nollning_26/nolleguide/studentlife/guild/portrait_sex_en.png differ diff --git a/assets/data/nollning_26/nolleguide/studentlife/guild/portrait_sex_sv.png b/assets/data/nollning_26/nolleguide/studentlife/guild/portrait_sex_sv.png index 0c1f869a..17bc6f28 100644 Binary files a/assets/data/nollning_26/nolleguide/studentlife/guild/portrait_sex_sv.png and b/assets/data/nollning_26/nolleguide/studentlife/guild/portrait_sex_sv.png differ diff --git a/assets/data/nollning_26/nolleguide/studentlife/guild/portrait_styrelse3_en.png b/assets/data/nollning_26/nolleguide/studentlife/guild/portrait_styrelse3_en.png index 46163801..cdda5389 100644 Binary files a/assets/data/nollning_26/nolleguide/studentlife/guild/portrait_styrelse3_en.png and b/assets/data/nollning_26/nolleguide/studentlife/guild/portrait_styrelse3_en.png differ diff --git a/assets/data/nollning_26/nolleguide/studentlife/guild/portrait_styrelse3_sv.png b/assets/data/nollning_26/nolleguide/studentlife/guild/portrait_styrelse3_sv.png index 6929e4e9..dabd4baa 100644 Binary files a/assets/data/nollning_26/nolleguide/studentlife/guild/portrait_styrelse3_sv.png and b/assets/data/nollning_26/nolleguide/studentlife/guild/portrait_styrelse3_sv.png differ diff --git a/assets/data/nollning_26/nolleguide/studentlife/guild/title_libu_en.png b/assets/data/nollning_26/nolleguide/studentlife/guild/title_libu_en.png deleted file mode 100644 index 784fc073..00000000 Binary files a/assets/data/nollning_26/nolleguide/studentlife/guild/title_libu_en.png and /dev/null differ diff --git a/assets/data/nollning_26/nolleguide/studentlife/guild/title_libu_sv.png b/assets/data/nollning_26/nolleguide/studentlife/guild/title_libu_sv.png deleted file mode 100644 index f8caac1e..00000000 Binary files a/assets/data/nollning_26/nolleguide/studentlife/guild/title_libu_sv.png and /dev/null differ diff --git a/assets/data/nollning_26/uppdrag/clouds.png b/assets/data/nollning_26/uppdrag/clouds.png new file mode 100644 index 00000000..6ac81f0d Binary files /dev/null and b/assets/data/nollning_26/uppdrag/clouds.png differ diff --git a/assets/data/nollning_26/uppdrag/done_check.png b/assets/data/nollning_26/uppdrag/done_check.png new file mode 100644 index 00000000..d07e4ce8 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/done_check.png differ diff --git a/assets/data/nollning_26/uppdrag/done_pixelart_placeholder.png b/assets/data/nollning_26/uppdrag/done_pixelart_placeholder.png new file mode 100644 index 00000000..08e294eb Binary files /dev/null and b/assets/data/nollning_26/uppdrag/done_pixelart_placeholder.png differ diff --git a/assets/data/nollning_26/uppdrag/ej_vald.png b/assets/data/nollning_26/uppdrag/ej_vald.png new file mode 100644 index 00000000..19caad1d Binary files /dev/null and b/assets/data/nollning_26/uppdrag/ej_vald.png differ diff --git a/assets/data/nollning_26/uppdrag/knapp_blue.png b/assets/data/nollning_26/uppdrag/knapp_blue.png new file mode 100644 index 00000000..0b75f311 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/knapp_blue.png differ diff --git a/assets/data/nollning_26/uppdrag/knapp_done.png b/assets/data/nollning_26/uppdrag/knapp_done.png new file mode 100644 index 00000000..79f78c31 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/knapp_done.png differ diff --git a/assets/data/nollning_26/uppdrag/knapp_locked.png b/assets/data/nollning_26/uppdrag/knapp_locked.png new file mode 100644 index 00000000..c6b74881 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/knapp_locked.png differ diff --git a/assets/data/nollning_26/uppdrag/knapp_orange.png b/assets/data/nollning_26/uppdrag/knapp_orange.png new file mode 100644 index 00000000..e1308a2e Binary files /dev/null and b/assets/data/nollning_26/uppdrag/knapp_orange.png differ diff --git a/assets/data/nollning_26/uppdrag/knapp_purple.png b/assets/data/nollning_26/uppdrag/knapp_purple.png new file mode 100644 index 00000000..c1788d82 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/knapp_purple.png differ diff --git a/assets/data/nollning_26/uppdrag/knapp_red.png b/assets/data/nollning_26/uppdrag/knapp_red.png new file mode 100644 index 00000000..a5267692 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/knapp_red.png differ diff --git a/assets/data/nollning_26/uppdrag/knapp_yellow.png b/assets/data/nollning_26/uppdrag/knapp_yellow.png new file mode 100644 index 00000000..a0e0831e Binary files /dev/null and b/assets/data/nollning_26/uppdrag/knapp_yellow.png differ diff --git "a/assets/data/nollning_26/uppdrag/l\303\244mna_in_uppdrag.png" "b/assets/data/nollning_26/uppdrag/l\303\244mna_in_uppdrag.png" new file mode 100644 index 00000000..3f71a371 Binary files /dev/null and "b/assets/data/nollning_26/uppdrag/l\303\244mna_in_uppdrag.png" differ diff --git a/assets/data/nollning_26/uppdrag/pixelart_barbiedans.png b/assets/data/nollning_26/uppdrag/pixelart_barbiedans.png new file mode 100644 index 00000000..68dc6abd Binary files /dev/null and b/assets/data/nollning_26/uppdrag/pixelart_barbiedans.png differ diff --git a/assets/data/nollning_26/uppdrag/pixelart_fadder.png b/assets/data/nollning_26/uppdrag/pixelart_fadder.png new file mode 100644 index 00000000..479b2a91 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/pixelart_fadder.png differ diff --git a/assets/data/nollning_26/uppdrag/pixelart_kreativ.png b/assets/data/nollning_26/uppdrag/pixelart_kreativ.png new file mode 100644 index 00000000..79e02960 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/pixelart_kreativ.png differ diff --git a/assets/data/nollning_26/uppdrag/pixelart_placeholder.png b/assets/data/nollning_26/uppdrag/pixelart_placeholder.png new file mode 100644 index 00000000..6a7d6c1f Binary files /dev/null and b/assets/data/nollning_26/uppdrag/pixelart_placeholder.png differ diff --git a/assets/data/nollning_26/uppdrag/pixelart_resa.png b/assets/data/nollning_26/uppdrag/pixelart_resa.png new file mode 100644 index 00000000..7a49d079 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/pixelart_resa.png differ diff --git a/assets/data/nollning_26/uppdrag/pixelart_spel.png b/assets/data/nollning_26/uppdrag/pixelart_spel.png new file mode 100644 index 00000000..cc8ec6e1 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/pixelart_spel.png differ diff --git "a/assets/data/nollning_26/uppdrag/pixelart_t\303\244vling.png" "b/assets/data/nollning_26/uppdrag/pixelart_t\303\244vling.png" new file mode 100644 index 00000000..49aff093 Binary files /dev/null and "b/assets/data/nollning_26/uppdrag/pixelart_t\303\244vling.png" differ diff --git "a/assets/data/nollning_26/uppdrag/pixelart_\303\244lg.png" "b/assets/data/nollning_26/uppdrag/pixelart_\303\244lg.png" new file mode 100644 index 00000000..27f58509 Binary files /dev/null and "b/assets/data/nollning_26/uppdrag/pixelart_\303\244lg.png" differ diff --git a/assets/data/nollning_26/uppdrag/placeholder_button.png b/assets/data/nollning_26/uppdrag/placeholder_button.png new file mode 100644 index 00000000..32de05f1 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/placeholder_button.png differ diff --git a/assets/data/nollning_26/uppdrag/pokekort_exkl.jpg b/assets/data/nollning_26/uppdrag/pokekort_exkl.jpg new file mode 100644 index 00000000..e3e58528 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/pokekort_exkl.jpg differ diff --git a/assets/data/nollning_26/uppdrag/rubrik1.png b/assets/data/nollning_26/uppdrag/rubrik1.png new file mode 100644 index 00000000..105c2ff1 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/rubrik1.png differ diff --git a/assets/data/nollning_26/uppdrag/rubrik2.png b/assets/data/nollning_26/uppdrag/rubrik2.png new file mode 100644 index 00000000..b33ec396 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/rubrik2.png differ diff --git a/assets/data/nollning_26/uppdrag/turn_in_quest.png b/assets/data/nollning_26/uppdrag/turn_in_quest.png new file mode 100644 index 00000000..773c8b7f Binary files /dev/null and b/assets/data/nollning_26/uppdrag/turn_in_quest.png differ diff --git a/assets/data/nollning_26/uppdrag/vald.png b/assets/data/nollning_26/uppdrag/vald.png new file mode 100644 index 00000000..44e15e55 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/vald.png differ diff --git a/assets/data/nollning_26/uppdrag/vecka0.png b/assets/data/nollning_26/uppdrag/vecka0.png new file mode 100644 index 00000000..3a483861 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/vecka0.png differ diff --git a/assets/data/nollning_26/uppdrag/vecka1.png b/assets/data/nollning_26/uppdrag/vecka1.png new file mode 100644 index 00000000..467fe98b Binary files /dev/null and b/assets/data/nollning_26/uppdrag/vecka1.png differ diff --git a/assets/data/nollning_26/uppdrag/vecka2.png b/assets/data/nollning_26/uppdrag/vecka2.png new file mode 100644 index 00000000..1cc2c0fd Binary files /dev/null and b/assets/data/nollning_26/uppdrag/vecka2.png differ diff --git a/assets/data/nollning_26/uppdrag/vecka3.png b/assets/data/nollning_26/uppdrag/vecka3.png new file mode 100644 index 00000000..2b040135 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/vecka3.png differ diff --git a/assets/data/nollning_26/uppdrag/vecka4.png b/assets/data/nollning_26/uppdrag/vecka4.png new file mode 100644 index 00000000..ce42d206 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/vecka4.png differ diff --git a/assets/data/nollning_26/uppdrag/week0.png b/assets/data/nollning_26/uppdrag/week0.png new file mode 100644 index 00000000..3919986d Binary files /dev/null and b/assets/data/nollning_26/uppdrag/week0.png differ diff --git a/assets/data/nollning_26/uppdrag/week1.png b/assets/data/nollning_26/uppdrag/week1.png new file mode 100644 index 00000000..367951d7 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/week1.png differ diff --git a/assets/data/nollning_26/uppdrag/week2.png b/assets/data/nollning_26/uppdrag/week2.png new file mode 100644 index 00000000..c907dea5 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/week2.png differ diff --git a/assets/data/nollning_26/uppdrag/week3.png b/assets/data/nollning_26/uppdrag/week3.png new file mode 100644 index 00000000..b4cde62f Binary files /dev/null and b/assets/data/nollning_26/uppdrag/week3.png differ diff --git a/assets/data/nollning_26/uppdrag/week4.png b/assets/data/nollning_26/uppdrag/week4.png new file mode 100644 index 00000000..e479c685 Binary files /dev/null and b/assets/data/nollning_26/uppdrag/week4.png differ diff --git a/assets/fonts/LoRes12OT-Regular.ttf b/assets/fonts/LoRes12OT-Regular.ttf new file mode 100644 index 00000000..bcc2b372 Binary files /dev/null and b/assets/fonts/LoRes12OT-Regular.ttf differ diff --git a/assets/fonts/PressStart2P-Regular.ttf b/assets/fonts/PressStart2P-Regular.ttf new file mode 100644 index 00000000..2442affb Binary files /dev/null and b/assets/fonts/PressStart2P-Regular.ttf differ diff --git a/assets/fonts/Schoolbell-Regular.ttf b/assets/fonts/Schoolbell-Regular.ttf new file mode 100644 index 00000000..dfc507d6 Binary files /dev/null and b/assets/fonts/Schoolbell-Regular.ttf differ diff --git a/assets/fonts/VT323-Regular.ttf b/assets/fonts/VT323-Regular.ttf new file mode 100644 index 00000000..6aec5993 Binary files /dev/null and b/assets/fonts/VT323-Regular.ttf differ diff --git a/assets/img/enclose_moose/apple/end_idle0.png b/assets/img/enclose_moose/apple/end_idle0.png new file mode 100644 index 00000000..0225abfc Binary files /dev/null and b/assets/img/enclose_moose/apple/end_idle0.png differ diff --git a/assets/img/enclose_moose/apple/end_idle1.png b/assets/img/enclose_moose/apple/end_idle1.png new file mode 100644 index 00000000..a84d0ce7 Binary files /dev/null and b/assets/img/enclose_moose/apple/end_idle1.png differ diff --git a/assets/img/enclose_moose/apple/forward_animation0.png b/assets/img/enclose_moose/apple/forward_animation0.png new file mode 100644 index 00000000..0543931a Binary files /dev/null and b/assets/img/enclose_moose/apple/forward_animation0.png differ diff --git a/assets/img/enclose_moose/apple/forward_animation1.png b/assets/img/enclose_moose/apple/forward_animation1.png new file mode 100644 index 00000000..4b3f7adb Binary files /dev/null and b/assets/img/enclose_moose/apple/forward_animation1.png differ diff --git a/assets/img/enclose_moose/apple/forward_animation2.png b/assets/img/enclose_moose/apple/forward_animation2.png new file mode 100644 index 00000000..caa4d56b Binary files /dev/null and b/assets/img/enclose_moose/apple/forward_animation2.png differ diff --git a/assets/img/enclose_moose/apple/forward_animation3.png b/assets/img/enclose_moose/apple/forward_animation3.png new file mode 100644 index 00000000..ed4e0f98 Binary files /dev/null and b/assets/img/enclose_moose/apple/forward_animation3.png differ diff --git a/assets/img/enclose_moose/apple/reverse_animation0.png b/assets/img/enclose_moose/apple/reverse_animation0.png new file mode 100644 index 00000000..179b4fd6 Binary files /dev/null and b/assets/img/enclose_moose/apple/reverse_animation0.png differ diff --git a/assets/img/enclose_moose/apple/reverse_animation1.png b/assets/img/enclose_moose/apple/reverse_animation1.png new file mode 100644 index 00000000..17dc9f07 Binary files /dev/null and b/assets/img/enclose_moose/apple/reverse_animation1.png differ diff --git a/assets/img/enclose_moose/apple/reverse_animation2.png b/assets/img/enclose_moose/apple/reverse_animation2.png new file mode 100644 index 00000000..cf849643 Binary files /dev/null and b/assets/img/enclose_moose/apple/reverse_animation2.png differ diff --git a/assets/img/enclose_moose/apple/start_idle0.png b/assets/img/enclose_moose/apple/start_idle0.png new file mode 100644 index 00000000..d1f8adf8 Binary files /dev/null and b/assets/img/enclose_moose/apple/start_idle0.png differ diff --git a/assets/img/enclose_moose/apple/start_idle1.png b/assets/img/enclose_moose/apple/start_idle1.png new file mode 100644 index 00000000..3a3388dd Binary files /dev/null and b/assets/img/enclose_moose/apple/start_idle1.png differ diff --git a/assets/img/enclose_moose/bees/animation0.png b/assets/img/enclose_moose/bees/animation0.png new file mode 100644 index 00000000..0a46f61e Binary files /dev/null and b/assets/img/enclose_moose/bees/animation0.png differ diff --git a/assets/img/enclose_moose/bees/animation1.png b/assets/img/enclose_moose/bees/animation1.png new file mode 100644 index 00000000..03df1bc9 Binary files /dev/null and b/assets/img/enclose_moose/bees/animation1.png differ diff --git a/assets/img/enclose_moose/bees/animation2.png b/assets/img/enclose_moose/bees/animation2.png new file mode 100644 index 00000000..bf6f4c81 Binary files /dev/null and b/assets/img/enclose_moose/bees/animation2.png differ diff --git a/assets/img/enclose_moose/bees/end_idle0.png b/assets/img/enclose_moose/bees/end_idle0.png new file mode 100644 index 00000000..6dab505e Binary files /dev/null and b/assets/img/enclose_moose/bees/end_idle0.png differ diff --git a/assets/img/enclose_moose/bees/end_idle1.png b/assets/img/enclose_moose/bees/end_idle1.png new file mode 100644 index 00000000..b2b13b2a Binary files /dev/null and b/assets/img/enclose_moose/bees/end_idle1.png differ diff --git a/assets/img/enclose_moose/bees/start_idle0.png b/assets/img/enclose_moose/bees/start_idle0.png new file mode 100644 index 00000000..564884b4 Binary files /dev/null and b/assets/img/enclose_moose/bees/start_idle0.png differ diff --git a/assets/img/enclose_moose/bees/start_idle1.png b/assets/img/enclose_moose/bees/start_idle1.png new file mode 100644 index 00000000..d4da244d Binary files /dev/null and b/assets/img/enclose_moose/bees/start_idle1.png differ diff --git a/assets/img/enclose_moose/cherry/end_idle0.png b/assets/img/enclose_moose/cherry/end_idle0.png new file mode 100644 index 00000000..57d79570 Binary files /dev/null and b/assets/img/enclose_moose/cherry/end_idle0.png differ diff --git a/assets/img/enclose_moose/cherry/end_idle1.png b/assets/img/enclose_moose/cherry/end_idle1.png new file mode 100644 index 00000000..ad795917 Binary files /dev/null and b/assets/img/enclose_moose/cherry/end_idle1.png differ diff --git a/assets/img/enclose_moose/cherry/forward_animation0.png b/assets/img/enclose_moose/cherry/forward_animation0.png new file mode 100644 index 00000000..1ce5e2b1 Binary files /dev/null and b/assets/img/enclose_moose/cherry/forward_animation0.png differ diff --git a/assets/img/enclose_moose/cherry/forward_animation1.png b/assets/img/enclose_moose/cherry/forward_animation1.png new file mode 100644 index 00000000..51b5e170 Binary files /dev/null and b/assets/img/enclose_moose/cherry/forward_animation1.png differ diff --git a/assets/img/enclose_moose/cherry/forward_animation2.png b/assets/img/enclose_moose/cherry/forward_animation2.png new file mode 100644 index 00000000..db912ff1 Binary files /dev/null and b/assets/img/enclose_moose/cherry/forward_animation2.png differ diff --git a/assets/img/enclose_moose/cherry/forward_animation3.png b/assets/img/enclose_moose/cherry/forward_animation3.png new file mode 100644 index 00000000..7df9ba76 Binary files /dev/null and b/assets/img/enclose_moose/cherry/forward_animation3.png differ diff --git a/assets/img/enclose_moose/cherry/reverse_animation0.png b/assets/img/enclose_moose/cherry/reverse_animation0.png new file mode 100644 index 00000000..82d13443 Binary files /dev/null and b/assets/img/enclose_moose/cherry/reverse_animation0.png differ diff --git a/assets/img/enclose_moose/cherry/reverse_animation1.png b/assets/img/enclose_moose/cherry/reverse_animation1.png new file mode 100644 index 00000000..71fc0c76 Binary files /dev/null and b/assets/img/enclose_moose/cherry/reverse_animation1.png differ diff --git a/assets/img/enclose_moose/cherry/reverse_animation2.png b/assets/img/enclose_moose/cherry/reverse_animation2.png new file mode 100644 index 00000000..bed2737f Binary files /dev/null and b/assets/img/enclose_moose/cherry/reverse_animation2.png differ diff --git a/assets/img/enclose_moose/cherry/reverse_animation3.png b/assets/img/enclose_moose/cherry/reverse_animation3.png new file mode 100644 index 00000000..02405af3 Binary files /dev/null and b/assets/img/enclose_moose/cherry/reverse_animation3.png differ diff --git a/assets/img/enclose_moose/cherry/start_idle0.png b/assets/img/enclose_moose/cherry/start_idle0.png new file mode 100644 index 00000000..7d038fd9 Binary files /dev/null and b/assets/img/enclose_moose/cherry/start_idle0.png differ diff --git a/assets/img/enclose_moose/cherry/start_idle1.png b/assets/img/enclose_moose/cherry/start_idle1.png new file mode 100644 index 00000000..61fea071 Binary files /dev/null and b/assets/img/enclose_moose/cherry/start_idle1.png differ diff --git a/assets/img/enclose_moose/grass/0_idle0.png b/assets/img/enclose_moose/grass/0_idle0.png new file mode 100644 index 00000000..52fe3bb3 Binary files /dev/null and b/assets/img/enclose_moose/grass/0_idle0.png differ diff --git a/assets/img/enclose_moose/grass/1_idle0.png b/assets/img/enclose_moose/grass/1_idle0.png new file mode 100644 index 00000000..47a9fb33 Binary files /dev/null and b/assets/img/enclose_moose/grass/1_idle0.png differ diff --git a/assets/img/enclose_moose/grass/1_idle1.png b/assets/img/enclose_moose/grass/1_idle1.png new file mode 100644 index 00000000..691c5c40 Binary files /dev/null and b/assets/img/enclose_moose/grass/1_idle1.png differ diff --git a/assets/img/enclose_moose/grass/2_idle0.png b/assets/img/enclose_moose/grass/2_idle0.png new file mode 100644 index 00000000..23ff392a Binary files /dev/null and b/assets/img/enclose_moose/grass/2_idle0.png differ diff --git a/assets/img/enclose_moose/grass/2_idle1.png b/assets/img/enclose_moose/grass/2_idle1.png new file mode 100644 index 00000000..7a1679f0 Binary files /dev/null and b/assets/img/enclose_moose/grass/2_idle1.png differ diff --git a/assets/img/enclose_moose/logo.png b/assets/img/enclose_moose/logo.png new file mode 100644 index 00000000..25aee5cd Binary files /dev/null and b/assets/img/enclose_moose/logo.png differ diff --git a/assets/img/enclose_moose/moose/end_idle0.png b/assets/img/enclose_moose/moose/end_idle0.png new file mode 100644 index 00000000..04d9b0c9 Binary files /dev/null and b/assets/img/enclose_moose/moose/end_idle0.png differ diff --git a/assets/img/enclose_moose/moose/end_idle1.png b/assets/img/enclose_moose/moose/end_idle1.png new file mode 100644 index 00000000..d61602a6 Binary files /dev/null and b/assets/img/enclose_moose/moose/end_idle1.png differ diff --git a/assets/img/enclose_moose/moose/start_idle0.png b/assets/img/enclose_moose/moose/start_idle0.png new file mode 100644 index 00000000..8ddcb767 Binary files /dev/null and b/assets/img/enclose_moose/moose/start_idle0.png differ diff --git a/assets/img/enclose_moose/moose/start_idle1.png b/assets/img/enclose_moose/moose/start_idle1.png new file mode 100644 index 00000000..e3dc1d91 Binary files /dev/null and b/assets/img/enclose_moose/moose/start_idle1.png differ diff --git a/assets/img/enclose_moose/portal/idle0.png b/assets/img/enclose_moose/portal/idle0.png new file mode 100644 index 00000000..5e30679c Binary files /dev/null and b/assets/img/enclose_moose/portal/idle0.png differ diff --git a/assets/img/enclose_moose/portal/idle1.png b/assets/img/enclose_moose/portal/idle1.png new file mode 100644 index 00000000..e283d9cd Binary files /dev/null and b/assets/img/enclose_moose/portal/idle1.png differ diff --git a/assets/img/enclose_moose/portal/idle2.png b/assets/img/enclose_moose/portal/idle2.png new file mode 100644 index 00000000..6ec71481 Binary files /dev/null and b/assets/img/enclose_moose/portal/idle2.png differ diff --git a/assets/img/enclose_moose/portal/idle3.png b/assets/img/enclose_moose/portal/idle3.png new file mode 100644 index 00000000..b2ffc35b Binary files /dev/null and b/assets/img/enclose_moose/portal/idle3.png differ diff --git a/assets/img/enclose_moose/wall/animation0.png b/assets/img/enclose_moose/wall/animation0.png new file mode 100644 index 00000000..5d717d64 Binary files /dev/null and b/assets/img/enclose_moose/wall/animation0.png differ diff --git a/assets/img/enclose_moose/wall/animation1.png b/assets/img/enclose_moose/wall/animation1.png new file mode 100644 index 00000000..f36f6c70 Binary files /dev/null and b/assets/img/enclose_moose/wall/animation1.png differ diff --git a/assets/img/enclose_moose/wall/animation2.png b/assets/img/enclose_moose/wall/animation2.png new file mode 100644 index 00000000..cc77f712 Binary files /dev/null and b/assets/img/enclose_moose/wall/animation2.png differ diff --git a/assets/img/enclose_moose/wall/animation3.png b/assets/img/enclose_moose/wall/animation3.png new file mode 100644 index 00000000..8165f3c0 Binary files /dev/null and b/assets/img/enclose_moose/wall/animation3.png differ diff --git a/assets/img/enclose_moose/wall/idle0.png b/assets/img/enclose_moose/wall/idle0.png new file mode 100644 index 00000000..9230c96a Binary files /dev/null and b/assets/img/enclose_moose/wall/idle0.png differ diff --git a/assets/img/enclose_moose/wall/idle1.png b/assets/img/enclose_moose/wall/idle1.png new file mode 100644 index 00000000..fe836f31 Binary files /dev/null and b/assets/img/enclose_moose/wall/idle1.png differ diff --git a/assets/img/enclose_moose/water/00000_idle0.png b/assets/img/enclose_moose/water/00000_idle0.png new file mode 100644 index 00000000..f1676d3a Binary files /dev/null and b/assets/img/enclose_moose/water/00000_idle0.png differ diff --git a/assets/img/enclose_moose/water/00001_idle0.png b/assets/img/enclose_moose/water/00001_idle0.png new file mode 100644 index 00000000..4ae457d8 Binary files /dev/null and b/assets/img/enclose_moose/water/00001_idle0.png differ diff --git a/assets/img/enclose_moose/water/00001_idle1.png b/assets/img/enclose_moose/water/00001_idle1.png new file mode 100644 index 00000000..97280bec Binary files /dev/null and b/assets/img/enclose_moose/water/00001_idle1.png differ diff --git a/assets/img/enclose_moose/water/00002_idle0.png b/assets/img/enclose_moose/water/00002_idle0.png new file mode 100644 index 00000000..ef8ec9f5 Binary files /dev/null and b/assets/img/enclose_moose/water/00002_idle0.png differ diff --git a/assets/img/enclose_moose/water/00002_idle1.png b/assets/img/enclose_moose/water/00002_idle1.png new file mode 100644 index 00000000..25d27250 Binary files /dev/null and b/assets/img/enclose_moose/water/00002_idle1.png differ diff --git a/assets/img/enclose_moose/water/0001_idle0.png b/assets/img/enclose_moose/water/0001_idle0.png new file mode 100644 index 00000000..42426886 Binary files /dev/null and b/assets/img/enclose_moose/water/0001_idle0.png differ diff --git a/assets/img/enclose_moose/water/0001_idle1.png b/assets/img/enclose_moose/water/0001_idle1.png new file mode 100644 index 00000000..ce836b40 Binary files /dev/null and b/assets/img/enclose_moose/water/0001_idle1.png differ diff --git a/assets/img/enclose_moose/water/0010_idle0.png b/assets/img/enclose_moose/water/0010_idle0.png new file mode 100644 index 00000000..4e787f89 Binary files /dev/null and b/assets/img/enclose_moose/water/0010_idle0.png differ diff --git a/assets/img/enclose_moose/water/0010_idle1.png b/assets/img/enclose_moose/water/0010_idle1.png new file mode 100644 index 00000000..177917b7 Binary files /dev/null and b/assets/img/enclose_moose/water/0010_idle1.png differ diff --git a/assets/img/enclose_moose/water/0011_idle0.png b/assets/img/enclose_moose/water/0011_idle0.png new file mode 100644 index 00000000..fbf12cb5 Binary files /dev/null and b/assets/img/enclose_moose/water/0011_idle0.png differ diff --git a/assets/img/enclose_moose/water/0011_idle1.png b/assets/img/enclose_moose/water/0011_idle1.png new file mode 100644 index 00000000..3ef8c75d Binary files /dev/null and b/assets/img/enclose_moose/water/0011_idle1.png differ diff --git a/assets/img/enclose_moose/water/0100_idle0.png b/assets/img/enclose_moose/water/0100_idle0.png new file mode 100644 index 00000000..afeac122 Binary files /dev/null and b/assets/img/enclose_moose/water/0100_idle0.png differ diff --git a/assets/img/enclose_moose/water/0100_idle1.png b/assets/img/enclose_moose/water/0100_idle1.png new file mode 100644 index 00000000..976a12f5 Binary files /dev/null and b/assets/img/enclose_moose/water/0100_idle1.png differ diff --git a/assets/img/enclose_moose/water/0101_idle0.png b/assets/img/enclose_moose/water/0101_idle0.png new file mode 100644 index 00000000..acd45276 Binary files /dev/null and b/assets/img/enclose_moose/water/0101_idle0.png differ diff --git a/assets/img/enclose_moose/water/0101_idle1.png b/assets/img/enclose_moose/water/0101_idle1.png new file mode 100644 index 00000000..0c7067a1 Binary files /dev/null and b/assets/img/enclose_moose/water/0101_idle1.png differ diff --git a/assets/img/enclose_moose/water/0110_idle0.png b/assets/img/enclose_moose/water/0110_idle0.png new file mode 100644 index 00000000..2ee6c989 Binary files /dev/null and b/assets/img/enclose_moose/water/0110_idle0.png differ diff --git a/assets/img/enclose_moose/water/0110_idle1.png b/assets/img/enclose_moose/water/0110_idle1.png new file mode 100644 index 00000000..9766b083 Binary files /dev/null and b/assets/img/enclose_moose/water/0110_idle1.png differ diff --git a/assets/img/enclose_moose/water/0111_idle0.png b/assets/img/enclose_moose/water/0111_idle0.png new file mode 100644 index 00000000..83184c2a Binary files /dev/null and b/assets/img/enclose_moose/water/0111_idle0.png differ diff --git a/assets/img/enclose_moose/water/0111_idle1.png b/assets/img/enclose_moose/water/0111_idle1.png new file mode 100644 index 00000000..dbddfda2 Binary files /dev/null and b/assets/img/enclose_moose/water/0111_idle1.png differ diff --git a/assets/img/enclose_moose/water/1000_idle0.png b/assets/img/enclose_moose/water/1000_idle0.png new file mode 100644 index 00000000..4d01b27d Binary files /dev/null and b/assets/img/enclose_moose/water/1000_idle0.png differ diff --git a/assets/img/enclose_moose/water/1000_idle1.png b/assets/img/enclose_moose/water/1000_idle1.png new file mode 100644 index 00000000..561839dd Binary files /dev/null and b/assets/img/enclose_moose/water/1000_idle1.png differ diff --git a/assets/img/enclose_moose/water/1001_idle0.png b/assets/img/enclose_moose/water/1001_idle0.png new file mode 100644 index 00000000..fdb2d7c7 Binary files /dev/null and b/assets/img/enclose_moose/water/1001_idle0.png differ diff --git a/assets/img/enclose_moose/water/1001_idle1.png b/assets/img/enclose_moose/water/1001_idle1.png new file mode 100644 index 00000000..d5675c17 Binary files /dev/null and b/assets/img/enclose_moose/water/1001_idle1.png differ diff --git a/assets/img/enclose_moose/water/1010_idle0.png b/assets/img/enclose_moose/water/1010_idle0.png new file mode 100644 index 00000000..1f58b071 Binary files /dev/null and b/assets/img/enclose_moose/water/1010_idle0.png differ diff --git a/assets/img/enclose_moose/water/1010_idle1.png b/assets/img/enclose_moose/water/1010_idle1.png new file mode 100644 index 00000000..ed31493b Binary files /dev/null and b/assets/img/enclose_moose/water/1010_idle1.png differ diff --git a/assets/img/enclose_moose/water/1011_idle0.png b/assets/img/enclose_moose/water/1011_idle0.png new file mode 100644 index 00000000..b83221f5 Binary files /dev/null and b/assets/img/enclose_moose/water/1011_idle0.png differ diff --git a/assets/img/enclose_moose/water/1011_idle1.png b/assets/img/enclose_moose/water/1011_idle1.png new file mode 100644 index 00000000..9e13296e Binary files /dev/null and b/assets/img/enclose_moose/water/1011_idle1.png differ diff --git a/assets/img/enclose_moose/water/1100_idle0.png b/assets/img/enclose_moose/water/1100_idle0.png new file mode 100644 index 00000000..d4f6502d Binary files /dev/null and b/assets/img/enclose_moose/water/1100_idle0.png differ diff --git a/assets/img/enclose_moose/water/1100_idle1.png b/assets/img/enclose_moose/water/1100_idle1.png new file mode 100644 index 00000000..f897be2c Binary files /dev/null and b/assets/img/enclose_moose/water/1100_idle1.png differ diff --git a/assets/img/enclose_moose/water/1101_idle0.png b/assets/img/enclose_moose/water/1101_idle0.png new file mode 100644 index 00000000..29276016 Binary files /dev/null and b/assets/img/enclose_moose/water/1101_idle0.png differ diff --git a/assets/img/enclose_moose/water/1101_idle1.png b/assets/img/enclose_moose/water/1101_idle1.png new file mode 100644 index 00000000..b862d9a2 Binary files /dev/null and b/assets/img/enclose_moose/water/1101_idle1.png differ diff --git a/assets/img/enclose_moose/water/1110_idle0.png b/assets/img/enclose_moose/water/1110_idle0.png new file mode 100644 index 00000000..d6597c40 Binary files /dev/null and b/assets/img/enclose_moose/water/1110_idle0.png differ diff --git a/assets/img/enclose_moose/water/1110_idle1.png b/assets/img/enclose_moose/water/1110_idle1.png new file mode 100644 index 00000000..23485c64 Binary files /dev/null and b/assets/img/enclose_moose/water/1110_idle1.png differ diff --git a/assets/img/enclose_moose/water/1111_idle0.png b/assets/img/enclose_moose/water/1111_idle0.png new file mode 100644 index 00000000..9d68ee7d Binary files /dev/null and b/assets/img/enclose_moose/water/1111_idle0.png differ diff --git a/assets/img/enclose_moose/water/1111_idle1.png b/assets/img/enclose_moose/water/1111_idle1.png new file mode 100644 index 00000000..a52d1161 Binary files /dev/null and b/assets/img/enclose_moose/water/1111_idle1.png differ diff --git a/assets/img/enclose_moose/water/corner_idle0.png b/assets/img/enclose_moose/water/corner_idle0.png new file mode 100644 index 00000000..37dea990 Binary files /dev/null and b/assets/img/enclose_moose/water/corner_idle0.png differ diff --git a/assets/img/enclose_moose/water/corner_idle1.png b/assets/img/enclose_moose/water/corner_idle1.png new file mode 100644 index 00000000..71cde374 Binary files /dev/null and b/assets/img/enclose_moose/water/corner_idle1.png differ diff --git a/assets/img/enclose_moose/wheat/animation0.png b/assets/img/enclose_moose/wheat/animation0.png new file mode 100644 index 00000000..ec0c2761 Binary files /dev/null and b/assets/img/enclose_moose/wheat/animation0.png differ diff --git a/assets/img/enclose_moose/wheat/animation1.png b/assets/img/enclose_moose/wheat/animation1.png new file mode 100644 index 00000000..f63e4885 Binary files /dev/null and b/assets/img/enclose_moose/wheat/animation1.png differ diff --git a/assets/img/enclose_moose/wheat/animation2.png b/assets/img/enclose_moose/wheat/animation2.png new file mode 100644 index 00000000..efd4049a Binary files /dev/null and b/assets/img/enclose_moose/wheat/animation2.png differ diff --git a/assets/img/enclose_moose/wheat/animation3.png b/assets/img/enclose_moose/wheat/animation3.png new file mode 100644 index 00000000..9dda5f48 Binary files /dev/null and b/assets/img/enclose_moose/wheat/animation3.png differ diff --git a/assets/img/enclose_moose/wheat/idle0.png b/assets/img/enclose_moose/wheat/idle0.png new file mode 100644 index 00000000..e3e0a90e Binary files /dev/null and b/assets/img/enclose_moose/wheat/idle0.png differ diff --git a/assets/img/enclose_moose/wheat/idle1.png b/assets/img/enclose_moose/wheat/idle1.png new file mode 100644 index 00000000..d147e113 Binary files /dev/null and b/assets/img/enclose_moose/wheat/idle1.png differ diff --git a/assets/img/enclose_moose/wheat/idle2.png b/assets/img/enclose_moose/wheat/idle2.png new file mode 100644 index 00000000..e3e0a90e Binary files /dev/null and b/assets/img/enclose_moose/wheat/idle2.png differ diff --git a/assets/img/enclose_moose/wheat/idle3.png b/assets/img/enclose_moose/wheat/idle3.png new file mode 100644 index 00000000..04dea086 Binary files /dev/null and b/assets/img/enclose_moose/wheat/idle3.png differ diff --git a/assets/img/moose_game/logo.png b/assets/img/moose_game/logo.png new file mode 100644 index 00000000..56013eb3 Binary files /dev/null and b/assets/img/moose_game/logo.png differ diff --git a/assets/img/moose_game/space_logo.png b/assets/img/moose_game/space_logo.png new file mode 100644 index 00000000..df1eff54 Binary files /dev/null and b/assets/img/moose_game/space_logo.png differ diff --git a/codemagic.yaml b/codemagic.yaml index c92ceb7c..f33265e4 100644 --- a/codemagic.yaml +++ b/codemagic.yaml @@ -8,6 +8,7 @@ workflows: cocoapods: default vars: MOOSE_GAME_SECRET: $MOOSE_GAME_SECRET + ENCLOSE_MOOSE_SECRET: $ENCLOSE_MOOSE_SECRET triggering: events: - push diff --git a/lib/content_wrapper.dart b/lib/content_wrapper.dart index ae36f911..370eec6e 100644 --- a/lib/content_wrapper.dart +++ b/lib/content_wrapper.dart @@ -100,7 +100,7 @@ class _ContentWrapperState extends State 0: t.news, 1: t.calendar, 2: t.home, //these maybe needs to change - 3: "Game", // t.notifications, + 3: "Games", // t.notifications, 4: t.other, }; // Shows state messages diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index eb36009d..a3bcd8fe 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -244,6 +244,13 @@ "introductionMap": "Map over LTH", "introductionMapClose": "Close", + "questUnlockTitle": "Try to unlock?", + "questUnlockEnterCode": "Enter code", + "questUnlockWrongCode": "Wrong code", + "questLockedTitle": "[ CODE NEEDED ]", + "questLockedDescription": "You need a code to unlock this quest!", + "questUnlockButton": "UNLOCK!", + "cafeShiftSignup": "You have signed up for the shift", "cafeShiftSignupText": "Thank you for wanting to work in the café! Remember to remove your sing-up if you can't make it.", "cafeShiftFail": "Something went wrong!", @@ -560,5 +567,4 @@ "eventNoSignup": "No signup required", "eventHasLottery": "Spots decided by lottery" - } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 1409eef9..c346dbdc 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1424,6 +1424,42 @@ abstract class AppLocalizations { /// **'Stäng'** String get introductionMapClose; + /// No description provided for @questUnlockTitle. + /// + /// In sv, this message translates to: + /// **'Försök låsa upp?'** + String get questUnlockTitle; + + /// No description provided for @questUnlockEnterCode. + /// + /// In sv, this message translates to: + /// **'Ange kod'** + String get questUnlockEnterCode; + + /// No description provided for @questUnlockWrongCode. + /// + /// In sv, this message translates to: + /// **'Fel kod'** + String get questUnlockWrongCode; + + /// No description provided for @questLockedTitle. + /// + /// In sv, this message translates to: + /// **'[ KOD KRÄVS ]'** + String get questLockedTitle; + + /// No description provided for @questLockedDescription. + /// + /// In sv, this message translates to: + /// **'Du behöver en kod för att låsa upp detta uppdrag!'** + String get questLockedDescription; + + /// No description provided for @questUnlockButton. + /// + /// In sv, this message translates to: + /// **'LÅS UPP!'** + String get questUnlockButton; + /// No description provided for @cafeShiftSignup. /// /// In sv, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 374ca121..54b14ccd 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -702,6 +702,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get introductionMapClose => 'Close'; + @override + String get questUnlockTitle => 'Try to unlock?'; + + @override + String get questUnlockEnterCode => 'Enter code'; + + @override + String get questUnlockWrongCode => 'Wrong code'; + + @override + String get questLockedTitle => '[ CODE NEEDED ]'; + + @override + String get questLockedDescription => 'You need a code to unlock this quest!'; + + @override + String get questUnlockButton => 'UNLOCK!'; + @override String get cafeShiftSignup => 'You have signed up for the shift'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 1f85faf0..41fc64a0 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -699,6 +699,25 @@ class AppLocalizationsSv extends AppLocalizations { @override String get introductionMapClose => 'Stäng'; + @override + String get questUnlockTitle => 'Försök låsa upp?'; + + @override + String get questUnlockEnterCode => 'Ange kod'; + + @override + String get questUnlockWrongCode => 'Fel kod'; + + @override + String get questLockedTitle => '[ KOD KRÄVS ]'; + + @override + String get questLockedDescription => + 'Du behöver en kod för att låsa upp detta uppdrag!'; + + @override + String get questUnlockButton => 'LÅS UPP!'; + @override String get cafeShiftSignup => 'Du är nu uppskriven på passet!'; diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 540b735a..df351f45 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -244,6 +244,13 @@ "introductionMap": "Karta över LTH", "introductionMapClose": "Stäng", + "questUnlockTitle": "Försök låsa upp?", + "questUnlockEnterCode": "Ange kod", + "questUnlockWrongCode": "Fel kod", + "questLockedTitle": "[ KOD KRÄVS ]", + "questLockedDescription": "Du behöver en kod för att låsa upp detta uppdrag!", + "questUnlockButton": "LÅS UPP!", + "cafeShiftSignup": "Du är nu uppskriven på passet!", "cafeShiftSignupText": "Tack för att du vill jobba i caféet! Kom ihåg att avanmäla dig om du får förhinder.", "cafeShiftFail": "Något gick fel!", diff --git a/lib/main.dart b/lib/main.dart index ba2dd65c..4e70fb05 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,7 +5,7 @@ import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:fsek_mobile/screens/home/nollningHome26.dart'; -import 'package:fsek_mobile/screens/moose_game/moose_game.dart'; +import 'package:fsek_mobile/screens/games/game_chooser_page.dart'; import 'package:fsek_mobile/screens/news/news.dart'; import 'package:fsek_mobile/screens/calendar/calendar.dart'; import 'package:fsek_mobile/screens/other/other.dart'; @@ -41,7 +41,7 @@ void main() async { // During introduction, modify NollningHomePage() and comment the normal HomePage() Destination(2, Icons.home, NollningHomePage()), // Destination(2, Icons.home, HomePage()), - Destination(3, CustomIcons.moose, MooseGamePage()), + Destination(3, CustomIcons.moose, GameChooserPage()), Destination(4, Icons.list, OtherContent()), ]; diff --git a/lib/screens/games/enclose_moose/asset_handler.dart b/lib/screens/games/enclose_moose/asset_handler.dart new file mode 100644 index 00000000..c0fe00c1 --- /dev/null +++ b/lib/screens/games/enclose_moose/asset_handler.dart @@ -0,0 +1,265 @@ +import "dart:math"; +import "enclose_grid_tile.dart"; + +class AssetHandler { + static const baseImgPath = "assets/img/enclose_moose/"; + static const baseAudioPath = "audio/enclose_moose/"; // why should this not have "assets/" in front? Because it is used with assetSource later maybe? + + static final animationWheatFrames = List.generate(4, (frameIndex) => "${baseImgPath}wheat/animation${frameIndex}.png"); + static final idleWheatFrames = List.generate(4, (frameIndex) => "${baseImgPath}wheat/idle${frameIndex}.png"); + static final cornerIdleWaterFrames = List.generate(2, (frameIndex) => "${baseImgPath}water/corner_idle${frameIndex}.png"); // Could implement a getOverlayFrames but currently it is only used for this anyway + + static List getAllFrames() { + List frames = []; + frames.addAll(animationWheatFrames); + frames.addAll(idleWheatFrames); + frames.addAll(cornerIdleWaterFrames); + + for (final type in EncloseGridCellType.values) { + frames.addAll(getAnimationFrames(type, returnAll: true) ?? []); + frames.addAll(getIdleFrames(type, returnAll: true) ?? []); + } + + return frames; + } + + static List? getAnimationFrames(EncloseGridCellType type, {returnAll = false, dynamic extra}) { + switch (type) { + case EncloseGridCellType.grass: + return null; + + case EncloseGridCellType.water: + return null; + + case EncloseGridCellType.wall: + return List.generate(4, (frameIndex) => "${baseImgPath}wall/animation${frameIndex}.png"); + + case EncloseGridCellType.portal: + return null; + + case EncloseGridCellType.cherry: + final List cherryFrames = []; + final isForward = (extra as bool?) ?? true; + if (isForward || returnAll) { + cherryFrames.addAll(List.generate(4, (frameIndex) => "${baseImgPath}cherry/forward_animation${frameIndex}.png")); + } + if (!isForward || returnAll) { + cherryFrames.addAll(List.generate(3, (frameIndex) => "${baseImgPath}cherry/reverse_animation${frameIndex}.png").reversed.toList()); + } + + return cherryFrames; + + case EncloseGridCellType.apple: + final List appleFrames = []; + final isForward = (extra as bool?) ?? true; + if (isForward || returnAll) { + appleFrames.addAll(List.generate(4, (frameIndex) => "${baseImgPath}apple/forward_animation${frameIndex}.png")); + } + if (!isForward || returnAll) { + appleFrames.addAll(List.generate(3, (frameIndex) => "${baseImgPath}apple/reverse_animation${frameIndex}.png").reversed.toList()); + } + + return appleFrames; + + case EncloseGridCellType.bees: + return List.generate(3, (frameIndex) => "${baseImgPath}bees/animation${frameIndex}.png"); + + case EncloseGridCellType.moose: + return []; // needs to be empty and not null to allow for seperate start and end idles. + } + } + + static List? getIdleFrames(EncloseGridCellType type, {bool returnAll = false, dynamic extra}) { + switch (type) { + case EncloseGridCellType.grass: + if (returnAll) { + return const [ + "${baseImgPath}grass/0_idle0.png", + "${baseImgPath}grass/1_idle0.png", "${baseImgPath}grass/1_idle1.png", + "${baseImgPath}grass/2_idle0.png", "${baseImgPath}grass/2_idle1.png" + ]; + } + + final usedRandom = (extra as Random?) ?? Random(); + if (usedRandom.nextDouble() < 0.003) { + return const ["${baseImgPath}grass/2_idle0.png", "${baseImgPath}grass/2_idle1.png"]; + } + + const grasses = [ + ["${baseImgPath}grass/0_idle0.png"], + ["${baseImgPath}grass/1_idle0.png", "${baseImgPath}grass/1_idle1.png"] + ]; + final grassFrames = grasses[usedRandom.nextInt(grasses.length)]; + + return grassFrames; + + case EncloseGridCellType.water: + if (returnAll) { + List waters = []; + for (final i in List.generate(16, (index) => index)) { + final binaryString = i.toRadixString(2).padLeft(4, "0"); + + if (binaryString == "0000") { + waters.addAll([ + "${baseImgPath}water/${binaryString}0_idle0.png", + "${baseImgPath}water/${binaryString}1_idle0.png", + "${baseImgPath}water/${binaryString}1_idle1.png", + "${baseImgPath}water/${binaryString}2_idle0.png", + "${baseImgPath}water/${binaryString}2_idle1.png" + ]); + } else { + waters.addAll([ + "${baseImgPath}water/${binaryString}_idle0.png", + "${baseImgPath}water/${binaryString}_idle1.png" + ]); + } + } + + return waters; + } + + final castedExtra = extra as (Random, List); + final usedRandom = castedExtra.$1; + final usedNeighboringWater = castedExtra.$2; + final slicedNeighboringWater = usedNeighboringWater.sublist(0, 4); + + final extraString = slicedNeighboringWater.map((val) => val ? "1" : "0").join(""); + if (extraString == "0000") { + if (usedNeighboringWater.any((element) => element)) { + return ["${baseImgPath}water/${extraString}0_idle0.png"]; + } + + if (usedRandom.nextDouble() < 0.003) { + return ["${baseImgPath}water/${extraString}2_idle0.png", "${baseImgPath}water/${extraString}2_idle1.png"]; + } + + final waters = [ + ["${baseImgPath}water/${extraString}0_idle0.png"], + ["${baseImgPath}water/${extraString}1_idle0.png", "${baseImgPath}water/${extraString}1_idle1.png"] + ]; + return waters[usedRandom.nextInt(waters.length)]; + } + + return [ + "${baseImgPath}water/${extraString}_idle0.png", + "${baseImgPath}water/${extraString}_idle1.png" + ]; + + case EncloseGridCellType.wall: + return const ["${baseImgPath}wall/idle0.png"]; + + case EncloseGridCellType.portal: + return List.generate(4, (frameIndex) => "${baseImgPath}portal/idle${frameIndex}.png"); + + case EncloseGridCellType.cherry: + final List cherryFrames = []; + final isStart = (extra as bool?) ?? true; + if (isStart || returnAll) { + cherryFrames.addAll(List.generate(2, (frameIndex) => "${baseImgPath}cherry/start_idle${frameIndex}.png")); + } + if (!isStart || returnAll) { + cherryFrames.addAll(List.generate(2, (frameIndex) => "${baseImgPath}cherry/end_idle${frameIndex}.png")); + } + + return cherryFrames; + + case EncloseGridCellType.apple: + final List appleFrames = []; + final isStart = (extra as bool?) ?? true; + if (isStart || returnAll) { + appleFrames.addAll(List.generate(2, (frameIndex) => "${baseImgPath}apple/start_idle${frameIndex}.png")); + } + if (!isStart || returnAll) { + appleFrames.addAll(List.generate(2, (frameIndex) => "${baseImgPath}apple/end_idle${frameIndex}.png")); + } + + return appleFrames; + + case EncloseGridCellType.bees: + final List beeFrames = []; + final isStart = (extra as bool?) ?? true; + if (isStart || returnAll) { + beeFrames.addAll(List.generate(2, (frameIndex) => "${baseImgPath}bees/start_idle${frameIndex}.png")); + } + if (!isStart || returnAll) { + beeFrames.addAll(List.generate(2, (frameIndex) => "${baseImgPath}bees/end_idle${frameIndex}.png")); + } + + return beeFrames; + + case EncloseGridCellType.moose: + final List mooseFrames = []; + final isStart = (extra as bool?) ?? true; + if (isStart || returnAll) { + mooseFrames.addAll(List.generate(2, (frameIndex) => "${baseImgPath}moose/start_idle${frameIndex}.png")); + } + if (!isStart || returnAll) { + mooseFrames.addAll(List.generate(2, (frameIndex) => "${baseImgPath}moose/end_idle${frameIndex}.png")); + } + + return mooseFrames; + } + } + + static (String, String) getMooseVoiceline(bool isEnclosed, {Random? random}) { + final usedRandom = random ?? Random(); + + if (usedRandom.nextDouble() < 0.01) { + return ("Testinspelning. Test, testing, testing!", "${baseAudioPath}test.mp3"); + } + + const freedVoicelines = [ + ("I love being a moose, and being free!", "${baseAudioPath}being_a_moose_and_being_free.mp3"), + ("Moohoho... I can escape thiiis wayyyyy", "${baseAudioPath}escape_this_way_2.mp3"), + ("Moohoho... I can escape this way", "${baseAudioPath}escape_this_way_3.mp3"), + ("Moo... I can escape this waaayy", "${baseAudioPath}escape_this_way_4.mp3"), + ("I can escape this wayyy", "${baseAudioPath}escape_this_way.mp3"), + ("I love being free, because I'm a moose! Moo moo", "${baseAudioPath}free_because_im_a_moose.mp3"), + ("Goodbye! Mooo", "${baseAudioPath}goodbye_moo.mp3"), + ("I'm a mooooose", "${baseAudioPath}im_a_moose_2.mp3"), + ("I'm a moose. Moo!", "${baseAudioPath}im_a_moose_3.mp3"), + ("Moo! I'm a moose", "${baseAudioPath}im_a_moose_4.mp3"), + ("Moo moo, I'm a moose!", "${baseAudioPath}im_a_moose_5.mp3"), + ("I'm a moose!", "${baseAudioPath}im_a_moose.mp3"), + ("I'm freeeee!", "${baseAudioPath}im_free_2.mp3"), + ("I'm freeeee! Mooooo", "${baseAudioPath}im_free.mp3"), + ("I'm moosing it!", "${baseAudioPath}moosing_it.mp3"), + ("Iiit's moosing tiiime!", "${baseAudioPath}moosing_time.mp3"), + ("Moo! You will never catch me", "${baseAudioPath}never_catch_me.mp3"), + ("You will never be able to enclose ME!", "${baseAudioPath}never_be_able_to_enclose_me.mp3"), + ("I'm out of here!", "${baseAudioPath}out_of_here.mp3"), + ("Moo! I'll sneak away like sooo...", "${baseAudioPath}sneak_away_like_so_2.mp3"), + ("I'll sneak away like sooo... Moohoho", "${baseAudioPath}sneak_away_like_so_3.mp3"), + ("Moo! I'll sneak away like sooo...", "${baseAudioPath}sneak_away_like_so_4.mp3"), + ("I'll sneak away like sooo...", "${baseAudioPath}sneak_away_like_so_5.mp3"), + ("I'll sneak away like sooo... Moohoho", "${baseAudioPath}sneak_away_like_so.mp3"), + ("I'll sneak away like sooo... Moohoho", "${baseAudioPath}sneak_away_like_so.mp3"), + ("I will first go THIS way and then I will go THAT way and then I'm free!", "${baseAudioPath}this_way_that_way_2.mp3"), + ("I will go THIS way and then I will go THAT way and then I'm gonna be free!", "${baseAudioPath}this_way_that_way.mp3"), + ("You can't enclose me, I'm a moose!", "${baseAudioPath}you_cant_enclose_me.mp3") + ]; + const enclosedVoicelines = [ + ("Moo... I don't want to be enclosed... Moo... );", "${baseAudioPath}dont_want_to_be_enclosed.mp3"), + ("I just want to be free... Like a moose!", "${baseAudioPath}free_like_a_moose.mp3"), + ("I want to hang out with the other mooses", "${baseAudioPath}hang_out_with_the_others.mp3"), + ("Moo... I have been enclosed. Oh no );", "${baseAudioPath}have_been_enclosed.mp3"), + ("Moohoho... I just want to be free. Why won't you let me? );", "${baseAudioPath}just_want_to_be_free.mp3"), + ("Why won't you let me be free? Moooo );", "${baseAudioPath}let_me_be_free.mp3"), + ("Why won't you let me be go?", "${baseAudioPath}let_me_go.mp3"), + ("Moo );", "${baseAudioPath}moo_sad_2.mp3"), + ("Moo );", "${baseAudioPath}moo_sad.mp3"), + ("I moost be free", "${baseAudioPath}moost_be_free.mp3"), + ("Why moost this happen to me?", "${baseAudioPath}moost_this_happen.mp3"), + ("Why moost you do this to me?", "${baseAudioPath}moost_you_do_this.mp3"), + ("Why must I be enclosed like this?", "${baseAudioPath}must_i_be_enclosed.mp3"), + ("A moose is not supposed to be enclosed. A moose is supposed to be free. Free me... Mooo", "${baseAudioPath}not_supposed_to_be_enclosed.mp3"), + ("I want to be freee );", "${baseAudioPath}want_to_be_free.mp3"), + ("Moo, you enclosed me );", "${baseAudioPath}you_enclosed_me.mp3"), + ]; + + final possibleVoicelines = isEnclosed ? enclosedVoicelines : freedVoicelines; + final chosenVoiceline = possibleVoicelines[usedRandom.nextInt(possibleVoicelines.length)]; + + return chosenVoiceline; + } +} diff --git a/lib/screens/games/enclose_moose/call_counter.dart b/lib/screens/games/enclose_moose/call_counter.dart new file mode 100644 index 00000000..0d340de9 --- /dev/null +++ b/lib/screens/games/enclose_moose/call_counter.dart @@ -0,0 +1,109 @@ +import "package:flutter/foundation.dart"; + +class CallCounter extends ChangeNotifier { + final Map _counter = {}; + final Map _values = {}; + final Map>> _futures = {}; + + @override + void dispose() { + removeAll(); + + super.dispose(); + } + + int? getCount(T key) { + return _counter[key]; + } + + E? getValue(T key) { + return _values[key]; + } + + void increment(T key, E value, {Duration duration = const Duration(seconds: 4)}) { + _counter[key] = (_counter[key] ?? 0) + 1; + _values[key] = value; + notifyListeners(); + + final future = Future.delayed(duration, () { + if (_counter[key] == 1) { + _counter[key] = 0; + notifyListeners(); + } else { + _counter[key] = _counter[key]! - 1; + } + + if (_futures[key]!.length != 0) { + _futures[key]!.removeAt(0); + } + }); + if (!_futures.containsKey(key)) { + _futures[key] = []; + } + _futures[key]!.add(future); + } + + bool isNotEmpty(T key) { + return exists(key) && _counter[key] != 0; + } + + bool exists(T key) { + return _counter[key] != null; + } + + void remove(T key) { + _counter.remove(key); + _values.remove(key); + notifyListeners(); + + for (final Future future in _futures[key] ?? []) { + future.ignore(); + } + _futures[key]?.clear(); + } + + void removeAll() { + for (final key in _futures.keys) { + remove(key); + } + } +} + +class RestrictedCallCounter extends ChangeNotifier { + RestrictedCallCounter({ + required this.callCounter, + required this.key + }) { + callCounter.addListener(notifyListeners); + } + + final CallCounter callCounter; + final T key; + + @override + void dispose() { + callCounter.removeListener(notifyListeners); + + super.dispose(); + } + + E? getValue() { + return callCounter.getValue(key); + } + + void increment(E value, {Duration duration = const Duration(seconds: 4)}) { + callCounter.increment(key, value, duration: duration); + } + + bool isNotEmpty() { + return callCounter.isNotEmpty(key); + } + + bool exists() { + return callCounter.exists(key); + } + + void remove() { + callCounter.remove(key); + } +} diff --git a/lib/screens/games/enclose_moose/enclose_grid.dart b/lib/screens/games/enclose_moose/enclose_grid.dart new file mode 100644 index 00000000..cf7570db --- /dev/null +++ b/lib/screens/games/enclose_moose/enclose_grid.dart @@ -0,0 +1,364 @@ +import "dart:collection"; +import "dart:math"; +import "package:flutter/material.dart"; +import "enclose_grid_tile.dart"; + +class EncloseGrid extends ChangeNotifier { + final int wallBudget; + late List flatGrid; + late final Map> portals; + late final int gridWidth; + late final int gridHeight; + late final EncloseGridTile mooseTile; + + int wallsLeft = 0; + int? score; + List? escapePath; + AxisDirection? escapeDirection; + + EncloseGrid(String encodedGrid, this.wallBudget) { + wallsLeft = wallBudget; + + const bonusScoreDict = { + EncloseGridCellType.cherry: 3, + EncloseGridCellType.apple: 10, + EncloseGridCellType.bees: -5 + }; + + final gridString = encodedGrid.replaceAll("\n", ""); + gridWidth = encodedGrid.indexOf("\n"); + gridHeight = gridString.length ~/ gridWidth; + + // fullGrid = [[]]; + flatGrid = []; + portals = {}; + for (final character in gridString.characters) { + final flatIndex = flatGrid.length; + + final type = EncloseGridCellType.fromString(character); + final portalIndex = type != EncloseGridCellType.portal ? null : int.parse(character); + final tile = EncloseGridTile( + index: flatIndex, + rowIndex: flatIndex ~/ gridWidth, + columnIndex: flatIndex % gridWidth, + type: type ?? EncloseGridCellType.grass, + bonusScore: bonusScoreDict[type] ?? 0, + portalIndex: portalIndex + ); + flatGrid.add(tile); + + if (type == EncloseGridCellType.portal) { + if (!portals.containsKey(portalIndex!)) { + portals[portalIndex] = []; + } + portals[portalIndex]!.add(tile); + } + if (type == EncloseGridCellType.moose) { + mooseTile = tile; + } + } + + updateEnclosure(); + } + + void toggleWall(EncloseGridTile tile, {bool doUpdate = true}) { + if (!tile.canToggleWall) return; + + if (tile.isGrass) { + if (wallsLeft == 0) return; + + tile.toggleWall(); + wallsLeft -= 1; + } else { + tile.toggleWall(); + wallsLeft += 1; + } + + if (doUpdate) { + updateEnclosure(); + } + } + + void reset({bool doUpdate = true}) { + wallsLeft = wallBudget; + + for (final wallIndex in getWallIndices()) { + final tile = flatGrid[wallIndex]; + tile.toggleWall(); + } + + if (doUpdate) { + updateEnclosure(); + } + } + + Set getEscapeDirections(EncloseGridTile tile) { + final Set escapeDirections = {}; + if (tile.columnIndex == 0) { + escapeDirections.add(AxisDirection.left); + } + if (tile.rowIndex == 0) { + escapeDirections.add(AxisDirection.up); + } + if (tile.columnIndex == gridWidth - 1) { + escapeDirections.add(AxisDirection.right); + } + if (tile.rowIndex == gridHeight - 1) { + escapeDirections.add(AxisDirection.down); + } + + return escapeDirections; + } + + EncloseGridTile? getDirectionalNeighbor(EncloseGridTile tile, AxisDirection direction, {AxisDirection? secondaryDirection}) { + Set escapeDirections = getEscapeDirections(tile); + if (escapeDirections.contains(direction) || escapeDirections.contains(secondaryDirection)) { + return null; + } + + final directionOffsetDict = { + AxisDirection.left: -1, + AxisDirection.up: -gridWidth, + AxisDirection.right: 1, + AxisDirection.down: gridWidth, + }; + final flatIndex = tile.index + directionOffsetDict[direction]! + (directionOffsetDict[secondaryDirection] ?? 0); + + return flatGrid[flatIndex]; + } + + Iterable getNeighbors(EncloseGridTile tile, {bool includeDiagonal = false, bool keepNulls = false}) { + final List neighbors = [ + getDirectionalNeighbor(tile, AxisDirection.left), + getDirectionalNeighbor(tile, AxisDirection.up), + getDirectionalNeighbor(tile, AxisDirection.right), + getDirectionalNeighbor(tile, AxisDirection.down) + ]; + if (includeDiagonal) { + neighbors.addAll([ + getDirectionalNeighbor(tile, AxisDirection.left, secondaryDirection: AxisDirection.up), + getDirectionalNeighbor(tile, AxisDirection.right, secondaryDirection: AxisDirection.up), + getDirectionalNeighbor(tile, AxisDirection.left, secondaryDirection: AxisDirection.down), + getDirectionalNeighbor(tile, AxisDirection.right, secondaryDirection: AxisDirection.down) + ]); + } + + if (tile.isPortal) { + for (final portalTile in portals[tile.portalIndex!]!) { + if (portalTile.index != tile.index) { + neighbors.add(portalTile); + } + } + } + + if (!keepNulls) { + return neighbors.nonNulls.toSet(); + } + + return neighbors; + } + + void updateEnclosure() { + // BFS from moosey. + + final oldEnclosure = getEnclosure(); + + final Queue queue = Queue(); + final Map visited = {}; + final Map parentMap = {}; + + queue.add(mooseTile); + visited[mooseTile] = 0; + parentMap[mooseTile] = null; + + while (queue.isNotEmpty) { + final current = queue.removeFirst(); + + for (final neighbor in getNeighbors(current)) { + final isVisited = visited.containsKey(neighbor!); + if (isVisited || !neighbor.isOpen) continue; + + parentMap[neighbor] = current; + visited[neighbor] = visited[current]! + 1; + + final neighborEscapeDirections = getEscapeDirections(neighbor); + if (neighborEscapeDirections.isNotEmpty) { + List backtrackPath = []; + EncloseGridTile? currentNode = neighbor; + while (currentNode != null) { + backtrackPath.add(currentNode); + currentNode = parentMap[currentNode]; + } + + escapePath = backtrackPath.reversed.toList(); + escapeDirection = neighborEscapeDirections.first; + score = null; + for (final EncloseGridTile tile in oldEnclosure ?? {}) { + tile.update( + newIsEnclosed: false, + newWaitFrames: null, + newReverseWaitFrames: null + ); + } + notifyListeners(); + + return; + } + + queue.add(neighbor); + } + } + + final enclosure = visited.keys.toSet(); + final enclosureDistances = { + for (final key in enclosure) key: visited[key]! + }; + + escapePath = null; + escapeDirection = null; + + var currentScore = enclosure.length; + for (final tile in enclosure) { + currentScore += tile.bonusScore; + } + score = currentScore; + + Map? expandDistances; + if (oldEnclosure != null) { + // Special case if the enclosure is expanded to make wheat start growing immediately (instead of waiting {distance to moose} frames to start) + expandDistances = {}; + for (final key in enclosure) { + if (oldEnclosure.contains(key)) { + expandDistances[key] = visited[key]!; + } else { + expandDistances[key] = 0; + EncloseGridTile currentNode = key; + while (!oldEnclosure.contains(currentNode)) { + expandDistances[key] = expandDistances[key]! + 1; + currentNode = parentMap[currentNode]!; + } + } + } + } + + final maxDistance = enclosureDistances.values.reduce(max); + final affectedTiles = enclosure.difference(oldEnclosure ?? {}).union(oldEnclosure?.difference(enclosure) ?? {}); + for (final tile in affectedTiles) { + final distance = enclosureDistances[tile]; + final expandDistance = expandDistances?[tile] ?? distance; + + tile.update( + newIsEnclosed: enclosure.contains(tile), + newWaitFrames: expandDistance == null ? null : expandDistance.toDouble(), // Could multiply these to change spread speed + newReverseWaitFrames: distance == null ? null : (maxDistance - distance).toDouble() + ); + } + + notifyListeners(); + } + + Set getWallIndices() { + return flatGrid + .where((tile) => tile.isWall) + .map((tile) => tile.index) + .toSet(); + } + + Set? getEnclosure() { + if (escapePath != null || score == null) return null; + + return flatGrid + .where((tile) => tile.isEnclosed) + .toSet(); + } + + /* + void _updateEscapePath() { + // multi-BFS from all boundaries. + + Queue queue = Queue(); + Set visited = Set(); + Map parentMap = Map(); + + for (int y = 0; y < gridHeight; y++) { + final leftIndex = y * gridWidth; + if (isOpen(leftIndex)) { + queue.add(leftIndex); + visited.add(leftIndex); + parentMap[leftIndex] = null; + } + + final rightIndex = y * gridWidth + gridWidth - 1; + if (isOpen(rightIndex)) { + queue.add(rightIndex); + visited.add(rightIndex); + parentMap[rightIndex] = null; + } + } + for (int x = 0; x < gridWidth; x++) { + final topIndex = x; + if (isOpen(topIndex)) { + queue.add(topIndex); + visited.add(topIndex); + parentMap[topIndex] = null; + } + + final bottomIndex = (gridHeight - 1) * gridWidth + x; + if (isOpen(bottomIndex)) { + queue.add(bottomIndex); + visited.add(bottomIndex); + parentMap[bottomIndex] = null; + } + } + + final searchDirections = [ + -1, + 1, + -gridWidth, + gridWidth + ]; + while (queue.isNotEmpty) { + final current = queue.removeFirst(); + + var neighbors = searchDirections.map((direction) => current + direction).toList(); + if (portals.containsKey(current)) { + neighbors.add(portals[current]!); + } + for (final neighbor in neighbors) { + final isVisited = visited.contains(neighbor); + final isInvalid = neighbor < 0 || neighbor >= flatGrid.length; + + visited.add(neighbor); + if (isVisited || isInvalid) continue; + + final ismoose = flatGrid[neighbor] == EncloseGridCellType.moose; + if (ismoose) { + List reversePath = [neighbor]; + int? currentBacktrack = current; + while (currentBacktrack != null) { + reversePath.add(currentBacktrack); + currentBacktrack = parentMap[currentBacktrack]; + } + + enclosed = null; + solution = reversePath.reversed.toSet(); + + return; + } + + final isOpen = flatGrid[neighbor] != EncloseGridCellType.water && flatGrid[neighbor] != EncloseGridCellType.wall; + if (isOpen) { + parentMap[neighbor] = current; + queue.add(neighbor); + } + } + } + + enclosed = List.generate(flatGrid.length, (e) => e) + .where((flatIndex) => !visited.contains(flatIndex)) + .where(isOpen) + .toSet(); + solution = null; + } + */ +} diff --git a/lib/screens/games/enclose_moose/enclose_grid_tile.dart b/lib/screens/games/enclose_moose/enclose_grid_tile.dart new file mode 100644 index 00000000..a0f9eb61 --- /dev/null +++ b/lib/screens/games/enclose_moose/enclose_grid_tile.dart @@ -0,0 +1,106 @@ +import "package:flutter/foundation.dart"; + +enum EncloseGridCellType { + grass, + water, + wall, + portal, + cherry, + apple, + bees, + moose; + + static const _decodingDict = { + ".": grass, + "~": water, + "C": cherry, + "G": apple, + "S": bees, + "H": moose + }; + + static EncloseGridCellType? fromString(String character) { + final digitRegExp = RegExp(r"\d"); + if (digitRegExp.hasMatch(character)) { + return portal; + } + + return _decodingDict[character]; + } +} + + +class EncloseGridTile extends ChangeNotifier { + EncloseGridTile({ + required this.index, + required this.rowIndex, + required this.columnIndex, + required type, + required this.bonusScore, + this.portalIndex + }) : _type = type; + + final int index; + final int rowIndex; + final int columnIndex; + EncloseGridCellType _type; + final int bonusScore; + final int? portalIndex; + + bool _isEnclosed = false; + // int? distance; + // int? expandDistance; + double? _waitFrames; // Don't really like these being properties of tile since they are really UI things + double? _reverseWaitFrames; + + EncloseGridCellType get type => _type; + bool get isEnclosed => _isEnclosed; + double? get waitFrames => _waitFrames; + double? get reverseWaitFrames => _reverseWaitFrames; + + bool get isWall => _type == EncloseGridCellType.wall; + bool get isWater => _type == EncloseGridCellType.water; + bool get isGrass => _type == EncloseGridCellType.grass; + bool get isPortal => _type == EncloseGridCellType.portal; + bool get isMoose => _type == EncloseGridCellType.moose; + bool get isBonus => _type == EncloseGridCellType.cherry || _type == EncloseGridCellType.apple || _type == EncloseGridCellType.bees; + bool get isOpen => !(isWater || isWall); + bool get canToggleWall => isGrass || isWall; + + void toggleWall({bool doUpdate = true}) { + if (!canToggleWall) return; + + final newType = isGrass ? EncloseGridCellType.wall : EncloseGridCellType.grass; + if (doUpdate) { + update(newType: newType); + } + } + + void update({EncloseGridCellType? newType, bool? newIsEnclosed, double? newWaitFrames, double? newReverseWaitFrames}) { + bool hasChanged = false; + + if (newType != null && newType != _type) { + _type = newType; + hasChanged = true; + } + + if (newIsEnclosed != null && newIsEnclosed != _isEnclosed) { + _isEnclosed = newIsEnclosed; + hasChanged = true; + } + + if (newWaitFrames != _waitFrames) { + _waitFrames = newWaitFrames; + hasChanged = true; + } + + if (newReverseWaitFrames != _reverseWaitFrames) { + _reverseWaitFrames = newReverseWaitFrames; + hasChanged = true; + } + + if (hasChanged) { + notifyListeners(); + } + } +} diff --git a/lib/screens/games/enclose_moose/enclose_moose_level_page.dart b/lib/screens/games/enclose_moose/enclose_moose_level_page.dart new file mode 100644 index 00000000..e1a68159 --- /dev/null +++ b/lib/screens/games/enclose_moose/enclose_moose_level_page.dart @@ -0,0 +1,1104 @@ +import "dart:math" as math; +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:built_collection/built_collection.dart"; +import "package:audioplayers/audioplayers.dart"; +import "package:dio/dio.dart"; +import "package:api_client/api_client.dart"; +import "package:fsek_mobile/services/api.service.dart"; +import "package:fsek_mobile/l10n/app_localizations.dart"; +import "enclose_grid.dart"; +import "enclose_grid_tile.dart"; +import "call_counter.dart"; +import "tile_widget.dart"; +import "asset_handler.dart"; +import "helper_widgets/wiggling_widget.dart"; +import "helper_widgets/outlined_text.dart"; +import "helper_widgets/highlighted_text.dart"; +import "helper_widgets/animated_smooth_arrow.dart"; +import "helper_widgets/animated_thought_bubble.dart"; +import "helper_widgets/submissions_bar_chart.dart"; +import "../../../util/time.dart"; + +class EncloseMooseLevelPage extends StatefulWidget { + const EncloseMooseLevelPage({ + required this.pageController, + required this.level, + required this.availableLevels + }); + + final PageController pageController; + final EncloseMooseLevelRead level; + final List availableLevels; + + @override + _EncloseMooseLevelState createState() => _EncloseMooseLevelState(); +} + +class _EncloseMooseLevelState extends State with TickerProviderStateMixin { + late bool _hasSubmitted = _usedLevel.playerSubmission != null; + late EncloseMooseLevelRead _usedLevel = widget.level; + + late final _grid = EncloseGrid(_usedLevel.encodedGrid, _usedLevel.wallBudget); + late final _portalOffset = math.Random(_usedLevel.levelId.hashCode).nextDouble() * 360; + + late List _dailies = widget.availableLevels.where((level) => level.dayIndex != null).toList(); + late List _nonDailies = widget.availableLevels.where((level) => level.dayIndex == null).toList(); + + int? _bestSolutionScore; + Set? _bestSolution; + + final _idleDuration = const Duration(milliseconds: 1000); + late final _idleController = AnimationController(vsync: this) + ..repeat( + reverse: false, + period: _idleDuration + ); + + final CallCounter _openCallCounter = CallCounter(); + final CallCounter _tooltipCallCounter = CallCounter(); + final CallCounter> _portalCallCounter = CallCounter(); + + final Map> _tileTooltipCallCounters = {}; + // Save these so we don't make new ones every time the grid rebuilds. + + late final Map _portalFilters = Map.fromIterables(_grid.portals.keys, _grid.portals.keys.map( + (portalIndex) => ColorFilter.matrix(_hueRotationMatrix(_portalOffset + 360 * portalIndex / _grid.portals.length)) + )); // Precompute these for performance + + // bool panPlacesWalls = true; + + String _mooseThought = ""; + AudioPlayer _audioPlayer = AudioPlayer(); + + @override + void initState() { + super.initState(); + + updateScore(null); // kind of only needed if level is enclosed from start + } + + @override + void dispose() { + _idleController.dispose(); + _audioPlayer.dispose(); + + for (final tileTooltipCallCounter in _tileTooltipCallCounters.values) { + tileTooltipCallCounter.dispose(); + } + + _openCallCounter.dispose(); + _tooltipCallCounter.dispose(); + _portalCallCounter.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // const backgroundImage = "assets/img/enclose_moose/grass/0_idle0.png"; + final t = AppLocalizations.of(context)!; + + final topBar = Padding( + // color: Colors.black.withAlpha(50), + padding: const EdgeInsets.all(5), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + WigglingWidget( + controller: _idleController, + child: IconButton( + onPressed: () { + showDialog(context: context, builder: _buildDayChooserDialog()); + }, + icon: const Icon(Icons.calendar_month_rounded), + color: Colors.white + ) + ), + + Visibility( + visible: _hasSubmitted, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: WigglingWidget( + controller: _idleController, + child: IconButton( + onPressed: () { + showDialog(context: context, builder: _buildSubmitDialog()); + }, + icon: const Icon(Icons.leaderboard_outlined), + color: Colors.white + ) + ) + ), + + const Spacer(), + + Row( + children: [ + Visibility( + visible: widget.availableLevels.first != _usedLevel, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: WigglingWidget( + controller: _idleController, + child: IconButton( + onPressed: () { + widget.pageController.jumpToPage(widget.pageController.page!.toInt() - 1); + }, + icon: const Icon(Icons.chevron_left), + color: Colors.white + ) + ) + ), + + WigglingWidget( + controller: _idleController, + child: OutlinedText( + text: _usedLevel.dayIndex != null ? "Day ${_usedLevel.dayIndex}" : "Bonus", + style: const TextStyle( + color: Colors.white, + fontSize: 25, + fontFamily: "Schoolbell" + ) + ) + ), + + Visibility( + visible: widget.availableLevels.last != _usedLevel, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: WigglingWidget( + controller: _idleController, + child: IconButton( + onPressed: () { + widget.pageController.jumpToPage(widget.pageController.page!.toInt() + 1); + }, + icon: const Icon(Icons.chevron_right), + color: Colors.white + ) + ) + ), + ] + ), + + Spacer(), + + WigglingWidget( + controller: _idleController, + child: IconButton( + onPressed: () { + _grid.reset(); + }, + icon: const Icon(Icons.restart_alt_outlined), + color: Colors.white + ) + ), + + WigglingWidget( + controller: _idleController, + child: IconButton( + onPressed: () { + showDialog(context: context, builder: _buildHelpDialog()); + }, + icon: const Icon(Icons.question_mark_outlined), + color: Colors.white + ) + ), + ] + ), + + WigglingWidget( + controller: _idleController, + child: OutlinedText( + text: t.localeName == "sv" ? _usedLevel.nameSv : _usedLevel.nameEn, + style: const TextStyle( + color: Colors.yellow, + fontSize: 20, + fontFamily: "Schoolbell" + ), + textAlign: TextAlign.center, + ) + ), + ] + ) + ); + + return Scaffold( + appBar: AppBar( + leading: WigglingWidget( + controller: _idleController, + child: const BackButton( + color: Colors.white + ) + ), + + title: WigglingWidget( + controller: _idleController, + child: Text.rich( + TextSpan( + children: "enclose.moose".characters.map((char) => WidgetSpan( // Could consider making all text like this + child: WigglingWidget( + controller: _idleController, + child: OutlinedText( + text: char, + style: const TextStyle( + color: Colors.white, + fontSize: 45, + fontFamily: "Schoolbell" + ) + ) + ) + )).toList() + ) + ) + ) + ), + body: Stack( + children: [ + SizedBox.expand( + // child: Image.asset( + // backgroundImage, + // fit: BoxFit.fill + // ) + child: Container( + color: Color(0xFF00501f) + ) + ), + + Column( + children: [ + Visibility( + visible: false, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + maintainInteractivity: false, + child: topBar + ), + + Expanded( + child: InteractiveViewer( + clipBehavior: Clip.none, // pretty cool! maybe not good though + child: Center( + child: LayoutBuilder( + builder: (context, constraints) { + final boardWidth = constraints.maxWidth; + final boardHeight = constraints.maxHeight; + + final cellSize = math.min( + boardWidth / _grid.gridWidth, + boardHeight / _grid.gridHeight, + ); + + return SizedBox( + width: _grid.gridWidth * cellSize, + height: _grid.gridHeight * cellSize + 0.0000001, // Adds a really small height, which for some reason sometimes fixes allowing clipping + child: Stack( + clipBehavior: Clip.none, + children: [ + GridView.count( + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: _grid.gridWidth, + children: _grid.flatGrid.map((tile) { + // final isEnclosed = _grid.enclosure?.contains(tile) ?? false; + + // const wheatFrameDuration = Duration(milliseconds: 40); + // final frameOffset = _grid.getDistance(tile); + // final waitTime = frameOffset == null ? null : wheatFrameDuration * frameOffset; + // final reverseFramOffset = frameOffset == null ? null : maxOffset - _grid.getDistance(tile, includeExpand: false)!; + // final reverseWaitTime = reverseFramOffset == null ? null : wheatFrameDuration * reverseFramOffset; + + final neighboringWater = !tile.isWater ? null : _grid.getNeighbors(tile, includeDiagonal: true, keepNulls: true).map( + (neighbor) => !(neighbor?.isWater ?? false) + ).toList(); + + return Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.green[900]!, + width: cellSize / 100 + ) + ), + child: OverflowBox( + maxHeight: double.infinity, + alignment: Alignment.bottomCenter, + child: TileWidget( + tile: tile, + vsync: this, + idleController: _idleController, + tileRandomFunction: () => math.Random(_usedLevel.levelId.hashCode + tile.index.hashCode), + onTap: () => _tapTile(tile), + portalFilter: _portalFilters[tile.portalIndex], + neighboringWater: neighboringWater, + tooltipCallCounter: _tileTooltipCallCounters.putIfAbsent( + tile, // key used + () => RestrictedCallCounter( + callCounter: _tooltipCallCounter, + key: tile + ) + ) + ) + ) + ); + }).toList() + ), + + /* // for panning to place walls. Interfered with the gridcell gesturedetector and not all that useful + GestureDetector( + onPanDown: (details) { + final rowIndex = (details.localPosition.dy / cellSize).floor(); + final columnIndex = (details.localPosition.dx / cellSize).floor(); + final flatIndex = rowIndex * _grid.gridWidth + columnIndex; + + if (_grid.flatGrid[flatIndex] == .grass) { + panPlacesWalls = true; + } else if (_grid.flatGrid[flatIndex] == .wall) { + panPlacesWalls = false; + } + + print("Start"); + print(flatIndex); + final toggleWallResult = _grid.toggleWall(flatIndex); + print(toggleWallResult); + setState(() {}); + }, + onPanUpdate: (details) { + final rowIndex = (details.localPosition.dy / cellSize).floor(); + final columnIndex = (details.localPosition.dx / cellSize).floor(); + final flatIndex = rowIndex * _grid.gridWidth + columnIndex; + + if ((_grid.flatGrid[flatIndex] == .grass && !panPlacesWalls) || (_grid.flatGrid[flatIndex] == .wall && panPlacesWalls)) return; + + print("Update"); + print(flatIndex); + final toggleWallResult = _grid.toggleWall(flatIndex); + print(toggleWallResult); + setState(() {}); + } + ), + */ + + ListenableBuilder( + listenable: _portalCallCounter, + builder: (context, child) => Stack( + clipBehavior: Clip.none, + children: List.generate(_grid.portals.length, (index) => index).map((portalIndex) { + final portalConnection = _portalCallCounter.getValue(portalIndex); + const defaultPortalColor = Color.fromRGBO(74, 187, 234, 255); + + if (portalConnection == null) { + return null; + } + + return ColorFiltered( + key: Key(portalConnection.toString()), // to keep from restarting animation when other portals change + colorFilter: _portalFilters[portalIndex]!, + child: IgnorePointer( + child: AnimatedSmoothArrow( + isVisible: _portalCallCounter.isNotEmpty(portalIndex), + onDisappear: () { + _portalCallCounter.remove(portalIndex); + }, + vsync: this, + flatIndices: portalConnection, + gridWidth: _grid.gridWidth, + cellSize: cellSize, + lineColor: defaultPortalColor, + showArrowHead: false, + idleController: _idleController, + frequency: 2 + ) + ) + ); + }).nonNulls.toList() + ) + ), + + ListenableBuilder( + listenable: Listenable.merge([ + _grid, + _openCallCounter + ]), + builder: (context, child) { + const thoughtCloudWidth = 150; // Would like this to be dynamic in some way but don't know how? Would have to specify the center coordinate + + return Stack( + clipBehavior: Clip.none, + fit: StackFit.expand, + children: [ + if (_grid.escapePath != null) + IgnorePointer( + child: AnimatedSmoothArrow( + isVisible: (_openCallCounter.getCount("escape") ?? 0) != 0, + vsync: this, + flatIndices: _grid.escapePath!.map((tile) => tile.index).toList(), + gridWidth: _grid.gridWidth, + cellSize: cellSize, + extraLength: cellSize, + extraDirection: _grid.escapeDirection, + lineColor: Colors.white.withAlpha(200), + idleController: _idleController, + frequency: 2 + ) + ), + + Positioned( + left: (_grid.mooseTile.columnIndex - 0.5) * cellSize - thoughtCloudWidth / 2, + right: (_grid.gridWidth - _grid.mooseTile.columnIndex - 1) * cellSize - thoughtCloudWidth / 2, + bottom: (_grid.gridHeight - _grid.mooseTile.rowIndex) * cellSize + 20, + child: IgnorePointer( + child: AnimatedThoughtBubble( + isVisible: (_openCallCounter.getCount("thinking") ?? 0) != 0, + text: _mooseThought, + color: Colors.white.withAlpha(200), + idleController: _idleController + ) + ) + ), + ] + ); + } + ) + ] + ) + ); + } + ) + ) + ) + ), + + ListenableBuilder( + listenable: Listenable.merge([ + _grid, + _openCallCounter + ]), + builder: (context, child) { + final isCurrentBest = _bestSolutionScore == _grid.score || _bestSolution == null; + final isNotShowingYours = _hasSubmitted && !setEquals(_grid.getWallIndices(), _usedLevel.playerSubmission!.playerSolution.toSet()); + + /* // Is pretty good to show what parts of the screen are clickable but feels restrictive as a user + return Container( + // color: Colors.black.withAlpha(50), + child: SafeArea( + */ + return SafeArea( + child: Padding( + padding: const EdgeInsets.only(right: 15, left: 15, top: 5), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + WigglingWidget( + controller: _idleController, + child: HighlightedText( + showHighlight: (_openCallCounter.getCount("walls") ?? 0) != 0, + child: OutlinedText( + text: "Walls: ${_grid.wallsLeft}/${_grid.wallBudget}", + style: const TextStyle( + fontSize: 25, + fontFamily: "Schoolbell" + ) + ) + ) + ), + + Spacer(), + + Visibility( + visible: !_hasSubmitted && _grid.escapePath == null, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: WigglingWidget( + child: TextButton( + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ) + ), + onPressed: _submitSolution, + child: const OutlinedText( + text: "Submit", + style: TextStyle( + color: Colors.white, + fontSize: 25, + fontFamily: "Schoolbell" + ) + ) + ), + controller: _idleController + ) + ), + + Spacer(), + + WigglingWidget( + controller: _idleController, + child: HighlightedText( + showHighlight: (_openCallCounter.getCount("score") ?? 0) != 0, + child: OutlinedText( + text: "Score: ${_grid.score ?? "N/A"}", + style: const TextStyle( + fontSize: 25, + fontFamily: "Schoolbell" + ) + ) + ) + ), + ] + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Visibility( + visible: _hasSubmitted && _grid.score != _usedLevel.optimalScore, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: WigglingWidget( + controller: _idleController, + child: OutlinedButton.icon( + onPressed: () { + _changeToSolution(_usedLevel.optimalSolution!); + }, + style: OutlinedButton.styleFrom( + backgroundColor: Color(0xFF15542D).withAlpha(200), // Colors.black.withAlpha(55), + visualDensity: VisualDensity.compact + ), + label: OutlinedText( + text: "Optimal: ${_usedLevel.optimalScore}", + style: const TextStyle( + // color: Colors.white, + fontSize: 20, + fontFamily: "Schoolbell" + ) + ), + icon: const Icon(Icons.star_sharp) + ) + ) + ), + + Visibility( + visible: isNotShowingYours || !isCurrentBest, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: WigglingWidget( + controller: _idleController, + child: OutlinedButton.icon( + onPressed: () { + _changeToSolution(_hasSubmitted ? _usedLevel.playerSubmission!.playerSolution : _bestSolution!); + }, + style: OutlinedButton.styleFrom( + backgroundColor: Color(0xFF15542D).withAlpha(200), // Colors.black.withAlpha(55), + visualDensity: VisualDensity.compact + ), + label: OutlinedText( + text: _hasSubmitted ? "Your solution: ${_usedLevel.playerSubmission!.playerScore}" : "Your best: $_bestSolutionScore", + style: const TextStyle( + // color: Colors.white, + fontSize: 20, + fontFamily: "Schoolbell" + ) + ), + icon: const Icon(Icons.keyboard_return_outlined) + ) + ) + ), + ] + ), + ] + ) + ) + ); + } + ) + ] + ), + + topBar, + ] + ) + ); + } + + List _hueRotationMatrix(double angleDeg) { // random matrix! + final angleRad = angleDeg * math.pi / 180; + final cosA = math.cos(angleRad); + final sinA = math.sin(angleRad); + + const lumR = 0.213; + const lumG = 0.715; + const lumB = 0.072; + + return [ + lumR + cosA * (1 - lumR) + sinA * (-lumR), + lumG + cosA * (-lumG) + sinA * (-lumG), + lumB + cosA * (-lumB) + sinA * (1 - lumB), + 0, 0, + + lumR + cosA * (-lumR) + sinA * 0.143, + lumG + cosA * (1 - lumG) + sinA * 0.140, + lumB + cosA * (-lumB) + sinA * -0.283, + 0, 0, + + lumR + cosA * (-lumR) + sinA * (-(1 - lumR)), + lumG + cosA * (-lumG) + sinA * lumG, + lumB + cosA * (1 - lumB) + sinA * lumB, + 0, 0, + + 0, 0, 0, 1, 0, + ]; + } + + void updateScore(int? oldScore) { + if (_grid.score == null) return; + + if (_grid.score! != oldScore) { + _openCallCounter.increment("score", 0, duration: const Duration(seconds: 1)); + + if (_bestSolutionScore == null || _grid.score! > _bestSolutionScore!) { + _bestSolutionScore = _grid.score!; + _bestSolution = _grid.getWallIndices(); + } + } + } + + void _tapTile(EncloseGridTile tile) async { + /* // Useful for debug! + final builder = EncloseMooseLevelReadBuilder(); + builder.levelId = 10; + builder.nameSv = "debug"; + builder.nameEn = "debug"; + builder.encodedGrid = ""; + builder.wallBudget = 10; + builder.playerSubmission = EncloseMooseSubmissionReadBuilder(); + builder.playerSubmission.levelId = 10; + builder.playerSubmission.submissionTime = DateTime.now(); + builder.playerSubmission.playerId = 0; + builder.playerSubmission.playerScore = 10; + builder.releaseDate = Date.now(); + builder.optimalScore = 54; + + _usedLevel = builder.build(); + */ + + if (_grid.wallsLeft == 0 && tile.isGrass) { + _openCallCounter.increment("walls", 0, duration: const Duration(seconds: 1)); + } else if (tile.canToggleWall) { + final oldScore = _grid.score; + _grid.toggleWall(tile); + + updateScore(oldScore); + } + + if (tile.isPortal) { + // final portalIndex = _grid.getPortalIndex(flatIndex)!; + final portalIndex = tile.portalIndex!; + for (final otherTile in _grid.portals[portalIndex]!) { + final shownPortal = _portalCallCounter.getValue(portalIndex); + final isOtherPortalShown = shownPortal != null && shownPortal[0] != tile.index; + if (otherTile.index == tile.index || isOtherPortalShown) { + continue; + } + + _portalCallCounter.increment(portalIndex, [tile.index, otherTile.index]); + } + } + + if (tile.isMoose) { + final mooseVoiceline = AssetHandler.getMooseVoiceline(_grid.escapePath == null); + _mooseThought = mooseVoiceline.$1; + _audioPlayer.release(); // Doesn't otherwise happen if the same voiceline is chosen + _audioPlayer.play(AssetSource(mooseVoiceline.$2)); + + final voicelineDuration = await _audioPlayer.onDurationChanged.first; + if (_grid.escapePath != null) { + _openCallCounter.increment("escape", 0, duration: voicelineDuration); + } + + _openCallCounter.increment("thinking", 0, duration: voicelineDuration); + } + + if (tile.isBonus) { + final bonusScore = tile.bonusScore; + if (bonusScore != 0) { + _tooltipCallCounter.increment(tile, (bonusScore > 0 ? "+" : "") + "$bonusScore if enclosed"); + } + } + } + + Future _submitSolution() async { + if (_bestSolution != null) { + _changeToSolution(_bestSolution!); + } + + final solution = _grid.getWallIndices(); + final submission = EncloseMooseSubmissionCreate( + (sub) => sub + ..playerSolution = SetBuilder(solution) + ); + + const encloseMooseToken = String.fromEnvironment("ENCLOSE_MOOSE_SECRET", defaultValue: ""); + try { + final response = await ApiService.apiClient + .getEncloseMooseApi() + .encloseMooseSubmitSolution( + levelId: _usedLevel.levelId, + encloseMooseSubmissionCreate: submission, + headers: {"enclose-moose-token": encloseMooseToken} + ); + + if (!mounted) return; + + setState(() { + _usedLevel = response.data!; + widget.availableLevels[widget.availableLevels.indexWhere((level) => level.levelId == _usedLevel.levelId)] = _usedLevel; + _hasSubmitted = true; + }); + + showDialog(context: context, builder: _buildSubmitDialog()); + } on DioException catch (e) { + print("An error occured trying to submit a solution: $e"); + + showDialog(context: context, builder: _buildSubmitErrorDialog(e.response?.statusMessage ?? "")); + } + } + + void _changeToSolution(Iterable solution) { + _grid.reset(doUpdate: false); + + for (final flatIndex in solution) { + final tile = _grid.flatGrid[flatIndex]; + _grid.toggleWall(tile, doUpdate: false); + } + + _grid.updateEnclosure(); + } + + Widget Function(BuildContext) _buildDayChooserDialog() { + final t = AppLocalizations.of(context)!; + final targetKey = GlobalKey(); + + return (context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final targetContext = targetKey.currentContext; + + if (targetContext != null) { + Scrollable.ensureVisible( + targetContext, + alignment: 0.5, + duration: Duration.zero, + ); + } + }); + + return AlertDialog( + contentPadding: const EdgeInsets.all(16), + constraints: BoxConstraints(maxHeight: 400, minWidth: MediaQuery.of(context).size.width), + scrollable: true, + content: Column( + spacing: 20, + children: [ + Column( + children: [ + if (_dailies.isNotEmpty) + const Text( + "Previous days", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 25, + fontFamily: "Schoolbell" + ) + ), + + ..._dailies.map((level) => Card.outlined( + key: level.levelId != _usedLevel.levelId ? null : targetKey, + color: level.levelId != _usedLevel.levelId ? null : Colors.amber.withAlpha(100), + child: ListTile( + onTap: () { + widget.pageController.jumpToPage(widget.availableLevels.indexOf(level)); + Navigator.of(context).pop(); + }, + title: Text.rich( + TextSpan( + children: [ + TextSpan( + text: "Day ${level.dayIndex}" + ), + + TextSpan( + text: " (${Time.format(level.releaseDate.toDateTime(), "%d %M %Y", locale: "en")})", + style: TextStyle( + color: Theme.of(context).primaryColor + ) + ), + ] + ), + style: const TextStyle( + fontSize: 18, + fontFamily: "Schoolbell" + ) + ), + trailing: level.playerSubmission == null ? null : Text( + level.playerSubmission!.playerScore == level.optimalScore ? "💎" : "✅", + style: const TextStyle( + fontSize: 18 + ) + ) + ) + )).toList() + ] + ), + + Column( + children: [ + if (_nonDailies.isNotEmpty) + const Text( + "Bonus levels", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 25, + fontFamily: "Schoolbell" + ) + ), + + ..._nonDailies.map((level) => Card.outlined( + key: level.levelId != _usedLevel.levelId ? null : targetKey, + color: level.levelId != _usedLevel.levelId ? null : Colors.amber.withAlpha(100), + child: ListTile( + onTap: () { + widget.pageController.jumpToPage(widget.availableLevels.indexOf(level)); + Navigator.of(context).pop(); + }, + title: Text( + t.localeName == "sv" ? level.nameSv : level.nameEn, + style: const TextStyle( + fontSize: 18, + fontFamily: "Schoolbell" + ) + ), + trailing: level.playerSubmission == null ? null : Text( + level.playerSubmission!.playerScore == level.optimalScore ? "💎" : "✅", + style: const TextStyle( + fontSize: 18 + ) + ) + ) + )).toList() + ] + ), + ] + ) + ); + }; + } + + Widget Function(BuildContext) _buildHelpDialog() { + return (context) => const SimpleDialog( + contentPadding: const EdgeInsets.all(16), + title: Text( + "How to play", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 25, + fontFamily: "Schoolbell" + ) + ), + children: [ + Text( + "The objective is to build an enclosure for the moose by placing walls (tapping) on the grid. The moose can not move over the water. The more grass tiles in your enclosure, the higher your score. You only have a limited amount of walls so use them wisely.", + style: TextStyle( + fontSize: 18, + fontFamily: "Schoolbell" + ) + ), + ] + ); + } + + Widget Function(BuildContext) _buildSubmitDialog() { + final Map scoreDistribution = {}; + for (final entry in _usedLevel.scoreDistribution!.entries) { + scoreDistribution[int.parse(entry.key)] = entry.value; + } + // scoreDistribution[10] = 10; + + final minScore = scoreDistribution.keys.reduce(math.min); + for (final score in List.generate(_usedLevel.optimalScore! - minScore, (score) => minScore + score + 1)) { + if (!scoreDistribution.containsKey(score)) { + scoreDistribution[score] = 0; // math.Random().nextInt(20); + } + } + + int totalCount = 0; + int countLessOrEqual = 0; + for (final entry in scoreDistribution.entries) { + totalCount += entry.value; + if (entry.key <= _usedLevel.playerSubmission!.playerScore) { + countLessOrEqual += entry.value; + } + } + final percentile = countLessOrEqual / totalCount; + + String grade; + if (_usedLevel.playerSubmission!.playerScore == _usedLevel.optimalScore) { + grade = "PERFECT!!! 💎"; + } else if (percentile >= 0.75) { + grade = "Pretty good! ✅"; + } else if (percentile >= 0.5) { + grade = "Not bad! ✅"; + } else if (percentile >= 0.25) { + grade = "Not the worst! ✅"; + } else { + grade = "Come on... ✅"; + } + + return (context) => SimpleDialog( + contentPadding: const EdgeInsets.all(16), + title: Text( + grade, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 25, + fontFamily: "Schoolbell" + ) + ), + children: [ + Padding( + padding: const EdgeInsets.only(left: 10, right: 10, bottom: 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: "You scored as good or better than ", + style: const TextStyle( + fontSize: 20, + fontFamily: "Schoolbell" + ) + ), + + TextSpan( + text: (percentile * 100).toInt().toString(), + style: const TextStyle( + fontSize: 20, + fontFamily: "Schoolbell", + fontWeight: FontWeight.bold + ) + ), + + TextSpan( + text: "% of players", + style: const TextStyle( + fontSize: 20, + fontFamily: "Schoolbell" + ) + ), + ] + ) + ) + ), + + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "Your score: ", + style: const TextStyle( + fontSize: 20, + fontFamily: "Schoolbell" + ) + ), + + TextSpan( + text: _usedLevel.playerSubmission!.playerScore.toString(), + style: TextStyle( + fontSize: 20, + fontFamily: "Schoolbell", + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor + ) + ) + ] + ) + ), + + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "Optimal: ", + style: const TextStyle( + fontSize: 20, + fontFamily: "Schoolbell" + ) + ), + + TextSpan( + text: _usedLevel.optimalScore.toString(), + style: TextStyle( + fontSize: 20, + fontFamily: "Schoolbell", + fontWeight: FontWeight.bold, + color: Colors.green[800] + ) + ) + ] + ) + ), + ] + ) + ), + + SizedBox( + width: MediaQuery.of(context).size.width * 0.9, // likely more than it is allowed to take + height: 300, + child: SubmissionsBarChart( + scoreDistribution: scoreDistribution, + playerScore: _usedLevel.playerSubmission!.playerScore, + optimalScore: _usedLevel.optimalScore! + ) + ), + ] + ); + } + + Widget Function(BuildContext) _buildSubmitErrorDialog(String errorMessage) { + return (context) => SimpleDialog( + contentPadding: const EdgeInsets.all(16), + title: const Text( + "There was an error with your submission", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 25, + fontFamily: "Schoolbell" + ) + ), + children: [ + Text( + "Please try again later.\n\nResponse was: \"$errorMessage\"", + style: const TextStyle( + fontSize: 18, + fontFamily: "Schoolbell" + ) + ), + ] + ); + } +} diff --git a/lib/screens/games/enclose_moose/enclose_moose_level_placeholder_page.dart b/lib/screens/games/enclose_moose/enclose_moose_level_placeholder_page.dart new file mode 100644 index 00000000..8d7741b9 --- /dev/null +++ b/lib/screens/games/enclose_moose/enclose_moose_level_placeholder_page.dart @@ -0,0 +1,389 @@ +import "package:flutter/material.dart"; +import "helper_widgets/wiggling_widget.dart"; +import "helper_widgets/outlined_text.dart"; +import "helper_widgets/highlighted_text.dart"; +import "helper_widgets/animated_thought_bubble.dart"; +import "helper_widgets/animated_sprite.dart"; +import "asset_handler.dart"; +import "enclose_grid_tile.dart"; + +enum EncloseMoosePlaceholderReason { + loading, + noLevels, + connectionError +} + +class EncloseMooseGamePlaceholderPage extends StatefulWidget { + const EncloseMooseGamePlaceholderPage({ + super.key, + required this.reason + }); + + final EncloseMoosePlaceholderReason reason; + + @override + _EncloseMooseGamePlaceholderState createState() => _EncloseMooseGamePlaceholderState(); +} + +class _EncloseMooseGamePlaceholderState extends State with TickerProviderStateMixin { + final _idleDuration = const Duration(milliseconds: 1000); + late final _idleController = AnimationController(vsync: this) + ..repeat( + reverse: false, + period: _idleDuration + ); + + @override + void dispose() { + _idleController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: WigglingWidget( + controller: _idleController, + child: const BackButton( + color: Colors.white + ) + ), + + title: Text.rich( + TextSpan( + children: "enclose.moose".characters.map((char) => WidgetSpan( + child: WigglingWidget( + controller: _idleController, + child: OutlinedText( + text: char, + style: const TextStyle( + color: Colors.white, + fontSize: 45, + fontFamily: "Schoolbell" + ) + ) + ) + )).toList() + ), + ) + ), + body: Stack( + children: [ + SizedBox.expand( + // child: Image.asset( + // backgroundImage, + // fit: BoxFit.fill + // ) + child: Container( + color: Color(0xFF014421) + ) + ), + + Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(5), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + WigglingWidget( + controller: _idleController, + child: IconButton( + onPressed: null, + icon: const Icon(Icons.calendar_month_rounded), + disabledColor: Colors.white + ) + ), + + Visibility( + visible: false, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: IconButton( + onPressed: null, + icon: const Icon(Icons.leaderboard_outlined), + color: Colors.white + ) + ), + + const Spacer(), + + Row( + children: [ + WigglingWidget( + controller: _idleController, + child: IconButton( + onPressed: null, + icon: const Icon(Icons.chevron_left), + disabledColor: Colors.white + ) + ), + + WigglingWidget( + controller: _idleController, + child: OutlinedText( + text: "Day ???", + style: const TextStyle( + color: Colors.white, + fontSize: 25, + fontFamily: "Schoolbell" + ) + ) + ), + + WigglingWidget( + controller: _idleController, + child: IconButton( + onPressed: null, + icon: const Icon(Icons.chevron_right), + disabledColor: Colors.white + ) + ), + ] + ), + + Spacer(), + + WigglingWidget( + controller: _idleController, + child: IconButton( + onPressed: null, + icon: const Icon(Icons.restart_alt_outlined), + disabledColor: Colors.white + ) + ), + + WigglingWidget( + controller: _idleController, + child: IconButton( + onPressed: () { + showDialog(context: context, builder: _buildHelpDialog()); + }, + icon: const Icon(Icons.question_mark_outlined), + color: Colors.white + ) + ), + ] + ), + + WigglingWidget( + controller: _idleController, + child: OutlinedText( + text: "???", + style: const TextStyle( + color: Colors.yellow, + fontSize: 20, + fontFamily: "Schoolbell" + ), + textAlign: TextAlign.center, + ) + ), + ] + ) + ), + + Expanded( + child: widget.reason == EncloseMoosePlaceholderReason.loading + ? Center( + child: CircularProgressIndicator( + color: Colors.white + ) + ) + : Stack( + clipBehavior: Clip.none, + children: [ + Center( + child: AnimatedSprite( + vsync: this, + idleFrames: AssetHandler.getIdleFrames(EncloseGridCellType.moose), + idleController: _idleController + ) + ), + + Center( + child: Transform.translate( + offset: const Offset(-15, -115), + child: SizedBox( + width: 200, + child: AnimatedThoughtBubble( + text: widget.reason == EncloseMoosePlaceholderReason.noLevels + ? "No levels yet. Check back in later.\n\nUntil then, I will roam free!!! (:" + : "There was an error getting the levels. Check back in later.\n\nDo I even exist outside of these levels? );", + color: Colors.white.withAlpha(180), + idleController: _idleController + ) + ) + ) + ), + ] + ), + + // child: WigglingWidget( + // controller: _idleController, + // child: widget.noLevels + // ? WigglingWidget( + // controller: _idleController, + // child: OutlinedText( + // text: "No levels yet. Check back in later!", + // style: const TextStyle( + // color: Colors.white, + // fontSize: 25, + // fontFamily: "Schoolbell" + // ) + // ) + // ) + // : CircularProgressIndicator( + // color: Colors.white, + // ), + // ), + ), + + SafeArea( + child: Padding( + padding: const EdgeInsets.only(right: 15, left: 15, top: 5), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + WigglingWidget( + controller: _idleController, + child: HighlightedText( + child: OutlinedText( + text: "Walls: ?/?", + style: const TextStyle( + fontSize: 25, + fontFamily: "Schoolbell" + ) + ) + ) + ), + + Spacer(), + + Visibility( + visible: false, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: TextButton( + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ) + ), + onPressed: null, + child: const OutlinedText( + text: "Submit", + style: TextStyle( + color: Colors.white, + fontSize: 25, + fontFamily: "Schoolbell" + ) + ) + ) + ), + + Spacer(), + + WigglingWidget( + controller: _idleController, + child: HighlightedText( + child: OutlinedText( + text: "Score: ?", + style: const TextStyle( + fontSize: 25, + fontFamily: "Schoolbell" + ) + ) + ) + ), + ] + ), + + Visibility( + visible: false, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + OutlinedButton.icon( + onPressed: null, + style: OutlinedButton.styleFrom( + backgroundColor: Color(0xFF15542D).withAlpha(200), // Colors.black.withAlpha(55), + visualDensity: VisualDensity.compact + ), + label: OutlinedText( + text: "Optimal: ?", + style: const TextStyle( + // color: Colors.white, + fontSize: 20, + fontFamily: "Schoolbell" + ) + ), + icon: const Icon(Icons.star_sharp) + ), + + OutlinedButton.icon( + onPressed: null, + style: OutlinedButton.styleFrom( + backgroundColor: Color(0xFF15542D).withAlpha(200), // Colors.black.withAlpha(55), + visualDensity: VisualDensity.compact + ), + label: OutlinedText( + text: "Your solution: ?", + style: const TextStyle( + // color: Colors.white, + fontSize: 20, + fontFamily: "Schoolbell" + ) + ), + icon: const Icon(Icons.keyboard_return_outlined) + ), + ] + ) + ), + ] + ) + ) + ) + ] + ) + ] + ) + ); + } + + Widget Function(BuildContext) _buildHelpDialog() { + return (context) => const SimpleDialog( + contentPadding: EdgeInsets.all(16), + title: Text( + "How to play", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 25, + fontFamily: "Schoolbell" + ) + ), + children: [ + Text( + "The objective is to build an enclosure for the moose by placing walls (tapping) on the grid. The moose can not move over the water. The more grass tiles in your enclosure, the higher your score. You only have a limited amount of walls so use them wisely.", + style: TextStyle( + fontSize: 18, + fontFamily: "Schoolbell" + ) + ), + ] + ); + } +} diff --git a/lib/screens/games/enclose_moose/enclose_moose_page.dart b/lib/screens/games/enclose_moose/enclose_moose_page.dart new file mode 100644 index 00000000..689e630b --- /dev/null +++ b/lib/screens/games/enclose_moose/enclose_moose_page.dart @@ -0,0 +1,98 @@ +import "package:flutter/material.dart"; +import "package:api_client/api_client.dart"; +import "package:fsek_mobile/services/api.service.dart"; +import "package:dio/dio.dart"; +import "asset_handler.dart"; +import "enclose_moose_level_placeholder_page.dart"; +import "enclose_moose_level_page.dart"; + +class EncloseMoosePage extends StatefulWidget { + @override + _EncloseMooseState createState() => _EncloseMooseState(); +} + +class _EncloseMooseState extends State with TickerProviderStateMixin { + List? _levels; + bool _exceptionOccurred = false; + + late final PageController? _pageController; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _precacheAllAssets(); + }); + + _fetchLevels(); + } + + void _precacheAllAssets() { + for (final frame in AssetHandler.getAllFrames()) { + precacheImage(AssetImage(frame), context); + } + } + + Future _fetchLevels() async { + try { + final response = await ApiService.apiClient.getEncloseMooseApi().encloseMooseGetAllLevels(); + + if (!mounted) return; + + setState(() { + _levels = response.data! + .where((level) => level.dayIndex != null) + .followedBy(response.data! + .where((level) => level.dayIndex == null)) + .toList(); + }); + + final lastDailyIndex = _levels!.lastIndexWhere((level) => level.dayIndex != null); + _pageController = PageController(initialPage: lastDailyIndex != -1 ? lastDailyIndex : 0); + } on DioException { // catch (e) { + setState(() { + _exceptionOccurred = true; + }); + + _pageController = null; // needs to be initialized + } + } + + @override + void dispose() { + _pageController?.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (_levels == null) { + if (_exceptionOccurred) { + return EncloseMooseGamePlaceholderPage(reason: .connectionError); + } else { + return EncloseMooseGamePlaceholderPage(reason: .loading); + } + } else if (_levels!.isEmpty) { + return EncloseMooseGamePlaceholderPage(reason: .noLevels); + } + + return PageView.builder( + controller: _pageController!, + allowImplicitScrolling: true, + physics: const NeverScrollableScrollPhysics(), + // scrollCacheExtent: ScrollCacheExtent.viewport(_levels.length.toDouble()), // keeps all pages alive. Too expensive, consider only saving game state (sharedPreferences) and then using that + itemCount: _levels!.length, + itemBuilder: (context, index) { + final level = _levels![index]; + + return EncloseMooseLevelPage( + pageController: _pageController, + level: level, + availableLevels: _levels! + ); + } + ); + } +} diff --git a/lib/screens/games/enclose_moose/helper_widgets/animated_smooth_arrow.dart b/lib/screens/games/enclose_moose/helper_widgets/animated_smooth_arrow.dart new file mode 100644 index 00000000..e8498275 --- /dev/null +++ b/lib/screens/games/enclose_moose/helper_widgets/animated_smooth_arrow.dart @@ -0,0 +1,134 @@ +import "package:flutter/material.dart"; +import "smooth_arrow_painter.dart"; + +class AnimatedSmoothArrow extends StatefulWidget { + const AnimatedSmoothArrow({ + super.key, + this.isVisible = true, + this.onDisappear, + this.vsync, + this.duration = const Duration(milliseconds: 500), + this.curve = Curves.easeInOut, + required this.flatIndices, + required this.gridWidth, + required this.cellSize, + this.extraLength = 0, + this.extraDirection, + this.showArrowHead = true, + this.lineColor = Colors.white, + this.idleController, + this.frequency + }); + + final bool isVisible; + final Function? onDisappear; + final TickerProvider? vsync; + final Duration duration; + final Curve curve; + final List flatIndices; + final int gridWidth; + final double cellSize; + final double extraLength; + final AxisDirection? extraDirection; + final Color lineColor; + final bool showArrowHead; + final AnimationController? idleController; + final double? frequency; + + @override + State createState() => _AnimatedSmoothArrowState(); +} + +class _AnimatedSmoothArrowState extends State with SingleTickerProviderStateMixin { + late final AnimationController _controller = AnimationController( + vsync: widget.vsync ?? this, + duration: widget.duration + ); + + @override + void initState() { + super.initState(); + + if (widget.isVisible) { + _animate(reverse: false); + } + } + + @override + void didUpdateWidget(covariant AnimatedSmoothArrow oldWidget) { + super.didUpdateWidget(oldWidget); + + if (!oldWidget.isVisible && widget.isVisible) { + _animate(reverse: false); + } else if (oldWidget.isVisible && !widget.isVisible) { + _animate(reverse: true); + } + } + + Future _animate({required bool reverse}) async { + if (!reverse) { + await _controller.forward(); + } else { + await _controller.reverse(); + + if (mounted && widget.onDisappear != null) { + widget.onDisappear!(); + } + } + } + + @override + void dispose() { + _controller.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.idleController == null) { + return AnimatedBuilder( + animation: _controller, + builder: (_, __) { + return CustomPaint( + painter: SmoothArrowPainter( + flatIndices: widget.flatIndices, + gridWidth: widget.gridWidth, + cellSize: widget.cellSize, + extraLength: widget.extraLength, + extraDirection: widget.extraDirection, + lineColor: widget.lineColor, + showArrowHead: widget.showArrowHead, + animationProgress: widget.curve.transform(_controller.value) + ), + ); + }, + ); + } + + return AnimatedBuilder( + animation: widget.idleController!, + builder: (context, child) { + return AnimatedBuilder( + animation: _controller, + builder: (_, __) { + return CustomPaint( + painter: SmoothArrowPainter( + flatIndices: widget.flatIndices, + gridWidth: widget.gridWidth, + cellSize: widget.cellSize, + extraLength: widget.extraLength, + extraDirection: widget.extraDirection, + lineColor: widget.lineColor, + showArrowHead: widget.showArrowHead, + animationProgress: widget.curve.transform(_controller.value), + idleProgress: widget.idleController!.value, + frequency: widget.frequency + ), + ); + }, + ); + } + ); + } +} diff --git a/lib/screens/games/enclose_moose/helper_widgets/animated_sprite.dart b/lib/screens/games/enclose_moose/helper_widgets/animated_sprite.dart new file mode 100644 index 00000000..4529ffd2 --- /dev/null +++ b/lib/screens/games/enclose_moose/helper_widgets/animated_sprite.dart @@ -0,0 +1,147 @@ +import "package:flutter/material.dart"; + +class AnimatedSprite extends StatefulWidget { + const AnimatedSprite({ + super.key, + this.vsync, + this.shouldAnimate = true, + this.animationCurve = Curves.linear, + this.animationFrames, + this.animationFrameDuration, + this.animationReverseFrameDuration, + this.animationWaitTime, + this.animationReverseWaitTime, + this.animationWaitFrames, + this.animationReverseWaitFrames, + this.shouldIdle = true, + this.idleFrames, + this.startIdleFrames, + this.endIdleFrames, + this.idleController + }); + + final TickerProvider? vsync; + final bool shouldAnimate; + final Curve animationCurve; + final List? animationFrames; + final Duration? animationFrameDuration; + final Duration? animationReverseFrameDuration; + final double? animationWaitFrames; + final double? animationReverseWaitFrames; + final Duration? animationWaitTime; + final Duration? animationReverseWaitTime; + final bool shouldIdle; + final List? idleFrames; + final List? startIdleFrames; + final List? endIdleFrames; + final AnimationController? idleController; + + @override + State createState() => _AnimatedSpriteState(); +} + +class _AnimatedSpriteState extends State with SingleTickerProviderStateMixin { + Animation _frameAnimation = const AlwaysStoppedAnimation(0); + late final AnimationController _controller = AnimationController(vsync: widget.vsync ?? this); + + @override + void initState() { + super.initState(); + + if (widget.shouldAnimate) { + _animate(widget, reverse: false); + } + } + + @override + void didUpdateWidget(covariant AnimatedSprite oldWidget) { + super.didUpdateWidget(oldWidget); + + if (!oldWidget.shouldAnimate && widget.shouldAnimate) { + _animate(widget, reverse: false); + } else if (oldWidget.shouldAnimate && !widget.shouldAnimate) { + _animate(oldWidget, reverse: true); + } + } + + void _animate(AnimatedSprite usedWidget, {required bool reverse}) { + if (widget.animationFrames == null) return; + + const defaultFrameDuration = const Duration(milliseconds: 40); + + final usedFrameDuration = usedWidget.animationFrameDuration ?? usedWidget.animationReverseFrameDuration ?? defaultFrameDuration; + final usedWaitFrames = usedWidget.animationWaitFrames ?? (usedWidget.animationWaitTime?.inMilliseconds ?? 0) / usedFrameDuration.inMilliseconds; + + final usedReverseFrameDuration = usedWidget.animationReverseFrameDuration ?? usedWidget.animationFrameDuration ?? defaultFrameDuration; + final usedReverseWaitFrames = usedWidget.animationReverseWaitFrames ?? (usedWidget.animationReverseWaitTime?.inMilliseconds ?? 0) / usedReverseFrameDuration.inMilliseconds; + + final begin = -usedWaitFrames; + final end = usedWidget.animationFrames!.length + usedReverseWaitFrames; + + _controller.duration = usedFrameDuration * (end - begin); + _controller.reverseDuration = usedReverseFrameDuration * (end - begin);; + + _frameAnimation = Tween( + begin: begin, + end: end + ).animate(CurvedAnimation(parent: _controller, curve: widget.animationCurve)); + + if (!reverse) { + _controller.forward(); + } else { + _controller.reverse(); + } + } + + @override + void dispose() { + _controller.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _frameAnimation, + builder: (context, child) { + final animationFramesLength = widget.animationFrames?.length ?? 0; + + final frame = _frameAnimation.value; + if (!widget.shouldAnimate && !widget.shouldIdle && (frame < 0 || frame > animationFramesLength - 1)) return const SizedBox.shrink(); + + if (widget.shouldIdle && widget.idleController != null && (widget.idleFrames != null || widget.startIdleFrames != null || widget.endIdleFrames != null)) { + if (frame <= 0 || frame >= animationFramesLength) { + final usedStartIdleFrames = widget.startIdleFrames ?? widget.idleFrames; + final usedEndIdleFrames = widget.endIdleFrames ?? widget.idleFrames; + final usedIdleFrames = (frame <= 0 && !(animationFramesLength == 0 && widget.shouldAnimate)) ? usedStartIdleFrames : usedEndIdleFrames; + if (usedIdleFrames?.isEmpty ?? true) return const SizedBox.shrink(); + + return AnimatedBuilder( + animation: widget.idleController!, + builder: (context, child) { + final idleFrame = (widget.idleController!.value * usedIdleFrames!.length) + .floor() + .clamp(0, usedIdleFrames.length - 1); + + return Image.asset( + usedIdleFrames[idleFrame], + fit: BoxFit.fill, + filterQuality: FilterQuality.none + ); + } + ); + } + } + + if (widget.animationFrames == null) return const SizedBox.shrink(); + + return Image.asset( + widget.animationFrames![frame.toInt().clamp(0, widget.animationFrames!.length - 1)], + fit: BoxFit.fill, + filterQuality: FilterQuality.none + ); + }, + ); + } +} diff --git a/lib/screens/games/enclose_moose/helper_widgets/animated_thought_bubble.dart b/lib/screens/games/enclose_moose/helper_widgets/animated_thought_bubble.dart new file mode 100644 index 00000000..037f2d26 --- /dev/null +++ b/lib/screens/games/enclose_moose/helper_widgets/animated_thought_bubble.dart @@ -0,0 +1,191 @@ +import "package:flutter/material.dart"; +import "wiggling_widget.dart"; +import "cloud_painter.dart"; + +class AnimatedThoughtBubble extends StatefulWidget { + final String text; + + const AnimatedThoughtBubble({ + super.key, + this.isVisible = true, + this.onDisappear, + this.vsync, + this.duration = const Duration(milliseconds: 500), + this.curve = Curves.easeOutBack, + required this.text, + this.minPuffs = 7, + this.puffVariance = 5, + this.minAngular = 0.15, + this.angularVariance = 0.2, + this.minRadial = 0, + this.radialVariance = 0.15, + this.bulgeFactor = 0.3, + this.color = Colors.white, + this.idleController, + this.frequency = 2 + }); + + final bool isVisible; + final Function? onDisappear; + final TickerProvider? vsync; + final Duration duration; + final Curve curve; + final int minPuffs; + final int puffVariance; + final double minAngular; + final double angularVariance; + final double minRadial; + final double radialVariance; + final double bulgeFactor; + final Color color; + final AnimationController? idleController; + final double frequency; + + @override + State createState() => _AnimatedThoughtBubbleState(); +} + +class _AnimatedThoughtBubbleState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller = AnimationController( + vsync: widget.vsync ?? this, + duration: widget.duration + ); + late Animation _dot1Anim = CurvedAnimation( + parent: _controller, + curve: Interval(0.0, 0.3, curve: widget.curve) + ); + late Animation _dot2Anim = CurvedAnimation( + parent: _controller, + curve: Interval(0.3, 0.6, curve: widget.curve) + ); + late Animation _cloudAnim = CurvedAnimation( + parent: _controller, + curve: Interval(0.6, 1.0, curve: widget.curve) + ); + + @override + void initState() { + super.initState(); + + if (widget.isVisible) { + _animate(reverse: false); + } + } + + @override + void didUpdateWidget(covariant AnimatedThoughtBubble oldWidget) { + super.didUpdateWidget(oldWidget); + + if (!oldWidget.isVisible && widget.isVisible) { + _animate(reverse: false); + } else if (oldWidget.isVisible && !widget.isVisible) { + _animate(reverse: true); + } + } + + Future _animate({required bool reverse}) async { + if (!reverse) { + await _controller.forward(); + } else { + await _controller.reverse(); + + if (mounted && widget.onDisappear != null) { + widget.onDisappear!(); + } + } + } + + @override + void dispose() { + _controller.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ScaleTransition( + scale: _cloudAnim, + child: FadeTransition( + opacity: _cloudAnim, + child: WigglingWidget( // The wiggling makes the cloud regenerate (because we don't set a key) + controller: widget.idleController, + frequency: widget.frequency, + child: CustomPaint( + painter: CloudPainter( + minPuffs: widget.minPuffs, + puffVariance: widget.puffVariance, + minAngular: widget.minAngular, + angularVariance: widget.angularVariance, + minRadial: widget.minRadial, + radialVariance: widget.radialVariance, + bulgeFactor: widget.bulgeFactor, + color: widget.color + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: Text( + widget.text, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 14, + fontFamily: "Schoolbell", + color: Colors.black + ) + ) + ) + ) + ) + ) + ), + + Transform.translate( + offset: Offset(-5, 30), + child: ScaleTransition( + scale: _dot1Anim, + child: FadeTransition( + opacity: _dot1Anim, + child: WigglingWidget( + controller: widget.idleController, + frequency: widget.frequency, + child: Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: widget.color, + shape: BoxShape.circle + ) + ), + ) + ) + ) + ), + + Transform.translate( + offset: Offset(-13, 10), + child: ScaleTransition( + scale: _dot2Anim, + child: FadeTransition( + opacity: _dot2Anim, + child: WigglingWidget( + controller: widget.idleController, + frequency: widget.frequency, + child: Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: widget.color, + shape: BoxShape.circle + ) + ) + ) + ) + ) + ), + ] + ); + } +} diff --git a/lib/screens/games/enclose_moose/helper_widgets/cloud_painter.dart b/lib/screens/games/enclose_moose/helper_widgets/cloud_painter.dart new file mode 100644 index 00000000..ef9d0486 --- /dev/null +++ b/lib/screens/games/enclose_moose/helper_widgets/cloud_painter.dart @@ -0,0 +1,121 @@ +import "dart:math" as math; +import "package:flutter/material.dart"; + +class CloudPainter extends CustomPainter { + CloudPainter({ + this.minPuffs = 7, + this.puffVariance = 5, + this.minAngular = 0.15, + this.angularVariance = 0.2, + this.minRadial = 0, + this.radialVariance = 0.15, + this.bulgeFactor = 0.3, + this.color = Colors.white + }); + + final int minPuffs; + final int puffVariance; + final double minAngular; + final double angularVariance; + final double minRadial; + final double radialVariance; + final double bulgeFactor; + final Color color; + + final _rng = math.Random(); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width * 0.5, size.height * 0.5); + final a = size.width * 0.5; // could divide by sqrt(2) to ensure the bounds are enclosed within the ellipse but then the size becomes more unpredictable and harder for the dots + final b = size.height * 0.5; + + final numPuffs = minPuffs + _rng.nextInt(puffVariance); + + // Create lookup table for ellipse arc lengths by angle (index) + const tableResolution = 360; + const stepAngle = 2 * math.pi / tableResolution; + + final List arcLengthTable = [0.0]; + double accumulatedLength = 0.0; + Offset prevPos = Offset(center.dx + a, center.dy); + for (int i = 0; i < tableResolution; i++) { + final angle = i * stepAngle; + final currentPos = Offset(center.dx + a * math.cos(angle), center.dy + b * math.sin(angle)); + accumulatedLength += (currentPos - prevPos).distance; + arcLengthTable.add(accumulatedLength); + prevPos = currentPos; + } + + // Sample (jittered) points along the ellipse, spaced by arcLengths from lookup table + final distanceStep = accumulatedLength / numPuffs; + final List points = []; + for (int i = 0; i < numPuffs; i++) { + final aJitter = minAngular + _rng.nextDouble() * angularVariance; + final targetDistance = (i + aJitter) * distanceStep; + final targetIndex = arcLengthTable.indexWhere((length) => length > targetDistance) - 1; + + // Linearly interpolate targetDistance angle from lookup table + final t = (targetDistance - arcLengthTable[targetIndex]) / (arcLengthTable[targetIndex + 1] - arcLengthTable[targetIndex]); + final angle = (targetIndex + t) * stepAngle; + + final rJitter = minRadial + _rng.nextDouble() * radialVariance; + final rx = a * (1 + rJitter); + final ry = b * (1 + rJitter); + + points.add(Offset( + center.dx + rx * math.cos(angle), + center.dy + ry * math.sin(angle), + )); + } + + final path = Path(); + path.moveTo(points[0].dx, points[0].dy); + + // Draw cubic Bézier curves between sampled points + for (int i = 0; i < numPuffs; i++) { + final p1 = points[i]; + final p2 = points[(i + 1) % numPuffs]; + + final dx = p2.dx - p1.dx; + final dy = p2.dy - p1.dy; + final segLen = math.sqrt(dx * dx + dy * dy); + + // Control points on 1/4 and 3/4 of the arc length between points + final base1 = Offset(p1.dx + dx * 0.25, p1.dy + dy * 0.25); + final base2 = Offset(p1.dx + dx * 0.75, p1.dy + dy * 0.75); + + // Unit normal (outward) to ellipse surface + final normX = dy / segLen; + final normY = -dx / segLen; + + final bulge = segLen * bulgeFactor; // Bulge out proportional to segLen + final controlPoint1 = Offset( + base1.dx + normX * bulge, + base1.dy + normY * bulge, + ); + final controlPoint2 = Offset( + base2.dx + normX * bulge, + base2.dy + normY * bulge, + ); + + path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx, controlPoint2.dy, p2.dx, p2.dy); + } + + final cloudPaint = Paint() + ..color = color + ..style = PaintingStyle.fill; + canvas.drawPath(path, cloudPaint); + + // final largeCenter = Offset(size.width * 0.55, size.height + 25); + // canvas.drawCircle(largeCenter, 7, cloudPaint); + + // final smallCenter = Offset(size.width * 0.5, size.height + 40); + // canvas.drawCircle(smallCenter, 4, cloudPaint); + } + + @override + bool shouldRepaint(covariant CloudPainter oldDelegate) { + return true; + } +} diff --git a/lib/screens/games/enclose_moose/helper_widgets/highlighted_text.dart b/lib/screens/games/enclose_moose/helper_widgets/highlighted_text.dart new file mode 100644 index 00000000..f20c9b40 --- /dev/null +++ b/lib/screens/games/enclose_moose/helper_widgets/highlighted_text.dart @@ -0,0 +1,42 @@ +import "package:flutter/material.dart"; + +class HighlightedText extends StatefulWidget { + const HighlightedText({ + super.key, + required this.child, + this.showHighlight = false, + this.highlightShadow = const Shadow( + color: Colors.white, + blurRadius: 40 + ), + this.duration = const Duration(seconds: 1), + this.curve = Curves.easeOut + }); + + final Widget child; + final bool showHighlight; + final Shadow highlightShadow; + final Duration duration; + final Curve curve; + + @override + State createState() => _HighlightedTextState(); +} + +class _HighlightedTextState extends State { + @override + Widget build(BuildContext context) { + return AnimatedDefaultTextStyle( + style: widget.showHighlight + ? TextStyle( + shadows: [ + widget.highlightShadow + ] + ) + : const TextStyle(), + duration: widget.showHighlight ? Duration.zero : widget.duration, + curve: widget.curve, + child: widget.child + ); + } +} diff --git a/lib/screens/games/enclose_moose/helper_widgets/outlined_text.dart b/lib/screens/games/enclose_moose/helper_widgets/outlined_text.dart new file mode 100644 index 00000000..bd33cc0e --- /dev/null +++ b/lib/screens/games/enclose_moose/helper_widgets/outlined_text.dart @@ -0,0 +1,47 @@ +import "package:flutter/material.dart"; + +class OutlinedText extends StatefulWidget { + const OutlinedText({ + super.key, + required this.text, + this.style, + this.strokeWidth = 1.5, + this.strokeColor = Colors.black, + this.textAlign + }); + + final String text; + final TextStyle? style; + final double strokeWidth; + final Color strokeColor; + final TextAlign? textAlign; + + @override + State createState() => _OutlinedTextState(); +} + +class _OutlinedTextState extends State { + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Text( + widget.text, + style: TextStyle( + foreground: Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = widget.strokeWidth + ..color = widget.strokeColor + ).merge(widget.style), + textAlign: widget.textAlign + ), + + Text( + widget.text, + style: widget.style, + textAlign: widget.textAlign + ), + ] + ); + } +} diff --git a/lib/screens/games/enclose_moose/helper_widgets/smooth_arrow_painter.dart b/lib/screens/games/enclose_moose/helper_widgets/smooth_arrow_painter.dart new file mode 100644 index 00000000..55961400 --- /dev/null +++ b/lib/screens/games/enclose_moose/helper_widgets/smooth_arrow_painter.dart @@ -0,0 +1,194 @@ +import "dart:math" as math; +import "package:flutter/material.dart"; + +class SmoothArrowPainter extends CustomPainter { + const SmoothArrowPainter({ + required this.flatIndices, + required this.gridWidth, + required this.cellSize, + this.extraLength = 0, + this.extraDirection, + required this.animationProgress, + this.showArrowHead = true, + this.lineColor = Colors.white, + this.idleProgress, + this.frequency, + this.speed = 1 + }); + + final List flatIndices; + final int gridWidth; + final double cellSize; + final double extraLength; + final AxisDirection? extraDirection; + final double animationProgress; + final bool showArrowHead; + final Color lineColor; + final double? idleProgress; + final double? frequency; + final double speed; + + @override + void paint(Canvas canvas, Size size) { + if (flatIndices.length == 0) return; + + final paint = Paint() + ..color = lineColor + ..strokeWidth = cellSize / 15 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round; + + final path = _buildSmoothPath(flatIndices); + _drawAnimatedDashedPath(canvas, path, paint, animationProgress, idleProgress ?? 0, frequency); + + if (showArrowHead && animationProgress > 0) { + final metrics = path.computeMetrics().toList(); + final totalLength = metrics.fold( + 0.0, + (sum, metric) => sum + metric.length, + ); + double remaining = totalLength * animationProgress; + + for (final metric in metrics) { + if (remaining <= metric.length) { + final tangent = metric.getTangentForOffset(remaining); + if (tangent == null) return; + + _drawArrowHead(canvas, paint, tangent.position, -tangent.angle); // for some reason angle needs to be negated + + return; + } + + remaining -= metric.length; + } + } + } + + Offset _getCoords(int flatIndex, int gridWidth, double cellSize) { + final rowIndex = flatIndex ~/ gridWidth; + final columnIndex = flatIndex % gridWidth; + + return Offset( + (columnIndex + 0.5) * cellSize, + (rowIndex + 0.5) * cellSize + ); + } + + Path _buildSmoothPath(List flatIndices) { + final points = flatIndices.map((flatIndex) => _getCoords(flatIndex, gridWidth, cellSize)).toList(); + + final path = Path(); + path.moveTo(points.first.dx, points.first.dy); + + for (int i = 1; i < points.length - 1; i++) { + final rowIndexDiff = (flatIndices[i + 1] ~/ gridWidth - flatIndices[i] ~/ gridWidth).abs(); + final columnIndexDiff = (flatIndices[i + 1] % gridWidth - flatIndices[i] % gridWidth).abs(); + if (rowIndexDiff + columnIndexDiff != 1) { + // Avoid discontinuities (portals) + path.moveTo(points[i + 1].dx, points[i + 1].dy); + i++; // Since we use i + 1 here, skip that one. + + continue; + } + + final prev = points[i - 1]; + final current = points[i]; + final next = points[i + 1]; + + final incoming = current - prev; + final outgoing = next - current; + + final incomingLength = incoming.distance; + final outgoingLength = outgoing.distance; + + final radius = math.min(incomingLength, outgoingLength) / 2; + + final start = current - incoming / incomingLength * radius; + final end = current + outgoing / outgoingLength * radius; + + path.lineTo(start.dx, start.dy); + path.quadraticBezierTo(current.dx, current.dy, end.dx, end.dy); + } + + path.lineTo(points.last.dx, points.last.dy); + + // if (path.computeMetrics().length == 0) return path; + + final metric = path.computeMetrics().last; + final tangent = metric.getTangentForOffset(metric.length); + final directionAngleDict = { + AxisDirection.up: math.pi / 2, + AxisDirection.down: -math.pi / 2, + AxisDirection.left: math.pi, + AxisDirection.right: 0 + }; + final usedAngle = directionAngleDict[extraDirection] ?? tangent!.angle; + path..lineTo( + points.last.dx + extraLength * math.cos(usedAngle), + points.last.dy - extraLength * math.sin(usedAngle) + ); + + return path; + } + + void _drawArrowHead(Canvas canvas, Paint paint, Offset tip, double angle) { + final arrowLength = cellSize / 2; + const spread = math.pi / 6; + + final path = Path() + ..moveTo(tip.dx, tip.dy) + ..lineTo( + tip.dx - arrowLength * math.cos(angle - spread), + tip.dy - arrowLength * math.sin(angle - spread), + ) + ..moveTo(tip.dx, tip.dy) + ..lineTo( + tip.dx - arrowLength * math.cos(angle + spread), + tip.dy - arrowLength * math.sin(angle + spread), + ); + + canvas.drawPath(path, paint); + } + + void _drawAnimatedDashedPath(Canvas canvas, Path path, Paint paint, double animationProgress, double idleProgress, double? frequency) { + final dash = cellSize * 3 / 5; + final gap = cellSize * 2 / 5; + + final startDistance; + if (frequency == null) { + startDistance = speed * (idleProgress - 1) * (dash + gap); + } else { + final step = (idleProgress * frequency).floor(); + startDistance = (step - frequency) * (dash + gap) / frequency; + } + + final metrics = path.computeMetrics().toList(); + final totalLength = metrics.fold( + 0.0, + (sum, metric) => sum + metric.length, + ); + double remaining = totalLength * animationProgress; + for (final metric in metrics) { + if (remaining <= 0) break; + + final visibleLength = math.min(remaining, metric.length); + + double distance = startDistance; // consider making this discrete instead to get stuttering animations like the rest of the idle animations + while (distance < visibleLength) { + final start = distance; + final end = math.min(start + dash, visibleLength); + + canvas.drawPath(metric.extractPath(start, end), paint); + distance += dash + gap; + } + + remaining -= visibleLength; + } + } + + @override + bool shouldRepaint(covariant SmoothArrowPainter oldDelegate) { + return oldDelegate.animationProgress != animationProgress || oldDelegate.idleProgress != idleProgress; + } +} diff --git a/lib/screens/games/enclose_moose/helper_widgets/submissions_bar_chart.dart b/lib/screens/games/enclose_moose/helper_widgets/submissions_bar_chart.dart new file mode 100644 index 00000000..7c4df20b --- /dev/null +++ b/lib/screens/games/enclose_moose/helper_widgets/submissions_bar_chart.dart @@ -0,0 +1,234 @@ +import "dart:math" as math; +import "package:flutter/material.dart"; +import "package:fl_chart/fl_chart.dart"; + +class SubmissionsBarChart extends StatefulWidget { + const SubmissionsBarChart({ + required this.scoreDistribution, + required this.playerScore, + required this.optimalScore + }); + + final Map scoreDistribution; + final int playerScore; + final int optimalScore; + + @override + _SubmissionsBarChartState createState() => _SubmissionsBarChartState(); +} + +class _SubmissionsBarChartState extends State with SingleTickerProviderStateMixin { + late final entries = widget.scoreDistribution.entries.toList() + ..sort((a, b) => a.key.compareTo(b.key)); + + late final minScore = widget.scoreDistribution.keys.reduce(math.min); + late final maxOccurence = widget.scoreDistribution.values.reduce(math.max); + late final amountBars = widget.optimalScore - minScore + 1; + + int? _shownTooltip; + Future? _hideTooltipFuture; + late final _tooltipAnimationController = AnimationController( + vsync: this, + duration: Duration.zero, + reverseDuration: const Duration(milliseconds: 200) + ); + + final _chartTransformationController = TransformationController(); + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + _startAnimation(widget.playerScore); + } + + @override + void dispose() { + _tooltipAnimationController.dispose(); + _chartTransformationController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final barColorDict = { + widget.playerScore: Theme.of(context).primaryColor, // A bit weird on themeN where primaryColor is green + widget.optimalScore: Colors.green[800] + }; + const defaultBarColor = Colors.blue; + + final screenWidth = MediaQuery.of(context).size.width; + + return LayoutBuilder( + builder: (context, constraints) => ListenableBuilder( + listenable: Listenable.merge([ // Could split the animation into an animationController, but it works the same + _tooltipAnimationController, // This is pretty expensive, making the scaling a bit slow. The barGroups regenerate every time (even though the data is constant) because the barWidth changes + _chartTransformationController + ]), + + builder: (context, child) { + final scale = _chartTransformationController.value.getMaxScaleOnAxis(); + final barWidth = scale * (constraints.maxWidth * 0.7 - 75) / amountBars; + final currentAlpha = (255 * _tooltipAnimationController.value).toInt(); + + return BarChart( + BarChartData( + borderData: FlBorderData(show: false), + gridData: const FlGridData(show: false), + barTouchData: BarTouchData( + allowTouchBarBackDraw: true, + enabled: true, + touchTooltipData: BarTouchTooltipData( + maxContentWidth: screenWidth, + tooltipBorderRadius: const BorderRadius.all( + Radius.circular(12) + ), + tooltipBorder: BorderSide(color: Colors.black.withAlpha(currentAlpha)), + getTooltipColor: (group) => (barColorDict[group.x] ?? defaultBarColor).withAlpha(currentAlpha), + getTooltipItem: (group, groupIndex, rod, rodIndex) { + return BarTooltipItem( + "Score: ${entries[groupIndex].key}\nOccurrences: ${rod.toY.toInt()}", + TextStyle( + color: Colors.white.withAlpha(currentAlpha), + fontSize: 18, + fontFamily: "Schoolbell" + ) + ); + } + ), + handleBuiltInTouches: false, + touchCallback: (FlTouchEvent event, barTouchResponse) { + // event is FlPanStartEvent || event is FlPanUpdateEvent || event is FlPanEndEvent || // Wish I could use these when the graph is not zoomed in but FlTransformationConfig eats all pan gestures even if pan is disabled + if (!(event is FlTapUpEvent || event is FlLongPressStart || event is FlLongPressMoveUpdate || event is FlLongPressEnd)) return; + if (barTouchResponse == null || barTouchResponse.spot == null) return; + + final tappedBar = barTouchResponse.spot!.touchedBarGroup.x; + _startAnimation(tappedBar); + }, + ), + titlesData: FlTitlesData( + show: true, + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false) + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false) + ), + bottomTitles: AxisTitles( + axisNameSize: 30, + axisNameWidget: const Text( + "Score", + style: TextStyle( + fontSize: 20, + fontFamily: "Schoolbell" + ) + ), + sideTitles: SideTitles( + showTitles: true, + interval: scale == 1 ? 1 : amountBars / scale, + reservedSize: 28, + getTitlesWidget: (value, meta) { + if (scale == 1 && value != minScore && value != widget.optimalScore) return const SizedBox.shrink(); // Because the axis is categorical we can't use interval and instead have to use this + + final usedInterval = amountBars ~/ scale; // Evenly space labels so that at least one is always showing + if ((value.toInt() - minScore) % usedInterval != 0 && value != minScore && value != widget.optimalScore) return const SizedBox.shrink(); + + return SideTitleWidget( + space: 5, + meta: meta, + child: Text( + value.toInt().toString(), + style: const TextStyle( + fontSize: 16, + fontFamily: "Schoolbell" + ) + ) + ); + } + ) + ), + leftTitles: AxisTitles( + axisNameSize: 30, + axisNameWidget: const Text( + "Occurrence", + style: TextStyle( + fontSize: 20, + fontFamily: "Schoolbell" + ) + ), + sideTitles: SideTitles( + showTitles: true, + reservedSize: 28, + interval: maxOccurence.toDouble(), + getTitlesWidget: (value, meta) { + return SideTitleWidget( + space: 5, + meta: meta, + child: Text( + value.toInt().toString(), + style: const TextStyle( + fontSize: 16, + fontFamily: "Schoolbell" + ) + ) + ); + } + ) + ) + ), + barGroups: entries.map((entry) => BarChartGroupData( + showingTooltipIndicators: _shownTooltip == entry.key ? [0] : [], + x: entry.key, + barRods: [ + BarChartRodData( + toY: entry.value.toDouble(), + color: barColorDict[entry.key] ?? defaultBarColor, + width: barWidth, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(4) + ) + ), + ] + )).toList() + ), + transformationConfig: FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + minScale: 1, + maxScale: (amountBars / 5).ceilToDouble(), // Minimum of 5 bars in frame + transformationController: _chartTransformationController + ) + ); + } + ) + ); + } + + void _startAnimation(int bar, {Duration duration = const Duration(seconds: 4)}) async { // Tried with callCounter but this was just easier + _tooltipAnimationController.forward(from: _shownTooltip == bar ? null : 0); // start from zero if clicking on a new bar + _shownTooltip = bar; + + // _hideTooltipFuture = Future.delayed(duration, () async { + // if (!mounted) return; + + // await _tooltipAnimationController.reverse(); + // _shownTooltip = null; + // }); + + // Kind of a scuffed way to do it. If anyone has a better solution, please implement! + final tooltipFuture = Future.delayed(duration); + _hideTooltipFuture = tooltipFuture; + + await tooltipFuture; + if (!mounted) return; + if (_hideTooltipFuture != tooltipFuture) return; // A different bar has been clicked + + await _tooltipAnimationController.reverse(); + if (!mounted) return; + if (_hideTooltipFuture != tooltipFuture) return; // A different bar has been clicked during the fade-out + + setState(() { // we have to use setState since the tooltip is not showing anymore + _shownTooltip = null; + }); + } +} \ No newline at end of file diff --git a/lib/screens/games/enclose_moose/helper_widgets/wiggling_widget.dart b/lib/screens/games/enclose_moose/helper_widgets/wiggling_widget.dart new file mode 100644 index 00000000..f6de0fdb --- /dev/null +++ b/lib/screens/games/enclose_moose/helper_widgets/wiggling_widget.dart @@ -0,0 +1,62 @@ +import "dart:math" as math; +import "package:flutter/material.dart"; + +class WigglingWidget extends StatefulWidget { + const WigglingWidget({ + super.key, + required this.child, + required this.controller, + this.frequency = 2, + this.maxTranslate = const Offset(2, 1), + this.maxRotate = 1 + }); + + final Widget child; + final AnimationController? controller; + final double frequency; + final Offset maxTranslate; + final double maxRotate; + + @override + State createState() => _WigglingWidgetState(); +} + +class _WigglingWidgetState extends State { + final _random = math.Random(); + + Offset _currentOffset = Offset.zero; + double _currentRotation = 0; + + int _lastStep = -1; + + @override + Widget build(BuildContext context) { + if (widget.controller == null) { + return widget.child; + } + + return AnimatedBuilder( + animation: widget.controller!, + builder: (context, child) { + final step = (widget.controller!.value * widget.frequency).floor(); + + if (step != _lastStep) { + _currentOffset = widget.maxTranslate * (_random.nextDouble() * 2 - 1); + _currentRotation = (_random.nextDouble() * 2 - 1) * widget.maxRotate * math.pi / 180; + + _lastStep = step; + + // print("Updated! to $_currentOffset, $_currentRotation"); + } + + return Transform.translate( + offset: _currentOffset, + child: Transform.rotate( + angle: _currentRotation, + child: widget.child + ) + ); + }, + ); + } +} diff --git a/lib/screens/games/enclose_moose/tile_widget.dart b/lib/screens/games/enclose_moose/tile_widget.dart new file mode 100644 index 00000000..572e0bbb --- /dev/null +++ b/lib/screens/games/enclose_moose/tile_widget.dart @@ -0,0 +1,203 @@ +import "dart:math"; +import "package:flutter/material.dart"; +import "enclose_grid_tile.dart"; +import "helper_widgets/animated_sprite.dart"; +import "helper_widgets/wiggling_widget.dart"; +import "helper_widgets/outlined_text.dart"; +import "call_counter.dart"; +import "asset_handler.dart"; + +class TileWidget extends StatelessWidget { + TileWidget({ + super.key, + required this.tile, + required this.vsync, + required this.idleController, + required this.tileRandomFunction, + required this.onTap, + this.portalFilter, + this.neighboringWater, + this.tooltipCallCounter + }); + + final EncloseGridTile tile; + final TickerProvider vsync; + final AnimationController idleController; + final Random Function() tileRandomFunction; + final VoidCallback onTap; + final ColorFilter? portalFilter; + final List? neighboringWater; + final RestrictedCallCounter? tooltipCallCounter; + + final _wheatFrameDuration = Duration(milliseconds: 40); + static const _neighborIndexCorners = [(4, 0, 1), (5, 2, 1), (7, 2, 3), (6, 0, 3)]; // This is pretty shit but first is the index of the diagonal (from the order in getNeighbors), then the two indices for the directions it is composed of + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: tile, + builder: (context, child) { + final waitTime = tile.waitFrames == null ? null : _wheatFrameDuration * tile.waitFrames!; + final reverseWaitTime = tile.reverseWaitFrames == null ? null : _wheatFrameDuration * tile.reverseWaitFrames!; + + return GestureDetector( + onTap: onTap, + child: Stack( + alignment: Alignment.bottomCenter, + clipBehavior: Clip.none, + fit: StackFit.passthrough, + children: [ + if (tile.isGrass) + // Grass + AnimatedSprite( + key: Key("grass"), // keys needed because otherwise it would sometimes confuse them, leading to incorrect didUpdateWidget calls + vsync: vsync, + idleFrames: AssetHandler.getIdleFrames(tile.type, extra: tileRandomFunction()), + idleController: idleController + ), + + if (!tile.isWater) + // Wheat + AnimatedSprite( + key: Key("wheat"), + vsync: vsync, + shouldAnimate: tile.isEnclosed, + animationFrames: AssetHandler.animationWheatFrames, + animationFrameDuration: _wheatFrameDuration, + animationWaitTime: waitTime, + animationReverseWaitTime: reverseWaitTime, + endIdleFrames: AssetHandler.idleWheatFrames, + idleController: idleController + ) + else ...[ + // Water + AnimatedSprite( + key: Key("water"), + vsync: vsync, + idleFrames: AssetHandler.getIdleFrames(tile.type, extra: (tileRandomFunction(), neighboringWater)), + idleController: idleController + ), + + ..._neighborIndexCorners.indexed.map((indexedIndexCorner) { + final indexCorner = indexedIndexCorner.$2; + if (!neighboringWater![indexCorner.$1] || neighboringWater![indexCorner.$2] || neighboringWater![indexCorner.$3]) return SizedBox.shrink(); // Check if the extra is not needed on this corner + + final index = indexedIndexCorner.$1; + + return RotatedBox( + quarterTurns: index, + child: AnimatedSprite( + vsync: vsync, + idleFrames: AssetHandler.cornerIdleWaterFrames, + idleController: idleController + ) + ); + }).nonNulls, + ], + + // Wall + AnimatedSprite( + key: Key("wall"), + vsync: vsync, + shouldAnimate: tile.type == EncloseGridCellType.wall, + animationFrames: AssetHandler.getAnimationFrames(EncloseGridCellType.wall), + animationReverseFrameDuration: const Duration(milliseconds: 20), + endIdleFrames: AssetHandler.getIdleFrames(EncloseGridCellType.wall), + idleController: idleController + ), + + // Text(flatIndex.toString(), style: TextStyle(fontSize: 10)), + + // Text((_grid.getDistance(flatIndex) ?? -1).toString()), + + // Text( + // "${tile.waitFrames}, ${tile.reverseWaitFrames}", + // style: TextStyle( + // color: Colors.white, + // fontSize: 10 + // ) + // ), + + // Portal + if (tile.isPortal && portalFilter != null) + ColorFiltered( + colorFilter: portalFilter!, + child: AnimatedSprite( + key: Key("portal"), + vsync: vsync, + idleFrames: AssetHandler.getIdleFrames(tile.type), + idleController: idleController + ) + ), + + // Bonus (cherry, apple, bees) + if (tile.isBonus) + AnimatedSprite( + key: Key("bonus"), + vsync: vsync, + animationFrames: AssetHandler.getAnimationFrames(tile.type, extra: tile.isEnclosed), + shouldAnimate: tile.isEnclosed, + animationFrameDuration: const Duration(milliseconds: 100), // This means the total animation time is way longer than for wheat, which can lead to some weird looking behavior but worth it + animationWaitTime: waitTime, + animationReverseWaitTime: reverseWaitTime, + startIdleFrames: AssetHandler.getIdleFrames(tile.type, extra: true), + endIdleFrames: AssetHandler.getIdleFrames(tile.type, extra: false), + idleController: idleController + ), + + if (tile.isMoose) + AnimatedSprite( + key: Key("moose"), + vsync: vsync, + animationFrames: AssetHandler.getAnimationFrames(tile.type), + shouldAnimate: tile.isEnclosed, + startIdleFrames: AssetHandler.getIdleFrames(tile.type, extra: true), + endIdleFrames: AssetHandler.getIdleFrames(tile.type, extra: false), + idleController: idleController + ), + + // if (gridCell != EncloseGridCellType.grass && gridCell != EncloseGridCellType.wall && gridCell != EncloseGridCellType.portal && gridCell != EncloseGridCellType.apple) + // AnimatedSprite( + // frames: gridCell.getAnimationFrames(random: frameRandom), + // vsync: vsync, + // idleFrames: idleFrames, + // idleController: idleController + // ), + + // Tooltips + if (tooltipCallCounter != null) + Positioned( + top: -25, + child: ListenableBuilder( + listenable: tooltipCallCounter!, + builder: (context, child) { + if (!tooltipCallCounter!.exists()) return const SizedBox.shrink(); + + return AnimatedOpacity( + opacity: tooltipCallCounter!.isNotEmpty() ? 1 : 0, + duration: const Duration(milliseconds: 150), + onEnd: () { + tooltipCallCounter!.remove(); + }, + child: WigglingWidget( + controller: idleController, + child: OutlinedText( + text: tooltipCallCounter!.getValue()!, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontFamily: "Schoolbell" + ) + ) + ) + ); + } + ) + ) + ] + ) + ); + }, + ); + } +} diff --git a/lib/screens/games/game_chooser_page.dart b/lib/screens/games/game_chooser_page.dart new file mode 100644 index 00000000..857cb30c --- /dev/null +++ b/lib/screens/games/game_chooser_page.dart @@ -0,0 +1,119 @@ +import "package:flutter/material.dart"; +import "moose_game/moose_game.dart"; +import "enclose_moose/enclose_moose_page.dart"; +import "enclose_moose/helper_widgets/wiggling_widget.dart"; +import "enclose_moose/helper_widgets/outlined_text.dart"; +import "moose_game/game_theme.dart"; + +class GameChooserPage extends StatefulWidget { + @override + _GameChooserState createState() => _GameChooserState(); +} + +class _GameChooserState extends State with SingleTickerProviderStateMixin { + final _idleDuration = const Duration(milliseconds: 1000); + late final _idleController = AnimationController(vsync: this) + ..repeat( + reverse: false, + period: _idleDuration + ); + + @override + void dispose() { + _idleController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Expanded( + child: _filteredImage( + "assets/img/enclose_moose/logo.png", + extraChild: Center( + child: WigglingWidget( + controller: _idleController, + child: OutlinedButton( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute(builder: (context) => EncloseMoosePage()) + ), + child: Text.rich( + TextSpan( + children: "enclose.moose".characters.map((char) => WidgetSpan( + child: WigglingWidget( + controller: _idleController, + maxTranslate: const Offset(1, 0.5), + child: OutlinedText( + text: char, + style: const TextStyle( + fontSize: 30, + fontFamily: "Schoolbell" + ) + ) + ) + )).toList() + ) + ), + style: OutlinedButton.styleFrom( + backgroundColor: Colors.black.withAlpha(128), + visualDensity: VisualDensity.compact + ) + // child: Text("Enclose.moose") + ) + ) + ) + ) + ), + + Container( + height: 1, + color: Theme.of(context).primaryColor + ), + + Expanded( + child: _filteredImage( + mooseGameThumbnail, + extraChild: Center( + child: TextButton( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute(builder: (context) => MooseGamePage()) + ), + child: Text( + "Moose Game", + style: TextStyle( + fontFamily: "NF-Pixels", + fontSize: 30 + ) + ) + ) + ) + ) + ), + ] + ) + ); + } + + Widget _filteredImage(String imagePath, {Widget? extraChild, int filterAlpha = 128}) { + return Stack( + children: [ + Positioned.fill( + child: Image.asset( + imagePath, + fit: BoxFit.cover + ) + ), + + Container( + color: Colors.black.withAlpha(filterAlpha) + ), + + if (extraChild != null) + extraChild + ] + ); + } +} diff --git a/lib/screens/moose_game/game_object.dart b/lib/screens/games/moose_game/game_object.dart similarity index 100% rename from lib/screens/moose_game/game_object.dart rename to lib/screens/games/moose_game/game_object.dart diff --git a/lib/screens/moose_game/game_theme.dart b/lib/screens/games/moose_game/game_theme.dart similarity index 97% rename from lib/screens/moose_game/game_theme.dart rename to lib/screens/games/moose_game/game_theme.dart index e1a9bc06..ff1cfe55 100644 --- a/lib/screens/moose_game/game_theme.dart +++ b/lib/screens/games/moose_game/game_theme.dart @@ -51,6 +51,10 @@ const double _mooseFrameScale = 24 / 27; // the run frames are 27px wide /// Nudge in pixelart pixels, centers the moose head and body when jumping const Map _spaceMooseNudge = {"hilbert_space_jump": 2}; +const String mooseGameThumbnail = spaceTheme + ? "assets/img/moose_game/space_logo.png" + : "assets/img/moose_game/logo.png"; + Sprite _spaceMoose(String name) { final List size = _spaceMooseFrames[name]!; return Sprite( diff --git a/lib/screens/moose_game/ground.dart b/lib/screens/games/moose_game/ground.dart similarity index 66% rename from lib/screens/moose_game/ground.dart rename to lib/screens/games/moose_game/ground.dart index 726e0722..526603b9 100644 --- a/lib/screens/moose_game/ground.dart +++ b/lib/screens/games/moose_game/ground.dart @@ -1,6 +1,6 @@ import 'package:vector_math/vector_math.dart'; -import 'package:fsek_mobile/screens/moose_game/game_object.dart'; -import 'package:fsek_mobile/screens/moose_game/game_theme.dart'; +import 'package:fsek_mobile/screens/games/moose_game/game_object.dart'; +import 'package:fsek_mobile/screens/games/moose_game/game_theme.dart'; class Ground extends GameObject { Ground(double xPosition, double floorY) : super(Vector2(xPosition, floorY)) { diff --git a/lib/screens/moose_game/highscore.dart b/lib/screens/games/moose_game/highscore.dart similarity index 100% rename from lib/screens/moose_game/highscore.dart rename to lib/screens/games/moose_game/highscore.dart diff --git a/lib/screens/moose_game/moose.dart b/lib/screens/games/moose_game/moose.dart similarity index 93% rename from lib/screens/moose_game/moose.dart rename to lib/screens/games/moose_game/moose.dart index d4731c9d..1ac52824 100644 --- a/lib/screens/moose_game/moose.dart +++ b/lib/screens/games/moose_game/moose.dart @@ -1,8 +1,8 @@ import 'dart:ui'; -import 'package:fsek_mobile/screens/moose_game/game_object.dart'; -import 'package:fsek_mobile/screens/moose_game/game_theme.dart'; -import 'package:fsek_mobile/screens/moose_game/sprite.dart'; +import 'package:fsek_mobile/screens/games/moose_game/game_object.dart'; +import 'package:fsek_mobile/screens/games/moose_game/game_theme.dart'; +import 'package:fsek_mobile/screens/games/moose_game/sprite.dart'; import 'package:vector_math/vector_math.dart'; enum MooseState { running, jumping, ducking, falling } diff --git a/lib/screens/moose_game/moose_game.dart b/lib/screens/games/moose_game/moose_game.dart similarity index 97% rename from lib/screens/moose_game/moose_game.dart rename to lib/screens/games/moose_game/moose_game.dart index 4d07da94..35e2c9ee 100644 --- a/lib/screens/moose_game/moose_game.dart +++ b/lib/screens/games/moose_game/moose_game.dart @@ -2,12 +2,12 @@ import 'dart:math'; import 'dart:core'; import 'package:flutter/material.dart'; -import 'package:fsek_mobile/screens/moose_game/game_object.dart'; -import 'package:fsek_mobile/screens/moose_game/game_theme.dart'; -import 'package:fsek_mobile/screens/moose_game/ground.dart'; -import 'package:fsek_mobile/screens/moose_game/highscore.dart'; -import 'package:fsek_mobile/screens/moose_game/obstacle.dart'; -import 'package:fsek_mobile/screens/moose_game/sandwich.dart'; +import 'package:fsek_mobile/screens/games/moose_game/game_object.dart'; +import 'package:fsek_mobile/screens/games/moose_game/game_theme.dart'; +import 'package:fsek_mobile/screens/games/moose_game/ground.dart'; +import 'package:fsek_mobile/screens/games/moose_game/highscore.dart'; +import 'package:fsek_mobile/screens/games/moose_game/obstacle.dart'; +import 'package:fsek_mobile/screens/games/moose_game/sandwich.dart'; import 'package:fsek_mobile/services/api.service.dart'; import 'package:fsek_mobile/l10n/app_localizations.dart'; import 'package:audioplayers/audioplayers.dart'; diff --git a/lib/screens/moose_game/obstacle.dart b/lib/screens/games/moose_game/obstacle.dart similarity index 88% rename from lib/screens/moose_game/obstacle.dart rename to lib/screens/games/moose_game/obstacle.dart index 7fa57744..28a29d7d 100644 --- a/lib/screens/moose_game/obstacle.dart +++ b/lib/screens/games/moose_game/obstacle.dart @@ -2,11 +2,11 @@ import 'dart:math'; import 'dart:ui'; import 'package:vector_math/vector_math.dart'; -import 'package:fsek_mobile/screens/moose_game/game_object.dart'; -import 'package:fsek_mobile/screens/moose_game/game_theme.dart'; +import 'package:fsek_mobile/screens/games/moose_game/game_object.dart'; +import 'package:fsek_mobile/screens/games/moose_game/game_theme.dart'; import 'package:fsek_mobile/services/service_locator.dart'; import 'package:fsek_mobile/services/theme.service.dart'; -import 'package:fsek_mobile/screens/moose_game/obstacle_def.dart'; +import 'package:fsek_mobile/screens/games/moose_game/obstacle_def.dart'; List obstacles = buildObstacles(); diff --git a/lib/screens/moose_game/obstacle_def.dart b/lib/screens/games/moose_game/obstacle_def.dart similarity index 100% rename from lib/screens/moose_game/obstacle_def.dart rename to lib/screens/games/moose_game/obstacle_def.dart diff --git a/lib/screens/moose_game/sandwich.dart b/lib/screens/games/moose_game/sandwich.dart similarity index 63% rename from lib/screens/moose_game/sandwich.dart rename to lib/screens/games/moose_game/sandwich.dart index cdde9b95..950cc525 100644 --- a/lib/screens/moose_game/sandwich.dart +++ b/lib/screens/games/moose_game/sandwich.dart @@ -1,6 +1,6 @@ import 'package:vector_math/vector_math.dart'; -import 'package:fsek_mobile/screens/moose_game/game_object.dart'; -import 'package:fsek_mobile/screens/moose_game/game_theme.dart'; +import 'package:fsek_mobile/screens/games/moose_game/game_object.dart'; +import 'package:fsek_mobile/screens/games/moose_game/game_theme.dart'; class Sandwich extends GameObject { Sandwich(double xPosition, double floorY) diff --git a/lib/screens/moose_game/sprite.dart b/lib/screens/games/moose_game/sprite.dart similarity index 100% rename from lib/screens/moose_game/sprite.dart rename to lib/screens/games/moose_game/sprite.dart diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart index e822bdb6..50f7ea2b 100644 --- a/lib/screens/home/home.dart +++ b/lib/screens/home/home.dart @@ -9,7 +9,7 @@ import 'package:fsek_mobile/screens/guild_meeting/other_documents.dart'; import 'package:fsek_mobile/screens/guild_meeting/about_guild_meeting.dart'; import 'package:fsek_mobile/screens/guild_meeting/propositions.dart'; import 'package:fsek_mobile/screens/guild_meeting/motions.dart'; -import 'package:fsek_mobile/screens/moose_game/moose_game.dart'; +import 'package:fsek_mobile/screens/games/moose_game/moose_game.dart'; import 'package:cached_network_image/cached_network_image.dart'; diff --git a/lib/screens/home/nollningHome26.dart b/lib/screens/home/nollningHome26.dart index bc064db7..f197a81b 100644 --- a/lib/screens/home/nollningHome26.dart +++ b/lib/screens/home/nollningHome26.dart @@ -112,20 +112,20 @@ class _NollningHomePageState extends State { Container( width: screenWidth / buttonSize, child: InkWell( - //onTap: () => (Navigator.push(context, MaterialPageRoute(builder: (context) => QuestHomeScreen()))), - onTap: () => showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(t.localeName == "sv" ? "Stopp!" : "Stop!"), - content: Text(t.localeName == "sv" - ? "Uppdragen kommer att släppas snart! (men inte än)" - : "The quests will be released soon! (but not yet)"), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text("OK")) - ], - )), + onTap: () => (Navigator.push(context, MaterialPageRoute(builder: (context) => QuestHomeScreen()))), + // onTap: () => showDialog( + // context: context, + // builder: (context) => AlertDialog( + // title: Text(t.localeName == "sv" ? "Stopp!" : "Stop!"), + // content: Text(t.localeName == "sv" + // ? "Uppdragen kommer att släppas snart! (men inte än)" + // : "The quests will be released soon! (but not yet)"), + // actions: [ + // TextButton( + // onPressed: () => Navigator.of(context).pop(), + // child: Text("OK")) + // ], + // )), child: Image.asset(questButton) ), ) diff --git a/lib/screens/login/login_ui.dart b/lib/screens/login/login_ui.dart index 01b8452d..9b9db0e6 100644 --- a/lib/screens/login/login_ui.dart +++ b/lib/screens/login/login_ui.dart @@ -212,18 +212,12 @@ class _LoginUIState extends State with SingleTickerProviderStateMixin { ) { final _loginButton = ScaleTransition( scale: loginBtnAnimation as Animation, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, - ), + child: TextButton( key: Key("login_btn"), onPressed: state is! LoginLoading ? _onLoginButtonPressed : null, child: Text( t.loginLogIn, - style: TextStyle( - color: Theme.of(context).colorScheme.onSecondary, - fontSize: 16 - ), + style: TextStyle(fontSize: 16), ))); final _loadingIcon = ScaleTransition(scale: loadingAnimation as Animation, child: CircularProgressIndicator()); diff --git a/lib/screens/nollning/map_page.dart b/lib/screens/nollning/map_page.dart index 4aa5dbbc..d4026c5d 100644 --- a/lib/screens/nollning/map_page.dart +++ b/lib/screens/nollning/map_page.dart @@ -3,15 +3,27 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fsek_mobile/l10n/app_localizations.dart'; -class PlaceInfo { - final String titleSv; - final String titleEn; - final String descriptionSv; - final String descriptionEn; - final List descriptionAssets; - final List boxes; - // final String asset; +class DescriptionAsset { + const DescriptionAsset({ + required this.path, + required this.width, + required this.height + }); + + final String path; + final int width; + final int height; + + factory DescriptionAsset.fromJson(Map json) { + return DescriptionAsset( + path: json["path"], + width: json["width"], + height: json["height"] + ); + } +} +class PlaceInfo { const PlaceInfo({ required this.titleSv, required this.titleEn, @@ -22,14 +34,22 @@ class PlaceInfo { // required this.asset, }); + final String titleSv; + final String titleEn; + final String descriptionSv; + final String descriptionEn; + final List descriptionAssets; + final List boxes; + // final String asset; + factory PlaceInfo.fromJson(Map json) { return PlaceInfo( titleSv: json["title"]["sv"], titleEn: json["title"]["en"], descriptionSv: json["description"]["sv"], descriptionEn: json["description"]["en"], - descriptionAssets: List.from(json["descriptionAssets"]), - boxes: (json["boxes"] as List).map((box) => Rect.fromLTWH( + descriptionAssets: List>.from(json["descriptionAssets"]).map(DescriptionAsset.fromJson).toList(), + boxes: List.from(json["boxes"]).map((box) => Rect.fromLTWH( box["left"], box["top"], box["width"], box["height"] )).toList(), // asset: json["asset"], @@ -94,9 +114,10 @@ class _MapViewState extends State { Future _placePins(BuildContext context) async { final placeInfos = await loadJson(); + for (final placeInfo in placeInfos) { - for (final asset in placeInfo.descriptionAssets) { - precacheImage(AssetImage(asset), context); + for (final descriptionAsset in placeInfo.descriptionAssets) { + precacheImage(AssetImage(descriptionAsset.path), context); } } @@ -132,19 +153,26 @@ class _MapViewState extends State { } } + if(!mounted) return; setState(() { pins = pinList; }); } Widget Function(BuildContext) _buildPOIDialog(String title, String description, - List? descriptionAssets) { + List? descriptionAssets) { return (BuildContext context) => AlertDialog( // constraints: BoxConstraints( // minWidth: MediaQuery.of(context).size.width // ), backgroundColor: const Color(0xFFFF00FF), contentPadding: const EdgeInsets.all(20), + /* // Could add this but scrollable ones look a bit worse (imo) + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.6 + ), + */ + scrollable: true, content: Container( padding: const EdgeInsets.all(0), decoration: const BoxDecoration( @@ -167,7 +195,7 @@ class _MapViewState extends State { title, style: const TextStyle( color: Colors.white, - fontFamily: "NF-Pixels", + fontFamily: "LoRes12OT", fontSize: 30 ) ) @@ -185,12 +213,21 @@ class _MapViewState extends State { ), if (descriptionAssets != null) - ...descriptionAssets.map((asset) => InteractiveViewer( + ...descriptionAssets.map((descriptionAsset) => InteractiveViewer( minScale: 1, maxScale: 5, - child: Image.asset( - asset, - fit: BoxFit.contain + child: AspectRatio( + aspectRatio: descriptionAsset.width / descriptionAsset.height, + child: Stack( + children: [ + Center(child: const CircularProgressIndicator()), + + Image.asset( + descriptionAsset.path, + fit: BoxFit.contain + ), + ] + ) ) )), @@ -198,7 +235,7 @@ class _MapViewState extends State { description, style: const TextStyle( color: Colors.white, - fontFamily: "NF-Pixels", + fontFamily: "LoRes12OT", fontSize: 20 ), ) diff --git a/lib/screens/nollning/nolleguide_26/guild.dart b/lib/screens/nollning/nolleguide_26/guild.dart index d8efc43c..16b62355 100644 --- a/lib/screens/nollning/nolleguide_26/guild.dart +++ b/lib/screens/nollning/nolleguide_26/guild.dart @@ -8,7 +8,6 @@ import 'package:fsek_mobile/screens/nollning/nolleguide_26/portraitBuilder.dart' import 'package:fsek_mobile/screens/nollning/nolleguide_26/wallFactory.dart'; import 'package:fsek_mobile/screens/nollning/nolleguide_26/overlay.dart'; - class GuildPage extends StatefulWidget { @override _GuildPageState createState() => _GuildPageState(); @@ -81,7 +80,6 @@ class _GuildPageState extends State { final String fosetTitle = title("foset"); final String frejaTitle = title("freja", isLocale: false); final String jubileeTitle = title("jubilee"); - final String libuTitle = title("libu"); final String organisationTitle = title("organisation", isLocale: false); final String processionTitle = title("procession"); final String samvetetTitle = title("samvetet"); @@ -190,6 +188,20 @@ class _GuildPageState extends State { PortraitData(imagePath: styrelse4Portrait, flex: doublePortraitFlex, onTap: () => pushNavigator(index: 10, imagePath: styrelse4Portrait)), ]), + Image.asset(accountingTitle), + PortraitRow(portraits: [ + PortraitData(imagePath: treasurerPortrait, flex: largePortraitFlex, + onTap: () => pushNavigator(index: 27, imagePath: treasurerPortrait)), + ]), + PortraitRow(portraits: [ + PortraitData(imagePath: bookPortrait, flex: smallPortraitFlex, + onTap: () => pushNavigator(index: 28, imagePath: bookPortrait)), + ]), + Image.asset(serviceTitle), + PortraitRow(portraits: [ + PortraitData(imagePath: secretaryPortrait, flex: largePortraitFlex, + onTap: () => pushNavigator(index: 15, imagePath: secretaryPortrait)), + ]), Image.asset(studentcouncilTitle), PortraitRow(portraits: [ PortraitData(imagePath: eduPortrait, flex: largePortraitFlex, @@ -210,11 +222,6 @@ class _GuildPageState extends State { PortraitData(imagePath: jubileePortrait, flex: largePortraitFlex, onTap: () => pushNavigator(index: 14, imagePath: jubileePortrait)), ]), - Image.asset(serviceTitle), - PortraitRow(portraits: [ - PortraitData(imagePath: secretaryPortrait, flex: largePortraitFlex, - onTap: () => pushNavigator(index: 15, imagePath: secretaryPortrait)), - ]), Image.asset(truthTitle), PortraitRow(portraits: [ PortraitData(imagePath: truthPortrait, flex: largePortraitFlex, @@ -229,9 +236,8 @@ class _GuildPageState extends State { PortraitData(imagePath: samvetetPortrait, flex: largePortraitFlex, onTap: () => pushNavigator(index: 18, imagePath: samvetetPortrait)), ]), - Image.asset(libuTitle), PortraitRow(portraits: [ - PortraitData(imagePath: libuPortrait, flex: largePortraitFlex, + PortraitData(imagePath: libuPortrait, flex: smallPortraitFlex, onTap: () => pushNavigator(index: 19, imagePath: libuPortrait)), ]), Image.asset(facilitiesTitle), @@ -264,15 +270,6 @@ class _GuildPageState extends State { PortraitData(imagePath: frejaPortrait, flex: largePortraitFlex, onTap: () => pushNavigator(index: 26, imagePath: frejaPortrait)), ]), - Image.asset(accountingTitle), - PortraitRow(portraits: [ - PortraitData(imagePath: treasurerPortrait, flex: largePortraitFlex, - onTap: () => pushNavigator(index: 27, imagePath: treasurerPortrait)), - ]), - PortraitRow(portraits: [ - PortraitData(imagePath: bookPortrait, flex: smallPortraitFlex, - onTap: () => pushNavigator(index: 28, imagePath: bookPortrait)), - ]), Image.asset(cafeTitle), PortraitRow(portraits: [ PortraitData(imagePath: cafePortrait, flex: largePortraitFlex, diff --git a/lib/screens/nollning/questscreen_26/highscore.dart b/lib/screens/nollning/questscreen_26/highscore.dart new file mode 100644 index 00000000..64741f38 --- /dev/null +++ b/lib/screens/nollning/questscreen_26/highscore.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import 'package:fsek_mobile/api_client/lib/api_client.dart'; +import 'package:fsek_mobile/services/api.service.dart'; +import 'package:fsek_mobile/l10n/app_localizations.dart'; + +class HighscoreScreen extends StatefulWidget { + final double availableWidth; + final double availableHeight; + + HighscoreScreen({ + required this.availableHeight, + required this.availableWidth, + }); + + @override + _HighscoreScreenState createState() => _HighscoreScreenState(); +} + +class _HighscoreScreenState extends State { + Map groupPointsMap = {}; + NollningRead? nollning; + + @override + void initState() { + super.initState(); + _loadInitData(); + } + + @override + void dispose() { + super.dispose(); + } + + void _loadInitData() async { + final nollningResponse = await ApiService.apiClient + .getNollningApi() + .nollningGetNollningByYear(year: DateTime.now().year); + final nollningData = nollningResponse.data; + + if (nollningData != null) { + List mentorGroups = nollningData.nollningGroups + .where((group) => group.group.groupType.name == "mentor") + .toList(); + for (final nollningGroup in mentorGroups) { + final groupMissions = await ApiService.apiClient + .getNollningApi() + .nollningGetGroupMissionsFromNollningGroup( + nollningGroupId: nollningGroup.id, + ); + + int totalPoints = 0; + if (groupMissions.data != null && groupMissions.data!.isNotEmpty) { + for (final gm in groupMissions.data!.toList()) { + if (gm.isAccepted == "Accepted") { + totalPoints += gm.points; + } + } + } + groupPointsMap[nollningGroup] = totalPoints; + } + } + + setState(() { + this.nollning = nollningData; + }); + } + + List _createHighscoreCards(BuildContext context) { + var t = AppLocalizations.of(context)!; + List highscoreCards = []; + + Map sortedMap = Map.fromEntries( + groupPointsMap.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)), + ); + + int pos = 1; + + sortedMap.forEach((key, value) { + highscoreCards.add(highscoreCard(key, value, pos, context)); + pos++; + }); + + return highscoreCards; + } + + Widget highscoreCard( + NollningGroupRead group, + int points, + int position, + BuildContext context, + ) { + var t = AppLocalizations.of(context)!; + return Center( + child: Container( + height: widget.availableHeight / 15, + width: widget.availableWidth - 40, + child: Card( + shadowColor: Colors.transparent, + color: Colors.transparent, + child: Stack( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Center( + child: Text( + position.toString(), + style: TextStyle( + color: _getPositionColor(position), + fontFamily: "LoRes12OT", + fontWeight: FontWeight.w800, + fontSize: widget.availableWidth / 15, + ), + ), + ), + Container( + width: widget.availableWidth / 3, + height: widget.availableHeight / 10, + child: Center( + child: Text( + textAlign: TextAlign.center, + group.group.name.length <= 40 + ? group.group.name + : group.group.name.substring(0, 20) + "...", + style: TextStyle( + fontFamily: "LoRes12OT", + fontWeight: FontWeight.w600, + fontSize: group.group.name.length <= 10 + ? widget.availableWidth / 15 + : (group.group.name.length <= 20 + ? widget.availableWidth / 15 + : widget.availableWidth / 30), + color: _getPositionColor(position), + ), + ), + ), + ), + Center( + child: Text( + _pointsToString(points), + style: TextStyle( + fontFamily: "LoRes12OT", + fontWeight: FontWeight.normal, + fontSize: widget.availableWidth / 15, + color: _getPositionColor(position), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + var t = AppLocalizations.of(context)!; + + if (this.nollning == null) { + return Center(child: CircularProgressIndicator()); + } + + return Container( + height: widget.availableHeight, + width: widget.availableWidth, + child: Stack( + children: [ + SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: EdgeInsets.only(top: widget.availableHeight * 0.01), + child: Text( + "HIGH SCORES", + style: TextStyle( + fontFamily: "LoRes12OT", + fontSize: widget.availableWidth / 10, + color: Colors.white, // justera efter din bakgrund + ), + ), + ), + ..._createHighscoreCards(context), + ], + ), + ), + ], + ), + ); + } + + Color _getPositionColor(int pos) { + switch (pos % 4) { + case 0: + return Colors.indigo; + case 1: + return Colors.yellow; + case 2: + return Colors.pink; + default: + return Colors.cyan; + } + } + + String _pointsToString(int points) { + String pointString = points.toString(); + while (pointString.length < 6) { + pointString = "0" + pointString; + } + return pointString; + } +} diff --git a/lib/screens/nollning/questscreen_26/quest_home.dart b/lib/screens/nollning/questscreen_26/quest_home.dart index 62d1ce31..75e05e4a 100644 --- a/lib/screens/nollning/questscreen_26/quest_home.dart +++ b/lib/screens/nollning/questscreen_26/quest_home.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:fsek_mobile/l10n/app_localizations.dart'; +import 'package:fsek_mobile/screens/nollning/questscreen_26/highscore.dart'; +import 'package:fsek_mobile/screens/nollning/questscreen_26/quests.dart'; class QuestHomeScreen extends StatefulWidget { @override @@ -7,32 +8,116 @@ class QuestHomeScreen extends StatefulWidget { } class _QuestHomeScreenState extends State { - void initState() { - super.initState(); + String rubrik = "assets/data/nollning_26/uppdrag/rubrik1.png"; + Color backgroundColor = Color(0xFF00487B); + TabController? _controller; + + void _handleTabChange() { + setState(() { + rubrik = _controller!.index == 0 + ? "assets/data/nollning_26/uppdrag/rubrik1.png" + : "assets/data/nollning_26/uppdrag/rubrik2.png"; + backgroundColor = _controller!.index == 0 + ? Color(0xFF00487B) + : Colors.black; + }); } @override - Widget build(BuildContext context) { - var t = AppLocalizations.of(context)!; + void dispose() { + _controller?.removeListener(_handleTabChange); + super.dispose(); + } + @override + Widget build(BuildContext context) { double screenWidth = MediaQuery.of(context).size.width; double screenHeight = MediaQuery.of(context).size.height; + double appBarHeight = AppBar().preferredSize.height; + double statusBarHeight = MediaQuery.of(context).padding.top; + double availableHeight = screenHeight - appBarHeight - statusBarHeight; - return Scaffold( - extendBodyBehindAppBar: true, - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0.0, - scrolledUnderElevation: 0.0, - ), - body: InteractiveViewer( - panEnabled: true, - child: Container( - height: screenHeight, - width: screenWidth, - child: Align(alignment: Alignment.center, child: Text("Sidequests!")), - ), + return DefaultTabController( + length: 2, + child: Builder( + builder: (context) { + final controller = DefaultTabController.of(context); + if (_controller != controller) { + _controller?.removeListener(_handleTabChange); + _controller = controller; + _controller?.addListener(_handleTabChange); + } + return Scaffold( + appBar: AppBar(), + body: Container( + height: availableHeight, + width: screenWidth, + child: Stack( + children: [ + Positioned.fill(child: ColoredBox(color: backgroundColor)), + Column( + children: [ + Container( + width: screenWidth, + height: screenHeight / 8, + child: Stack( + children: [ + Positioned.fill( + child: Padding( + padding: EdgeInsets.only( + top: screenHeight * 0.03, + bottom: screenHeight * 0.01, + ), + child: Image.asset(rubrik, fit: BoxFit.fill), + ), + ), + Positioned.fill( + child: Row( + children: [ + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _controller?.animateTo(0), + ), + ), + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _controller?.animateTo(1), + ), + ), + ], + ), + ), + ], + ), + ), + Container( + width: screenWidth, + height: availableHeight - (screenHeight / 8), + child: TabBarView( + children: [ + QuestScreen( + availableHeight: + availableHeight - (screenHeight / 8), + availableWidth: screenWidth, + ), + HighscoreScreen( + availableHeight: + availableHeight - (screenHeight / 8), + availableWidth: screenWidth, + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + }, ), ); } -} \ No newline at end of file +} diff --git a/lib/screens/nollning/questscreen_26/quests.dart b/lib/screens/nollning/questscreen_26/quests.dart new file mode 100644 index 00000000..040ca988 --- /dev/null +++ b/lib/screens/nollning/questscreen_26/quests.dart @@ -0,0 +1,1679 @@ +import 'package:flutter/material.dart'; +import 'package:fsek_mobile/api_client/lib/api_client.dart'; +import 'package:fsek_mobile/services/api.service.dart'; +import 'package:fsek_mobile/l10n/app_localizations.dart'; +import 'package:fsek_mobile/util/nollning/week_tracker.dart'; +import 'package:fsek_mobile/util/storage_wrapper.dart'; + +import 'dart:async'; +import 'dart:math'; + +class QuestScreen extends StatefulWidget { + final double availableWidth; + final double availableHeight; + + QuestScreen({required this.availableHeight, required this.availableWidth}); + + @override + _QuestScreenState createState() => _QuestScreenState(); +} + +class _QuestScreenState extends State + with SingleTickerProviderStateMixin { + Map> missionsMap = {}; + Map> groupMissionsMap = {}; + Map _enteredUnlockCodes = {}; + final TokenStorageWrapper _storage = TokenStorageWrapper(); + // Bumped whenever a mission is unlocked, so the pushed mission-detail route + // rebuilds itself. + final ValueNotifier _unlockTick = ValueNotifier(0); + // Bumped a few times a second so all text which could be scrambled rebuilds. + final ValueNotifier _scrambleTick = ValueNotifier(0); + Timer? _scrambleTimer; + AdminUserRead? user; + NollningRead? nollning; + NollningGroupRead? nollningGroup; + late TabController _tabController; + + String ej_vald = "assets/data/nollning_26/uppdrag/ej_vald.png"; + String vald = "assets/data/nollning_26/uppdrag/vald.png"; + String done_check = "assets/data/nollning_26/uppdrag/done_check.png"; + String done_pixelart_placeholder = + "assets/data/nollning_26/uppdrag/done_pixelart_placeholder.png"; + String blue = "assets/data/nollning_26/uppdrag/knapp_blue.png"; + String orange = "assets/data/nollning_26/uppdrag/knapp_orange.png"; + String purple = "assets/data/nollning_26/uppdrag/knapp_purple.png"; + String red = "assets/data/nollning_26/uppdrag/knapp_red.png"; + String yellow = "assets/data/nollning_26/uppdrag/knapp_yellow.png"; + String locked = "assets/data/nollning_26/uppdrag/knapp_locked.png"; + String pixelart_placeholder = + "assets/data/nollning_26/uppdrag/pixelart_placeholder.png"; + String pixelart_barbiedans = + "assets/data/nollning_26/uppdrag/pixelart_barbiedans.png"; + String pixelart_fadder = + "assets/data/nollning_26/uppdrag/pixelart_fadder.png"; + String pixelart_kreativ = + "assets/data/nollning_26/uppdrag/pixelart_kreativ.png"; + String pixelart_resa = "assets/data/nollning_26/uppdrag/pixelart_resa.png"; + String pixelart_spel = "assets/data/nollning_26/uppdrag/pixelart_spel.png"; + String pixelart_tavling = + "assets/data/nollning_26/uppdrag/pixelart_tävling.png"; + String pixelart_alg = "assets/data/nollning_26/uppdrag/pixelart_älg.png"; + String poke_kort = "assets/data/nollning_26/uppdrag/pokekort_exkl.jpg"; + String clouds = "assets/data/nollning_26/uppdrag/clouds.png"; + String turn_in_sv = "assets/data/nollning_26/uppdrag/lämna_in_uppdrag.png"; + String turn_in_en = "assets/data/nollning_26/uppdrag/turn_in_quest.png"; + String placeholder_button = + "assets/data/nollning_26/uppdrag/placeholder_button.png"; + String vecka_0 = "assets/data/nollning_26/uppdrag/vecka0.png"; + String vecka_1 = "assets/data/nollning_26/uppdrag/vecka1.png"; + String vecka_2 = "assets/data/nollning_26/uppdrag/vecka2.png"; + String vecka_3 = "assets/data/nollning_26/uppdrag/vecka3.png"; + String vecka_4 = "assets/data/nollning_26/uppdrag/vecka4.png"; + String week_0 = "assets/data/nollning_26/uppdrag/week0.png"; + String week_1 = "assets/data/nollning_26/uppdrag/week1.png"; + String week_2 = "assets/data/nollning_26/uppdrag/week2.png"; + String week_3 = "assets/data/nollning_26/uppdrag/week3.png"; + String week_4 = "assets/data/nollning_26/uppdrag/week4.png"; + static const unicodeChars = + r'!#$%&*+/<=>?@\^_~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ§¤¢£¥µ¿¡«»×÷±¬°†‡•‰∞≈≠≤≥∑∏√∫∂ΩΔΘΛΞΠΣΦΨαβγδεζηθλμξπστφχψω¦'; + + // we have to use a monospace font to avoid glitching text changing width + static const lockedFont = "Consolas"; + + Random _rnd = Random(); + + String getRandomString(int length) => String.fromCharCodes( + Iterable.generate( + length, + (_) => unicodeChars.codeUnitAt(_rnd.nextInt(unicodeChars.length)), + ), + ); + + bool _isMissionLocked(dynamic mission) { + // If any attempts have been made by the group (GroupMission), the mission is unlocked. + if (mission is! AdventureMissionRead) return false; + if (mission.unlockCode == null) return false; + final entered = _enteredUnlockCodes[mission.id]; + if (entered == null) return true; + return entered.trim().toLowerCase() != + mission.unlockCode!.trim().toLowerCase(); + } + + String _unlockKey(int missionId) => "unlock_code_${nollning?.id}_$missionId"; + + /// Unlocks the mission and closes the dialog if the code is correct, + /// otherwise does nothing and returns false. + bool _submitUnlockCode( + AdventureMissionRead mission, + String code, + BuildContext dialogContext, + ) { + final trimmed = code.trim(); + if (trimmed.toLowerCase() != mission.unlockCode!.trim().toLowerCase()) { + return false; + } + setState(() { + _enteredUnlockCodes[mission.id] = trimmed; + }); + _storage.write(key: _unlockKey(mission.id), value: trimmed); + _unlockTick.value++; // notify detail page to rebuild itself + Navigator.of(dialogContext).pop(); + return true; + } + + Future _loadPersistedUnlockCodes( + List? missions, + ) async { + if (missions == null) return; + final codes = {}; + for (final mission in missions) { + if (mission.unlockCode == null) continue; + final stored = await _storage.read(_unlockKey(mission.id)); + if (stored != null) { + codes[mission.id] = stored; + } + } + if (mounted && codes.isNotEmpty) { + setState(() { + _enteredUnlockCodes.addAll(codes); + }); + } + } + + int _checkNollningWeek() { + return WeekTracker.determineWeek(); + } + + @override + void initState() { + super.initState(); + + this._tabController = TabController( + length: 3, + initialIndex: 2, + vsync: this, + ); //_checkNollningWeek() + + _loadInitData(); + + // Re-roll the getRandomString() calls in build a few times a second, so + // locked missions look like they are glitching. + _scrambleTimer = Timer.periodic(Duration(milliseconds: 300), (_) { + if (!mounted) return; + final anyLocked = missionsMap.values.any( + (missions) => missions.any(_isMissionLocked), + ); + if (!anyLocked) return; + // Only text widgets which can be scrambled are listening to this, + // so most widgets won't rebuild every 300ms + _scrambleTick.value++; + }); + } + + @override + void dispose() { + _scrambleTimer?.cancel(); + _tabController.dispose(); + _unlockTick.dispose(); + _scrambleTick.dispose(); + super.dispose(); + } + + void _refresh() { + setState(() { + this.missionsMap = {}; + this.groupMissionsMap = {}; + this._enteredUnlockCodes = {}; + this.user = null; + this.nollning = null; + this.nollningGroup = null; + }); + _loadInitData(); + } + + Future _loadInitData() async { + final user = await ApiService.apiClient.getUsersApi().usersGetMe(); + final userData = user.data; + final nollning = await ApiService.apiClient + .getNollningApi() + .nollningGetNollningByYear(year: DateTime.now().year); + final nollningData = nollning.data; + List? missionList; + NollningGroupRead? nollningGroupData; + List? groupMissionList; + + if (nollningData != null && userData != null) { + final missions = await ApiService.apiClient + .getNollningApi() + .nollningGetAllAdventureMissionsInNollning( + nollningId: nollningData.id, + ); + if (missions.data != null) { + missionList = missions.data!.toList(); + } + nollningGroupData = await _findUserNollningGroup( + nollningData, + userData.id, + ); + if (nollningGroupData != null) { + final groupmissions = await ApiService.apiClient + .getNollningApi() + .nollningGetGroupMissionsFromNollningGroup( + nollningGroupId: nollningGroupData.id, + ); + groupmissions.data != null + ? groupMissionList = groupmissions.data?.toList() + : (); + } + + setState(() { + this.user = userData; + this.nollning = nollningData; + this.nollningGroup = nollningGroupData; + missionList != null + ? missionList.forEach((mission) { + this.missionsMap + .putIfAbsent(mission.nollningWeek, () => []) + .add(mission); + }) + : (); + groupMissionList != null + ? groupMissionList.forEach((groupmission) { + this.groupMissionsMap + .putIfAbsent( + groupmission.adventureMission.nollningWeek, + () => [], + ) + .add(groupmission); + }) + : (); + }); + + await _loadPersistedUnlockCodes(missionList); + } + } + + NollningGroupRead? _findUserNollningGroup(NollningRead nollning, int userId) { + try { + final nollningGroup = nollning.nollningGroups.firstWhere( + (nollningGroup) => (nollningGroup.group.groupUsers.any( + (groupUser) => groupUser.user.id == userId, + )), + ); + return nollningGroup; + } catch (e) { + return null; + } + } + + // bool isSameDay(DateTime a, DateTime b) { + // print((a.year == b.year) && (a.month == b.month) && (a.day == b.day)); + // return ((a.year == b.year) && (a.month == b.month) && (a.day == b.day)); + // } + + @override + Widget build(BuildContext context) { + var t = AppLocalizations.of(context)!; + + double screenWidth = MediaQuery.of(context).size.width; + double screenHeight = MediaQuery.of(context).size.height; + + if (user == null || nollning == null) { + return Scaffold( + body: Container( + height: widget.availableHeight, + width: widget.availableWidth, + child: Stack(children: [Center(child: CircularProgressIndicator())]), + ), + ); + } + + if (_tabController.length != 0) + return Scaffold( + backgroundColor: Colors.transparent, + body: Column( + children: [ + Container( + width: widget.availableWidth, + height: widget.availableHeight / 20, + child: Row( + children: List.generate(3, (index) { + final labels = ["Missed", "Completed", "Current"]; + return Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _tabController.animateTo(index), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: widget.availableWidth / 30, + ), + child: AnimatedBuilder( + animation: _tabController, + builder: (context, _) { + final isSelected = _tabController.index == index; + return Stack( + children: [ + Positioned.fill( + child: Image.asset( + isSelected ? vald : ej_vald, + fit: BoxFit.fill, + ), + ), + Center( + child: Text( + labels[index], + style: TextStyle( + color: isSelected + ? Colors.orange + : Colors.blue, + fontFamily: "Consolas", + ), + ), + ), + ], + ); + }, + ), + ), + ), + ); + }), + ), + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _questTab(false, context), // Ej avklarade + _questTab(true, context), // Avklarade + _questTab(null, context), // Alla + ], + ), + ), + ], + ), + ); + return Container( + height: widget.availableHeight, + width: widget.availableWidth, + ); + } + + String _pointsFromMission(GroupMissionRead mission, BuildContext context) { + var t = AppLocalizations.of(context)!; + String finalString; + + switch (mission.isAccepted) { + case "Accepted": + finalString = + "${mission.points} / ${mission.adventureMission.maxPoints} ${t.introductionPoints2}"; + break; + + case "Failed": + finalString = + "${0} / ${mission.adventureMission.maxPoints} ${t.introductionPoints2}"; + break; + + case "Review": + finalString = + mission.adventureMission.minPoints == + mission.adventureMission.maxPoints + ? mission.adventureMission.maxPoints.toString() + : "${mission.adventureMission.minPoints} - ${mission.adventureMission.maxPoints} ${t.introductionPoints2}"; + break; + + default: + finalString = ""; + break; + } + + return finalString; + } + + // Not used right now, do we want them to be able to cancel attempts here... I dont think so (You would need manage permission anyway so...) + Future _removeMissionAttempt( + int adventureMissionId, + BuildContext context, + ) async { + var t = AppLocalizations.of(context)!; + + try { + final gmDelete = GroupMissionDelete( + (b) => b..adventureMissionId = adventureMissionId, + ); + final response = await ApiService.apiClient + .getNollningApi() + .nollningDeleteGroupMission( + nollningGroupId: nollningGroup!.id, + groupMissionDelete: gmDelete, + ); + + if (mounted) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(t.localeName == "sv" ? "Lyckat!" : "Success!"), + content: Text( + t.localeName == "sv" + ? "Uppdragsförsök borttaget!" + : "Mission attempt removed", + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + _refresh(); + }, + child: Text("OK"), + ), + ], + ), + ); + } + } catch (e) { + if (mounted) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(t.localeName == "sv" ? "Misslyckat!" : "Failure!"), + content: Text( + t.localeName == "sv" + ? "Misslyckad borttagning av försök" + : "Registration removal failed", + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text("OK"), + ), + ], + ), + ); + } + } + } + + Future _sendMissionAttempt( + AdventureMissionRead adventureMission, + BuildContext context, + VoidCallback onClose, + ) async { + var t = AppLocalizations.of(context)!; + + if (adventureMission.nollningWeek != (_checkNollningWeek())) { + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text( + t.localeName == "sv" ? "Misslyckat!" : "Failure!", + style: TextStyle( + fontFamily: "Consolas", + fontWeight: FontWeight.w600, + fontSize: widget.availableWidth / 20, + color: Colors.black, + ), + ), + content: Text( + t.localeName == "sv" + ? "Misslyckad registrering!\nUppdragsregistrering för denna veckan har stängt!" + : "Falied to register mission!\nMission registration for this week has closed!", + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text("OK"), + ), + ], + ), + ); + return; + } + try { + final gm = GroupMissionCreate((b) { + b..adventureMissionId = adventureMission.id; + b..isAccepted = null; + b..points = null; + }); + final response = await ApiService.apiClient + .getNollningApi() + .nollningAddGroupMission( + groupMissionCreate: gm, + nollningGroupId: nollningGroup!.id, + ); + + if (mounted) { + showDialog( + context: this.context, + builder: (dialogContext) => AlertDialog( + title: Text( + t.localeName == "sv" ? "Lyckat!" : "Success!", + style: TextStyle( + fontFamily: "Consolas", + fontWeight: FontWeight.w600, + fontSize: widget.availableWidth / 20, + color: Colors.black, + ), + ), + content: Text( + t.localeName == "sv" + ? "Uppdragsförsök registerat!" + : "Mission attempt registered", + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + _refresh(); + onClose(); // stänger hela mission-detaljvyn + }, + child: Text("OK"), + ), + ], + ), + ); + } + } catch (e) { + if (mounted) { + showDialog( + context: this.context, + builder: (dialogContext) => AlertDialog( + title: Text( + t.localeName == "sv" ? "Misslyckat!" : "Failure!", + style: TextStyle( + fontFamily: "Consolas", + fontWeight: FontWeight.w600, + fontSize: widget.availableWidth / 20, + color: Colors.black, + ), + ), + content: Text( + t.localeName == "sv" + ? "Misslyckad registrering" + : "Registration failed", + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text("OK"), + ), + ], + ), + ); + } + } + } + + void _openMission(dynamic element, BuildContext context) { + Navigator.of(context).push( + PageRouteBuilder( + opaque: false, + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, + pageBuilder: (ctx, _, __) => ValueListenableBuilder( + valueListenable: _unlockTick, + builder: (ctx2, _, __) => + _missionDetails(element, ctx2, () => Navigator.of(ctx2).pop()), + ), + ), + ); + } + + void _showUnlockDialog(AdventureMissionRead mission, BuildContext context) { + var t = AppLocalizations.of(context)!; + String enteredCode = ""; + bool wrongCode = false; + + showDialog( + context: context, + builder: (dialogContext) => StatefulBuilder( + builder: (dialogContext, setDialogState) { + void submit(String value) { + enteredCode = value; + if (!_submitUnlockCode(mission, value, dialogContext)) { + setDialogState(() { + wrongCode = true; + }); + } + } + + return AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + t.questUnlockTitle, + style: TextStyle( + fontFamily: "Consolas", + fontWeight: FontWeight.w600, + fontSize: widget.availableWidth / 20, + color: Colors.black, + ), + ), + TextField( + autofocus: true, + autocorrect: false, + textCapitalization: TextCapitalization.none, + decoration: InputDecoration( + labelText: t.questUnlockEnterCode, + errorText: wrongCode ? t.questUnlockWrongCode : null, + ), + onChanged: (value) { + enteredCode = value; + if (wrongCode) { + setDialogState(() { + wrongCode = false; + }); + } + }, + onSubmitted: submit, + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + submit(enteredCode); + }, + child: Text(t.eventYes), + ), + TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + }, + child: Text(t.eventNo), + ), + ], + ); + }, + ), + ); + } + + Widget _missionDetails( + dynamic mission, + BuildContext context, + VoidCallback onClose, + ) { + var t = AppLocalizations.of(context)!; + + if (mission is AdventureMissionRead) { + return Material( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onClose, + child: Container( + height: widget.availableHeight, + width: widget.availableWidth, + child: Stack( + children: [ + Positioned.fill( + child: Image.asset( + poke_kort, + fit: BoxFit.fill, + cacheWidth: 1440, + ), + ), + Padding( + padding: EdgeInsets.only(top: widget.availableHeight * 0.27), + child: Align( + alignment: Alignment.topCenter, + child: _isMissionLocked(mission) + ? _lockedPixelArt( + widget.availableHeight * 0.35, + false, + mission.titleSv, + ) + : Image.asset( + width: widget.availableHeight * 0.35, + height: widget.availableHeight * 0.35, + _pixelArt(mission), + fit: BoxFit.contain, + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: widget.availableHeight * 0.14), + child: Row( + children: [ + Image.asset( + _week_image(mission.nollningWeek, t.localeName == "sv"), + width: widget.availableWidth * 0.22, + ), + Padding( + padding: EdgeInsets.only( + left: widget.availableWidth * 0.03, + ), + child: ValueListenableBuilder( + valueListenable: _scrambleTick, + builder: (context, _, __) => Text( + _isMissionLocked(mission) + ? getRandomString(2) + + t.questLockedTitle + + getRandomString(2) + : t.localeName == "sv" + ? (mission.titleSv.length <= 21 + ? mission.titleSv + : mission.titleSv.substring(0, 18) + + "...") + : (mission.titleEn.length <= 21 + ? mission.titleEn + : mission.titleEn.substring(0, 18) + + "..."), + style: TextStyle( + fontFamily: _isMissionLocked(mission) + ? lockedFont + : null, + fontWeight: FontWeight.w600, + fontSize: widget.availableWidth / 15, + color: Colors.black, // Color(0xFFFCBD1D) + ), + ), + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only(left: 30, right: 30), + child: Column( + children: [ + SizedBox(height: widget.availableHeight * 0.73), + SizedBox( + height: widget.availableHeight * 0.30, + child: SingleChildScrollView( + child: Text( + _isMissionLocked(mission) + ? t.questLockedDescription + + (t.localeName == "sv" + ? (mission.unlockHintSv != null + ? "\n\n" + mission.unlockHintSv! + : "") + : (mission.unlockHintEn != null + ? "\n\n" + mission.unlockHintEn! + : "")) + : t.localeName == "sv" + ? mission.descriptionSv + : mission.descriptionEn, + style: TextStyle( + fontFamily: "Consolas", + fontWeight: FontWeight.normal, + fontSize: widget.availableWidth / 20, + ), + ), + ), + ), + Center( + child: ValueListenableBuilder( + valueListenable: _scrambleTick, + builder: (context, _, __) => Text( + _isMissionLocked(mission) + ? getRandomString(2) + + " - " + + getRandomString(2) + + " " + + t.introductionPoints2 + : mission.minPoints == mission.maxPoints + ? mission.maxPoints.toString() + : "${mission.minPoints} - ${mission.maxPoints} ${t.introductionPoints2}", + style: TextStyle( + fontFamily: _isMissionLocked(mission) + ? lockedFont + : "LoRes12OT", + fontWeight: FontWeight.w600, + fontSize: widget.availableWidth / 20, + ), + ), + ), + ), + SizedBox(height: 20), + ], + ), + ), + Padding( + padding: EdgeInsets.only( + bottom: widget.availableHeight * 0.13, + ), + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + height: widget.availableHeight / 15, + width: widget.availableWidth, + child: InkWell( + splashColor: + Colors.transparent, // Remove splash transparency + highlightColor: + Colors.transparent, // Remove highlight transparency + borderRadius: BorderRadius.circular(12), + onTap: () async { + if (_isMissionLocked(mission)) { + _showUnlockDialog(mission, context); + return; + } + final confirmed = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + content: Text( + t.localeName == "sv" + ? "Registrera uppdragsförsök?" + : "Register quest attempt?", + style: TextStyle( + fontFamily: "Consolas", + fontWeight: FontWeight.w600, + fontSize: widget.availableWidth / 20, + color: Colors.black, + ), + ), + actions: [ + TextButton( + onPressed: () => + Navigator.of(dialogContext).pop(true), + child: Text(t.eventYes), + ), + TextButton( + onPressed: () => + Navigator.of(dialogContext).pop(false), + child: Text(t.eventNo), + ), + ], + ), + ); + if (confirmed == true) { + await _sendMissionAttempt( + mission, + context, + onClose, + ); + } + }, + child: _isMissionLocked(mission) + ? Stack( + children: [ + Center( + child: Image.asset(placeholder_button), + ), + Center( + child: Text( + t.questUnlockButton, + style: TextStyle( + fontFamily: "LoRes12OT", + fontWeight: FontWeight.w600, + fontSize: widget.availableWidth / 20, + color: Colors.black, + ), + ), + ), + ], + ) + : Stack( + children: [ + Center( + child: Image.asset( + t.localeName == "sv" + ? turn_in_sv + : turn_in_en, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + if (mission is GroupMissionRead) { + return Material( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onClose, + child: Container( + height: widget.availableHeight, + width: widget.availableWidth, + child: Stack( + children: [ + Positioned.fill( + child: Image.asset( + poke_kort, + fit: BoxFit.fill, + cacheWidth: 1440, + ), + ), + Padding( + padding: EdgeInsets.only(top: widget.availableHeight * 0.27), + child: Align( + alignment: Alignment.topCenter, + child: Image.asset( + width: widget.availableHeight * 0.35, + height: widget.availableHeight * 0.35, + _pixelArt(mission), + fit: BoxFit.contain, + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: widget.availableHeight * 0.14), + child: Row( + children: [ + Image.asset( + _week_image( + mission.adventureMission.nollningWeek, + t.localeName == "sv", + ), + width: widget.availableWidth * 0.22, + ), + Padding( + padding: EdgeInsets.only( + left: widget.availableWidth * 0.03, + ), + child: Text( + t.localeName == "sv" + ? (mission.adventureMission.titleSv.length <= 21 + ? mission.adventureMission.titleSv + : mission.adventureMission.titleSv + .substring(0, 18) + + "...") + : (mission.adventureMission.titleEn.length <= 21 + ? mission.adventureMission.titleEn + : mission.adventureMission.titleEn + .substring(0, 18) + + "..."), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: widget.availableWidth / 15, + color: Colors.black, // Color(0xFFFCBD1D) + ), + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only(left: 30, right: 30), + child: Column( + children: [ + SizedBox(height: widget.availableHeight * 0.73), + SizedBox( + height: widget.availableHeight * 0.30, + child: SingleChildScrollView( + child: Text( + t.localeName == "sv" + ? mission.adventureMission.descriptionSv + : mission.adventureMission.descriptionEn, + style: TextStyle( + fontFamily: "Consolas", + fontWeight: FontWeight.normal, + fontSize: widget.availableWidth / 20, + ), + ), + ), + ), + SizedBox(height: 20), + Center( + child: Text( + _pointsFromMission(mission, context), + style: TextStyle( + fontFamily: "LoRes12OT", + fontWeight: FontWeight.w600, + fontSize: widget.availableWidth / 20, + ), + ), + ), + SizedBox(height: 5), + Center(child: _missionReview(context, mission)), + ], + ), + ), + ], + ), + ), + ), + ); + } + + return SizedBox(); + } + + Widget _questTab(bool? completedQuestsTab, BuildContext context) { + // completedQuestsTab: true = "Completed", false = "Missed", None = "Current" + + return Container( + height: widget.availableHeight, + width: widget.availableWidth, + child: Stack( + children: [ + SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: widget.availableHeight), + child: IntrinsicHeight( + child: Column( + children: [ + SizedBox(height: 10), + ...generateTabMissionCards( + missionsMap, + groupMissionsMap, + completedQuestsTab, + context, + ), + if (completedQuestsTab == null) ...[ + Spacer(), + Image.asset(clouds), + ], + ], + ), + ), + ), + ), + ], + ), + ); + } + + Widget createMissionCard(dynamic element, BuildContext context) { + var t = AppLocalizations.of(context)!; + + if (element is AdventureMissionRead) { + Container container = Container( + height: widget.availableHeight / 6, + width: widget.availableWidth - 40, + child: Card( + shadowColor: Colors.transparent, + color: Colors.transparent, + surfaceTintColor: null, + child: InkWell( + onTap: () { + _openMission(element, context); + }, + child: Stack( + children: [ + Positioned.fill( + child: Image.asset(_buttonArt(element), fit: BoxFit.fill), + ), + Positioned( + left: widget.availableHeight / 6 * 0.08, + top: widget.availableHeight / 6 * 0.1, + child: SizedBox( + width: widget.availableHeight / 6 * 0.73, + height: widget.availableHeight / 6 * 0.73, + child: Stack( + children: [ + if (_isMissionLocked(element)) + _lockedPixelArt( + widget.availableHeight / 6 * 0.73, + true, + element.titleSv, + ) + else ...[ + Image.asset( + pixelart_placeholder, + fit: BoxFit.contain, + ), + Positioned.fill( + child: Center( + child: Image.asset( + _pixelArt(element), + fit: BoxFit.contain, + ), + ), + ), + ], + ], + ), + ), + ), + Positioned.fill( + child: Padding( + padding: EdgeInsets.only( + left: + widget.availableHeight / 6 * 0.1 + // left margin + widget.availableHeight / 6 * 0.7 + // image width + 20, + ), + child: Align( + alignment: Alignment.centerLeft, + child: ValueListenableBuilder( + valueListenable: _scrambleTick, + builder: (context, _, __) => Text( + _isMissionLocked(element) + ? getRandomString(3) + + t.questLockedTitle + + getRandomString(3) + : t.localeName == "sv" + ? (element.titleSv.length <= 40 + ? element.titleSv + : element.titleSv.substring(0, 35) + "...") + : (element.titleEn.length <= 40 + ? element.titleEn + : element.titleEn.substring(0, 35) + "..."), + style: TextStyle( + fontFamily: _isMissionLocked(element) + ? lockedFont + : "LoRes12OT", + fontWeight: FontWeight.w600, + fontSize: widget.availableWidth / 20, + color: Colors.white, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + + return Center(child: container); + } else if (element is GroupMissionRead) { + switch (element.isAccepted) { + case "Accepted": + { + Container container = Container( + height: widget.availableHeight / 6, + width: widget.availableWidth - 40, + child: Card( + shadowColor: Colors.transparent, + color: Colors.transparent, + surfaceTintColor: null, + child: InkWell( + onTap: () { + _openMission(element, context); + }, + child: Stack( + children: [ + Positioned.fill( + child: Image.asset( + _buttonArt(element), + fit: BoxFit.fill, + ), + ), + Positioned( + left: widget.availableHeight / 6 * 0.08, + top: widget.availableHeight / 6 * 0.1, + child: SizedBox( + width: widget.availableHeight / 6 * 0.73, + height: widget.availableHeight / 6 * 0.73, + child: Stack( + children: [ + Positioned.fill( + child: Center( + child: Image.asset( + _pixelArt(element), + fit: BoxFit.contain, + ), + ), + ), + Image.asset( + done_pixelart_placeholder, + fit: BoxFit.contain, + ), + Image.asset(done_check, fit: BoxFit.contain), + ], + ), + ), + ), + Positioned.fill( + child: Padding( + padding: EdgeInsets.only( + left: + widget.availableHeight / + 6 * + 0.1 + // left margin + widget.availableHeight / + 6 * + 0.7 + // image width + 20, + ), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + t.localeName == "sv" + ? (element.adventureMission.titleSv.length <= + 40 + ? element.adventureMission.titleSv + : element.adventureMission.titleSv + .substring(0, 35) + + "...") + : (element.adventureMission.titleEn.length <= + 40 + ? element.adventureMission.titleEn + : element.adventureMission.titleEn + .substring(0, 35) + + "..."), + style: TextStyle( + fontFamily: "LoRes12OT", + fontWeight: FontWeight.w600, + fontSize: widget.availableWidth / 20, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + + return Center(child: container); + } + + case "Failed": + { + Container container = Container( + height: widget.availableHeight / 6, + width: widget.availableWidth - 40, + child: Card( + shadowColor: Colors.transparent, + color: Colors.transparent, + surfaceTintColor: null, + child: InkWell( + onTap: () { + _openMission(element, context); + }, + child: Stack( + children: [ + Positioned.fill( + child: Image.asset( + _buttonArt(element), + fit: BoxFit.fill, + ), + ), + Positioned( + left: widget.availableHeight / 6 * 0.08, + top: widget.availableHeight / 6 * 0.1, + child: SizedBox( + width: widget.availableHeight / 6 * 0.73, + height: widget.availableHeight / 6 * 0.73, + child: Stack( + children: [ + Positioned.fill( + child: Center( + child: Image.asset( + _pixelArt(element), + fit: BoxFit.contain, + ), + ), + ), + Image.asset( + done_pixelart_placeholder, + fit: BoxFit.contain, + ), + ], + ), + ), + ), + Positioned.fill( + child: Padding( + padding: EdgeInsets.only( + left: + widget.availableHeight / + 6 * + 0.1 + // left margin + widget.availableHeight / + 6 * + 0.7 + // image width + 20, + ), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + t.localeName == "sv" + ? (element.adventureMission.titleSv.length <= + 40 + ? element.adventureMission.titleSv + : element.adventureMission.titleSv + .substring(0, 35) + + "...") + : (element.adventureMission.titleEn.length <= + 40 + ? element.adventureMission.titleEn + : element.adventureMission.titleEn + .substring(0, 35) + + "..."), + style: TextStyle( + fontFamily: "LoRes12OT", + fontWeight: FontWeight.w600, + fontSize: widget.availableWidth / 20, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + + return Center(child: container); + } + + case "Review": + { + Container container = Container( + height: widget.availableHeight / 6, + width: widget.availableWidth - 40, + child: Card( + shadowColor: Colors.transparent, + color: Colors.transparent, + surfaceTintColor: null, + child: InkWell( + onTap: () { + _openMission(element, context); + }, + child: Stack( + children: [ + Positioned.fill( + child: Image.asset( + _buttonArt(element), + fit: BoxFit.fill, + ), + ), + Positioned( + left: widget.availableHeight / 6 * 0.08, + top: widget.availableHeight / 6 * 0.1, + child: SizedBox( + width: widget.availableHeight / 6 * 0.73, + height: widget.availableHeight / 6 * 0.73, + child: Stack( + children: [ + Image.asset( + pixelart_placeholder, + fit: BoxFit.contain, + ), + Positioned.fill( + child: Center( + child: Image.asset( + _pixelArt(element), + fit: BoxFit.contain, + ), + ), + ), + Image.asset( + done_pixelart_placeholder, + fit: BoxFit.contain, + ), + ], + ), + ), + ), + Positioned.fill( + child: Padding( + padding: EdgeInsets.only( + left: + widget.availableHeight / + 6 * + 0.1 + // left margin + widget.availableHeight / + 6 * + 0.7 + // image width + 20, + ), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + t.localeName == "sv" + ? (element.adventureMission.titleSv.length <= + 40 + ? element.adventureMission.titleSv + : element.adventureMission.titleSv + .substring(0, 35) + + "...") + : (element.adventureMission.titleEn.length <= + 40 + ? element.adventureMission.titleEn + : element.adventureMission.titleEn + .substring(0, 35) + + "..."), + style: TextStyle( + fontFamily: "LoRes12OT", + fontWeight: FontWeight.w600, + fontSize: widget.availableWidth / 20, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + return Center(child: container); + } + } + } + return SizedBox(); + } + + Text _missionReview(BuildContext context, GroupMissionRead gm) { + var t = AppLocalizations.of(context)!; + switch (gm.isAccepted) { + case "Accepted": + return Text( + t.localeName == "sv" ? "Uppdrag accepterat" : "Mission success", + style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold), + ); + + case "Failed": + return Text( + t.localeName == "sv" ? "Uppdrag misslyckat" : "Mission failed", + style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold), + ); + + default: + return Text( + t.localeName == "sv" + ? "Uppdrag under granskning" + : "Mission under review", + style: TextStyle( + color: Color(0xFFcfac23), + fontWeight: FontWeight.bold, + ), + ); + } + } + + List generateTabMissionCards( + Map> missionsMap, + Map> groupMissionsMap, + bool? completedQuestsTab, // true=completed, false=missed, None=current + BuildContext context, + ) { + final week = _checkNollningWeek(); + final allMissions = []; + final groupMissions = []; + + if (completedQuestsTab == null) { + allMissions.addAll(missionsMap[week] ?? []); + groupMissions.addAll(groupMissionsMap[week] ?? []); + } else { + for (int w = 0; w <= week; w += 1) { + allMissions.addAll(missionsMap[w] ?? []); + groupMissions.addAll(groupMissionsMap[w] ?? []); + } + } + + final missionList = []; + allMissions.forEach((mission) { + if (groupMissions.any( + (groupMission) => groupMission.adventureMission.id == mission.id, + )) { + missionList.add( + groupMissions.firstWhere( + (groupMission) => (mission.id == groupMission.adventureMission.id), + ), + ); + } else { + missionList.add(mission); + } + }); + + var filteredMissionList = missionList; + if (completedQuestsTab == true) { + filteredMissionList = missionList.where((m) => _isCompleted(m)).toList(); + } else if (completedQuestsTab == false) { + // A past week's quest that was never attempted never becomes a + // GroupMission, so without this it shows up in no tab at all. + filteredMissionList = missionList + .where( + (m) => + _isFailed(m) || + (m is AdventureMissionRead && m.nollningWeek < week), + ) + .toList(); + } + + List list = []; + filteredMissionList.forEach( + (element) => (list.add(createMissionCard(element, context))), + ); + + return list; + } + + bool _isCompleted(dynamic mission) { + if (mission is GroupMissionRead) { + return mission.isAccepted == "Accepted" || mission.isAccepted == "Review"; + } + return false; + } + + bool _isFailed(dynamic mission) { + if (mission is GroupMissionRead) { + return mission.isAccepted == "Failed"; + } + return false; + } + + // Colors used for the "?" of locked missions. + static const lockedQuestionColors = [ + Color(0xFF4C7FE0), // blue + Color(0xFFE08A2E), // orange + Color(0xFF8A5AD1), // purple + Color(0xFFD1483F), // red + Color(0xFFE0C13A), // yellow + Color(0xFF3FAE7A), // green + Color(0xFFD1519B), // pink + ]; + + Color getColorFromTitle(String titleSv) { + // Hashes titleSv, uses it to pick a color from a list. + // Used for locked missions so we can tell them apart visually. + + int hash = 0; + for (final codeUnit in titleSv.codeUnits) { + hash = (hash * 31 + codeUnit) & 0x7FFFFFFF; + } + return lockedQuestionColors[hash % lockedQuestionColors.length]; + } + + Widget _lockedPixelArt(double size, bool showBackground, String titleSv) { + return SizedBox( + width: size, + height: size, + child: Stack( + children: [ + if (showBackground) + Positioned.fill( + child: Image.asset(pixelart_placeholder, fit: BoxFit.contain), + ), + Positioned.fill( + child: Padding( + padding: EdgeInsets.all(size * 0.01), + child: FittedBox( + fit: BoxFit.contain, + child: Text( + "?", + style: TextStyle( + fontFamily: "LoRes12OT", + fontWeight: FontWeight.w600, + height: 1.0, + color: getColorFromTitle(titleSv), + ), + ), + ), + ), + ), + ], + ), + ); + } + + String _pixelArt(dynamic mission) { + String category = ""; + String pixelArt; + if (mission is AdventureMissionRead) { + category = mission.missionCategory; + } else if (mission is GroupMissionRead) { + category = mission.adventureMission.missionCategory; + } + switch (category) { + case "Barbiedans": + pixelArt = pixelart_barbiedans; + break; + case "Fadder": + pixelArt = pixelart_fadder; + break; + case "Kreativ": + pixelArt = pixelart_kreativ; + break; + case "Resa": + pixelArt = pixelart_resa; + break; + case "Spel": + pixelArt = pixelart_spel; + break; + case "Tävling": + pixelArt = pixelart_tavling; + break; + case "Älg": + pixelArt = pixelart_alg; + break; + default: + pixelArt = pixelart_placeholder; + break; + } + return pixelArt; + } + + String _buttonArt(dynamic mission) { + if (_isMissionLocked(mission)) { + return locked; + } + String category = ""; + String pixelArt; + if (mission is AdventureMissionRead) { + category = mission.missionCategory; + } else if (mission is GroupMissionRead) { + category = mission.adventureMission.missionCategory; + } + switch (category) { + case "Barbiedans": + pixelArt = purple; + break; + case "Fadder": + pixelArt = orange; + break; + case "Kreativ": + pixelArt = blue; + break; + case "Resa": + pixelArt = orange; + break; + case "Spel": + pixelArt = red; + break; + case "Tävling": + pixelArt = yellow; + break; + case "Älg": + pixelArt = orange; + break; + default: + pixelArt = orange; + break; + } + return pixelArt; + } + + String _week_image(int week, bool lang) { + if (lang) { + switch (week) { + case 0: + return vecka_0; + case 1: + return vecka_1; + case 2: + return vecka_2; + case 3: + return vecka_3; + default: + return vecka_4; + } + } else { + switch (week) { + case 0: + return week_0; + case 1: + return week_1; + case 2: + return week_2; + case 3: + return week_3; + default: + return week_4; + } + } + } +} diff --git a/lib/themes.dart b/lib/themes.dart index b87ab481..41cd0de5 100644 --- a/lib/themes.dart +++ b/lib/themes.dart @@ -94,6 +94,7 @@ final ThemeData themeFdark = ThemeData( style: ButtonStyle( backgroundColor: WidgetStateProperty.all(Colors.orange[700]!), foregroundColor: WidgetStateProperty.all(Colors.white), + overlayColor: WidgetStateProperty.all(Colors.white.withAlpha(26)), ), ), @@ -177,6 +178,7 @@ final ThemeData themeN = ThemeData( style: ButtonStyle( backgroundColor: WidgetStateProperty.all(Colors.green[700]!), foregroundColor: WidgetStateProperty.all(Colors.white), + overlayColor: WidgetStateProperty.all(Colors.white.withAlpha(26)), ), ), @@ -260,6 +262,7 @@ final ThemeData themePi = ThemeData( style: ButtonStyle( backgroundColor: WidgetStateProperty.all(Color.fromARGB(255, 245, 147, 35)), foregroundColor: WidgetStateProperty.all(Colors.black), + overlayColor: WidgetStateProperty.all(Colors.white.withAlpha(26)), ), ), @@ -374,6 +377,7 @@ final ThemeData themeF = ThemeData( style: ButtonStyle( backgroundColor: WidgetStateProperty.all(Colors.orange[700]!), foregroundColor: WidgetStateProperty.all(Colors.white), + overlayColor: WidgetStateProperty.all(Colors.white.withAlpha(26)), ), ), @@ -425,6 +429,7 @@ final ThemeData themeO = ThemeData( style: ButtonStyle( backgroundColor: WidgetStateProperty.all(const Color.fromRGBO(191, 54, 12, 1)), foregroundColor: WidgetStateProperty.all(Colors.white), + overlayColor: WidgetStateProperty.all(Colors.white.withAlpha(26)), ), ), @@ -480,6 +485,7 @@ final ThemeData themeD = ThemeData( style: ButtonStyle( backgroundColor: WidgetStateProperty.all(Color(0xFFF280A1)), foregroundColor: WidgetStateProperty.all(Colors.white), + overlayColor: WidgetStateProperty.all(Colors.white.withAlpha(26)), ), ), @@ -516,6 +522,7 @@ final ThemeData themeV = ThemeData( style: ButtonStyle( backgroundColor: WidgetStateProperty.all(Colors.blue[400]!), foregroundColor: WidgetStateProperty.all(Colors.black), + overlayColor: WidgetStateProperty.all(Colors.white.withAlpha(26)), ), ), diff --git a/lib/widgets/bottom_app_bar.dart b/lib/widgets/bottom_app_bar.dart index 792c5a43..ac0a0599 100644 --- a/lib/widgets/bottom_app_bar.dart +++ b/lib/widgets/bottom_app_bar.dart @@ -1,7 +1,7 @@ import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:fsek_mobile/screens/moose_game/moose_game.dart'; +import 'package:fsek_mobile/screens/games/moose_game/moose_game.dart'; import 'package:fsek_mobile/widgets/easterEgg/animated_nils.dart'; import 'package:fsek_mobile/widgets/easterEgg/easteregg_code_dialog.dart'; import 'package:fsek_mobile/widgets/easterEgg/familyGuyFoset.dart'; diff --git a/lib/widgets/forgotten_password.dart b/lib/widgets/forgotten_password.dart index 5e26e25a..4ed369c5 100644 --- a/lib/widgets/forgotten_password.dart +++ b/lib/widgets/forgotten_password.dart @@ -37,16 +37,7 @@ class ForgottenPassword { ), Padding( padding: EdgeInsets.all(8.0), - child: Container( - width: 220.0, - child: Text( - contentText, - textAlign: TextAlign.center, - style: TextStyle( - //fontWeight: FontWeight.w600, - //color: Colors.black, - ), - )), + child: Text(contentText), ), Visibility( visible: notClicked, @@ -73,17 +64,9 @@ class ForgottenPassword { child: Padding( padding: const EdgeInsets.all(8.0), child: Stack(children: [ - ElevatedButton( + TextButton( key: Key('submit_btn'), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary), - child: Text( - t.resetSubmit, - style: TextStyle( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSecondary, - ), - ), + child: Text(t.resetSubmit), onPressed: () { setState(() { resetController.text.isEmpty diff --git a/pubspec.lock b/pubspec.lock index 9c0a5dde..7b55e093 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -392,6 +392,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: b938f77d042cbcd822936a7a359a7235bad8bd72070de1f827efc2cc297ac888 + url: "https://pub.dev" + source: hosted + version: "1.2.0" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 243d9aff..47dc76b6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ description: Mobile application for the F-guild # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # -version: 1.30.6122+1 +version: 1.31.6122+1 environment: sdk: '>=3.10.0 <4.0.0' @@ -48,6 +48,7 @@ dependencies: flutter_localizations: sdk: flutter flutter_image_compress: ^2.0.4 + fl_chart: ^1.2.0 #pdf_render: ^1.4.12 path_provider: any flutter_pdfview: ^1.4.3 # For PDF viewer @@ -113,6 +114,19 @@ flutter: - assets/audio/gameoverfart.mp3 - assets/img/ - assets/img/moose_game/ + - assets/audio/enclose_moose/ + - assets/img/enclose_moose/ + - assets/img/enclose_moose/grass/ + - assets/img/enclose_moose/wheat/ + - assets/img/enclose_moose/wheat/ + - assets/img/enclose_moose/water/ + - assets/img/enclose_moose/wall/ + - assets/img/enclose_moose/portal/ + - assets/img/enclose_moose/cherry/ + - assets/img/enclose_moose/apple/ + - assets/img/enclose_moose/apple/ + - assets/img/enclose_moose/bees/ + - assets/img/enclose_moose/moose/ - assets/img/memes/ - assets/data/nollning_26/homescreen/ - assets/data/nollning_26/schema/ @@ -133,6 +147,7 @@ flutter: - assets/data/nollning_26/calendar/ - assets/data/nollning_26/map/ - assets/data/nollning_26/map/pin_media/ + - assets/data/nollning_26/uppdrag/ fonts: - family: Brandon Grotesque @@ -156,6 +171,12 @@ flutter: - family: Consolas fonts: - asset: assets/fonts/Consolas-Regular.ttf + - family: Schoolbell + fonts: + - asset: assets/fonts/Schoolbell-Regular.ttf + - family: LoRes12OT + fonts: + - asset: assets/fonts/LoRes12OT-Regular.ttf # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware.