big palette energy

This commit is contained in:
bryanthaboi
2026-08-26 12:04:06 -04:00
parent 522d8cecf3
commit 3e4772abf4
99 changed files with 4884 additions and 438 deletions
+765
View File
@@ -0,0 +1,765 @@
return {
{ name = "BGB", palettes = {
{ "BGB_v0.3", "142c3c548c74acd494ecfccc" },
{ "Blue", "0000600000c05f5fffc0c0ff" },
{ "GB_Kiosk", "1434046c6c04bcbc1cececb4" },
{ "GB_Light", "004f3b00694a009a7100b581" },
{ "Grey", "1414145c5c5ca4a4a4ececec" },
{ "Greys_High_Contrast", "000000686868b0b0b0ffffff" },
{ "KIG_Green", "20402030703040a04050d050" },
{ "LCD_Green", "0c1c24346c548cc474e4fcd4" },
{ "Red", "600000c00000ff6060ffc0c0" },
{ "Super_GB", "311852ad2921de944affefce" },
{ "Yellow", "202010686830b0a848f8f078" },
{ "Yellow_BGB", "556f3593a73bd2cf4df4ebbf" },
{ "NGM_Brown", "483818987838d8b058f8e088" },
} },
{ name = "BGB/Inverted", palettes = {
{ "BGB_v0.3 - Inverted", "ecfcccacd494548c74142c3c" },
{ "Blue - Inverted", "c0c0ff5f5fff0000c0000060" },
{ "GB_Kiosk - Inverted", "ececb4bcbc1c6c6c04143404" },
{ "GB_Light - Inverted", "00b581009a7100694a004f3b" },
{ "Grey - Inverted", "ecececa4a4a45c5c5c141414" },
{ "Greys_High_Contrast - Inverted", "ffffffb0b0b0686868000000" },
{ "KIG_Green - Inverted", "50d05040a040307030204020" },
{ "LCD_Green - Inverted", "e4fcd48cc474346c540c1c24" },
{ "Red - Inverted", "ffc0c0ff6060c00000600000" },
{ "Super_GB - Inverted", "ffefcede944aad2921311852" },
{ "Yellow - Inverted", "f8f078b0a848686830202010" },
{ "Yellow_BGB- Inverted", "f4ebbfd2cf4d93a73b556f35" },
{ "NGM_Brown - Inverted", "f8e088d8b058987838483818" },
} },
{ name = "GalleryWebApp Palettes", palettes = {
{ "Artistic Caffeinated Lactose", "2416069e754fdea963fdfef5" },
{ "AQP Pikes Peak", "2a201efa2b25868779ebeee7" },
{ "Azure Clouds", "00000012412732b66d47ff99" },
{ "BGB Emulator", "08182034685688c070e0f8d0" },
{ "Black & White", "000000555555aaaaaaffffff" },
{ "Black Zero", "2e463d385d49577b467e8416" },
{ "CGA Palette Crush 1", "000000ff55ff55ffffffffff" },
{ "CGA Palette Crush 2", "000000ff555555ffffffffff" },
{ "CMYKeystone", "373737fb00fa0be8fdffff00" },
{ "Candy Cotton Tower Raid", "3800168f0039e65790e6aec4" },
{ "Caramel Fudge Paranoia", "3f1711b01553cf7163cf9255" },
{ "Childhood in Greenland", "405010708028a0a840d0d058" },
{ "Cyanide Blues", "0e1e3d1e479321aff59efbe3" },
{ "Deep Haze Green", "00000027421f467818a1d909" },
{ "Dies ist meine Wassermelone", "222903558429f27d7affdbcb" },
{ "Drowning at Night", "030c2220293f586164a9b0b3" },
{ "DUN 2000 Remastered", "000000725441c09e7dfbf1cd" },
{ "Flowerfeldstrasse", "171f6275868fc5c5cee9d9cc" },
{ "Floyd Steinberg in Love", "4c1c2dd23c4e5fb1f5eaf5fa" },
{ "GB Light", "0b7a6d16a59619c7b31ddece" },
{ "GB Pocket", "1f1f1f4d533c8b956dc4cfa1" },
{ "Glowing Mountains", "2f1c35514f6ca1a8b8ffbf98" },
{ "Golden Elephant Curry", "0000004f3000c27600ff9c00" },
{ "Grafixkid Gray", "2b2b26706b66a89f94e0dbcd" },
{ "Grafixkid Green", "4c625a7b9278abc396dbf4b4" },
{ "Knee-Deep in the Wood", "2c24103c3ca9d5690ffffe6e" },
{ "LNK late Awakening", "5a39216b8c427bc67bffffb5" },
{ "MTR Aran Remixed", "2c1700b62558047e60aedf1e" },
{ "My Friend from Bavaria", "382977b60077df7925feda1b" },
{ "Nortorious Comadante", "0402ac04aaac54fefcfcfe54" },
{ "Original GB", "0f380f30623077a1129bbc0f" },
{ "Purple Rain", "68006aff00848570b2adfffc" },
{ "Romeros Garden", "323727574431649a57ebc4ab" },
{ "Rusted City Rain", "2c2137764462a96868edb4a1" },
{ "Space Haze Overload", "0b06306b1fb1cc3495f8e3c4" },
{ "Starlit Memories", "4f133f6f20966d53bd869ad9" },
{ "Sunflower Holidays", "000000881400ff5555ffff55" },
{ "Super Hyper Mega GB", "331e50a63725d68e49f7e7c6" },
{ "The death of Yung Columbus", "1d1414462917ff2261b5ff32" },
{ "The starry knight", "123f770f86b637cae5f5db37" },
{ "There is Always Money", "221a09977b25fed638fdfe0a" },
{ "Tramonto al Parco degli Acquedotti", "0c0a3e912978e64a4ef3c677" },
{ "VTB 1985", "000000520000db0000ff0000" },
{ "Waterfront Plaza", "10253342678e6f9edfcecece" },
{ "Youth Ikarus reloaded", "1e00009e0000f78e50cef7f7" },
} },
{ name = "GB Color (Combo Palettes)", palettes = {
{ "GBC - Down + A", "000000ff0000ffff00ffffff" },
{ "GBC - Down + B", "0000007b4a00ffff00ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
{ "GBC - Down", "0000009494ffff9494ffffa5" },
{ "GBC - Left + A", "00000052528c8c8cdeffffff", "000000943a3aff8484ffffff", "000000843100ffad63ffffff" },
{ "GBC - Left + B", "000000525252a5a5a5ffffff" },
{ "GBC - Left", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
{ "GBC - Right + A", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC - Right + B", "ffffffffde00008484000000" },
{ "GBC - Right", "000000ff420052ff00ffffff" },
{ "GBC - Up + A", "000000943a3aff8484ffffff", "0000000084007bff31ffffff", "0000000000ff63a5ffffffff" },
{ "GBC - Up + B", "5a3108846b29ce9c84ffe6c5", "000000843100ffad63ffffff", "000000843100ffad63ffffff" },
{ "GBC - Up", "000000843100ffad63ffffff" },
} },
{ name = "GB Color (Unique Palettes)", palettes = {
{ "GBC Palette 001 (Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 002 (World)", "000000006300ffff00a59cff" },
{ "GBC Palette 003 (USA, Europe)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 004 (USA, Europe)", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 005 (USA)", "ffffffffde00008484000000" },
{ "GBC Palette 006 (USA, Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 007 (USA, Europe)", "000000ff0000ff9c00ffffff" },
{ "GBC Palette 008 (World)", "ffffffffff00ff840052de00", "0000ff63a5ffffffffffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 009 (USA)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 010 (Europe)", "000000843100ffad63ffffff", "0000000084007bff31ffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 011 (Europe)", "000000843100ffad63ffffff", "0000000084007bff31ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 012 (Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 013 (World) (Rev A)", "000000ff0000ff9c00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 014 (World)", "000000ff0000ff9c00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 015 (Japan)", "00000052528c8c8cdeffffff", "4a0000943a00ffd600ffc542", "0000ffff00005abdffffffff" },
{ "GBC Palette 016 (USA, Europe)", "003a3a63947394b5ffffff9c", "4a0000943a00ffd600ffc542", "000000943a3aff8484ffffff" },
{ "GBC Palette 017 (USA, Europe)", "00000052528c8c8cdeffffff", "4a0000943a00ffd600ffc542", "0000ffff00005abdffffffff" },
{ "GBC Palette 018 (USA, Europe) (Rev A)", "00000052528c8c8cdeffffff", "4a0000943a00ffd600ffc542", "0000ffff00005abdffffffff" },
{ "GBC Palette 019 (USA, Europe)", "00000052528c8c8cdeffffff", "4a0000943a00ffd600ffc542", "0000ffff00005abdffffffff" },
{ "GBC Palette 020 (World) (Rev A)", "0000000000ff63a5ffffffff", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 021 (World)", "0000000000ff63a5ffffffff", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 022 (Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 023 (World) (Rev A)", "000000843100ffad63ffffff" },
{ "GBC Palette 024 (World)", "000000843100ffad63ffffff" },
{ "GBC Palette 025 (USA)", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 026 (Europe)", "000000843100ffad63ffffff" },
{ "GBC Palette 027 (Japan)", "000000b573007bff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 028 (Australia)", "000000b573007bff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 029 (Japan)", "000000b573007bff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 030 (Japan)", "00000042737badad84ffffff", "000000944200ff7300ffffff", "0000ffff00005abdffffffff" },
{ "GBC Palette 031 (Europe)", "000000b573007bff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 032 (USA) (Rev A)", "000000b573007bff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 033 (USA)", "000000b573007bff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 034 (World)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 035 (Japan) (Rev A)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "0084ffffff7bffffff0000ff" },
{ "GBC Palette 036 (Japan)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "0084ffffff7bffffff0000ff" },
{ "GBC Palette 037 (Japan)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "0084ffffff7bffffff0000ff" },
{ "GBC Palette 038 (USA, Europe)", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 039 (Japan)", "00000052528c8c8cdeffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 040 (USA, Europe)", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 041 (USA, Europe)", "00000052528c8c8cdeffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 042 (USA, Europe)", "00000052528c8c8cdeffffff", "000000943a3aff8484ffffff", "000000843100ffad63ffffff" },
{ "GBC Palette 043 (USA)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 044 (Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 045 (Japan)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "0084ffffff7bffffff0000ff" },
{ "GBC Palette 046 (Japan)", "000000843100ffad63ffffff" },
{ "GBC Palette 047 (Japan)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "000000630000d60000ff6352" },
{ "GBC Palette 048 (USA, Europe)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "0084ffffff7bffffff0000ff" },
{ "GBC Palette 049 (USA, Europe)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "0084ffffff7bffffff0000ff" },
{ "GBC Palette 050 (USA, Europe)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "0084ffffff7bffffff0000ff" },
{ "GBC Palette 051 (USA, Europe)", "000000006300ffff00a59cff", "000000630000d60000ff6352", "000000630000d60000ff6352" },
{ "GBC Palette 052 (USA, Europe)", "000000843100ffad63ffffff" },
{ "GBC Palette 053 (France)", "000000943a3aff8484ffffff", "004a0031840000ff00ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 054 (Germany)", "000000943a3aff8484ffffff", "004a0031840000ff00ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 055 (USA, Europe) (Rev A)", "000000943a3aff8484ffffff", "004a0031840000ff00ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 056 (USA, Europe) (Rev B)", "000000943a3aff8484ffffff", "004a0031840000ff00ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 057 (USA, Europe)", "000000943a3aff8484ffffff", "004a0031840000ff00ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 058 (Europe)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 059 (Europe)", "000000ff420052ff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 060 (Japan)", "000000843100ffad63ffffff" },
{ "GBC Palette 061 (USA, Europe)", "000000843100ffad63ffffff" },
{ "GBC Palette 062 (Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 063 (Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 064 (Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 065 (World)", "0000000000ff63a5ffffffff", "000000630000ff0000ffff00", "0000000084007bff31ffffff" },
{ "GBC Palette 066 (Japan)", "00000042737badad84ffffff", "000000944200ff7300ffffff", "000000944200ff7300ffffff" },
{ "GBC Palette 067 (USA, Europe)", "00000042737badad84ffffff", "000000944200ff7300ffffff", "000000944200ff7300ffffff" },
{ "GBC Palette 068 (Europe)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 069 (France)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 070 (Germany)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 071 (Europe)", "000000843100ffad63ffffff" },
{ "GBC Palette 072 (USA, Europe)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 073 (Europe)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 074 (USA)", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 075 (Japan)", "000000843100ffad63ffffff" },
{ "GBC Palette 076 (Europe)", "00000052528c8c8cdeffffff", "00000052528c8c8cdeffffff", "4a0000943a00ffd600ffc542" },
{ "GBC Palette 077 (USA)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 078 (Europe)", "00000052528c8c8cdeffffff", "4a0000943a00ffd600ffc542", "4a0000943a00ffd600ffc542" },
{ "GBC Palette 079 (Japan) (Rev A)", "0000009c6300ffce00ffffff" },
{ "GBC Palette 080 (Japan) (Rev 0A)", "000000ff0000ffff00ffffff" },
{ "GBC Palette 081 (Japan) (Rev B)", "000000ff0000ffff00ffffff" },
{ "GBC Palette 082 (Japan) (Rev C)", "000000ff0000ffff00ffffff" },
{ "GBC Palette 083 (Japan) (Rev D)", "000000ff0000ffff00ffffff" },
{ "GBC Palette 084 (Japan) (Rev A)", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 085 (Japan)", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 086 (Japan)", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 087 (Japan) (Rev A)", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 088 (Japan)", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 089 (Germany)", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 090 (USA, Europe)", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 091 (Spain)", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 092 (Spain)", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 093 (USA, Europe)", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 094 (Germany)", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 095 (France)", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 096 (France)", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 097 (Italy)", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 098 (Italy)", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 099 (World)", "000000ff0000ffff00ffffff", "000000ff0000ffff00ffffff", "0000ffff00005abdffffffff" },
{ "GBC Palette 100 (Japan)", "003a3a63947394b5ffffff9c" },
{ "GBC Palette 101 (USA, Europe)", "003a3a63947394b5ffffff9c" },
{ "GBC Palette 102 (Europe) (En,Fr,De)", "000000ff524affffff6bff00", "0000ff63a5ffffffffffffff", "000000843100ffad63ffffff" },
{ "GBC Palette 103 (World)", "ffffffffde00008484000000" },
{ "GBC Palette 104 (Europe)", "ffffffffde00008484000000" },
{ "GBC Palette 105 (USA)", "ffffffffde00008484000000" },
{ "GBC Palette 106 (USA, Europe) (Rev A)", "000000843100ffad63ffffff", "0000000084007bff31ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 107 (USA, Europe) (Rev A)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 108 (Japan)", "003a3a63947394b5ffffff9c", "4a0000943a00ffd600ffc542", "000000943a3aff8484ffffff" },
{ "GBC Palette 109 (World) (Rev A)", "000000ad5a42ffff94b5b5ff", "943a3aff8484ffffff000000", "943a3aff8484ffffff000000" },
{ "GBC Palette 110 (World)", "000000ad5a42ffff94b5b5ff", "943a3aff8484ffffff000000", "943a3aff8484ffffff000000" },
{ "GBC Palette 111 (USA, Europe) (Rev A)", "5a5a5a9c843163efefffffce", "000000944200ff7300ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 112 (USA, Europe) (Rev B)", "5a5a5a9c843163efefffffce", "000000944200ff7300ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 113 (USA, Europe)", "5a5a5a9c843163efefffffce", "000000944200ff7300ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 114 (Japan) (Rev B)", "5a5a5a9c843163efefffffce", "000000944200ff7300ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 115 (Japan)", "5a5a5a9c843163efefffffce", "000000944200ff7300ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 116 (USA, Europe)", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff", "0000000084007bff31ffffff" },
{ "GBC Palette 117 (World)", "000000ff524affffff6bff00", "0000ff63a5ffffffffffffff", "000000843100ffad63ffffff" },
{ "GBC Palette 118 (World) (Rev A)", "000000ff0000ffff00ffffff" },
{ "GBC Palette 119 (World)", "000000ff0000ffff00ffffff" },
{ "GBC Palette 120 (Europe)", "000000ff0000ffff00ffffff", "000000ff0000ffff00ffffff", "0000ffff00005abdffffffff" },
{ "GBC Palette 121 (USA)", "000000ff0000ffff00ffffff", "000000ff0000ffff00ffffff", "0000ffff00005abdffffffff" },
{ "GBC Palette 122 (USA)", "000000ff420052ff00ffffff", "000000ff420052ff00ffffff", "0000ffff00005abdffffffff" },
{ "GBC Palette 123 (USA, Europe) (Rev A)", "000000ff420052ff00ffffff", "000000ff420052ff00ffffff", "0000ffff00005abdffffffff" },
{ "GBC Palette 124 (USA, Europe)", "000000ff0000ff9c00ffffff" },
{ "GBC Palette 125 (Japan)", "000000ff0000ffff00ffffff", "000000ff0000ffff00ffffff", "0000ffff00005abdffffffff" },
{ "GBC Palette 126 (USA, Europe)", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 127 (USA)", "000000ff524affffff6bff00", "0000ff63a5ffffffffffffff", "000000843100ffad63ffffff" },
{ "GBC Palette 128 (Europe)", "000000ff524affffff6bff00", "0000ff63a5ffffffffffffff", "000000843100ffad63ffffff" },
{ "GBC Palette 129 (Europe)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 130 (USA, Europe)", "0000000084007bff31ffffff", "000000943a3aff8484ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 131 (USA, Europe)", "0000000063c57bff31ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 132 (World)", "00000042737badad84ffffff", "000000944200ff7300ffffff", "0000ffff00005abdffffffff" },
{ "GBC Palette 133 (USA, Europe)", "00000042737badad84ffffff", "000000843100ffad63ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 134 (USA, Europe)", "0000000000ff63a5ffffffff", "000000943a3aff8484ffffff", "ff00000084ffffff7bffffff" },
{ "GBC Palette 135 (Japan)", "000000525252a5a5a5ffffff" },
{ "GBC Palette 136 (Japan) (Rev A)", "000000843100ffad63ffffff" },
{ "GBC Palette 137 (Japan)", "000000843100ffad63ffffff" },
{ "GBC Palette 138 (USA)", "000000ff420052ff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 139 (Japan)", "000000ff0000ff9c00ffffff", "000000ff0000ff9c00ffffff", "0000ffff00005abdffffffff" },
{ "GBC Palette 140 (Japan)", "000000ff420052ff00ffffff", "000000ff420052ff00ffffff", "0000ffff00005abdffffffff" },
{ "GBC Palette 141 (Japan)", "000000ff420052ff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "GBC Palette 142 (USA, Europe)", "000000ff0000ff9c00ffffff", "000000ff0000ff9c00ffffff", "0000ffff00005abdffffffff" },
{ "GBC Palette 143 (Japan) (Rev A)", "000000943a3aff8484ffffff", "004a0031840000ff00ffffff", "0000000000ff63a5ffffffff" },
{ "GBC Palette 144 (Japan)", "000000943a3aff8484ffffff", "004a0031840000ff00ffffff", "0000000000ff63a5ffffffff" },
} },
{ name = "GB Color (Unused Palettes)", palettes = {
{ "Unused0", "000000b573007bff00ffffff", "000000943a3aff8484ffffff", "000000943a3aff8484ffffff" },
{ "Unused1", "000000ff0000ffff00ffffff", "0000000000ff63a5ffffffff", "0000ffff00005abdffffffff" },
{ "Unused2", "000000ad5a42ffff94b5b5ff", "943a3aff8484ffffff000000", "943a3aff8484ffffff000000" },
{ "Unused3", "ffffffffde00008484000000", "000000943a3aff8484ffffff", "0000000084007bff31ffffff" },
{ "Unused4", "000000525252a5a5a5ffffff" },
{ "Unused5", "0000009c6300ffce00ffffff" },
} },
{ name = "R.A.Helllord/Limited Edition Pockets", palettes = {
{ "Classic Blue LE", "0000000232741b559e5775af" },
{ "Classic Green LE", "00000001633f0b8a68549d80" },
{ "Classic Indigo LE", "0000002631725457a47b80b4" },
{ "Classic Pink LE", "000000d8648beb8cb3efa1bf" },
{ "Classic Red LE", "00000095030dbc2b2bcb5d5b" },
{ "Classic Silver LE", "000000908a85cececee0dfde" },
{ "Classic Spice Orange LE", "000000c96722e38f48e8a36c" },
{ "Classic Yellow LE", "000000cf8b08d2921de8be63" },
{ "Glow in the Dark LE", "00000005a56d4dcc808cf68a" },
{ "Transparent Blue LE", "0000001e124f361aa6646dcb", "1e124f361aa6646dcb7f8cff", "1e124f361aa6646dcb7f8cff" },
{ "Transparent Clear LE", "0000003e3b395959599c9a99", "3e3b395959599c9a99d7d7d7", "3e3b395959599c9a99d7d7d7" },
{ "Transparent Green LE", "000000003c0600b10003ce09", "003c0600b10003ce09afe4ab", "003c0600b10003ce09afe4ab" },
{ "Transparent Orange LE", "0000003c1405ad2d01e56400", "3c1405ad2d01e56400ebbf8f", "3c1405ad2d01e56400ebbf8f" },
{ "Transparent Purple LE", "0000003c2e498d6ea7a986bb", "3c2e498d6ea7a986bbe0d6e5", "3c2e498d6ea7a986bbe0d6e5" },
{ "Transparent Red LE", "0000006d1a1a991019c51d21", "6d1a1a991019c51d21eab5b1", "6d1a1a991019c51d21eab5b1" },
{ "Transparent Smoke LE", "000000282421363333686464", "282421363333686464bfbfbe", "282421363333686464bfbfbe" },
} },
{ name = "R.A.Helllord/Limited Edition Pockets Inverted", palettes = {
{ "Classic Blue LE - inverted", "5775af1b559e023274000000" },
{ "Classic Green LE - inverted", "549d800b8a6801633f000000" },
{ "Classic Indigo LE - inverted", "7b80b45457a4263172000000" },
{ "Classic Pink LE - inverted", "efa1bfeb8cb3d8648b000000" },
{ "Classic Red LE - inverted", "cb5d5bbc2b2b95030d000000" },
{ "Classic Silver LE - inverted", "e0dfdececece908a85000000" },
{ "Classic Spice Orange LE - inverted", "e8a36ce38f48c96722000000" },
{ "Classic Yellow LE - inverted", "e8be63d2921dcf8b08000000" },
{ "Glow in the Dark LE - inverted", "8cf68a4dcc8005a56d000000" },
{ "Transparent Blue LE - inverted", "646dcb361aa61e124f000000", "7f8cff646dcb361aa61e124f", "7f8cff646dcb361aa61e124f" },
{ "Transparent Clear LE - inverted", "9c9a995959593e3b39000000", "d7d7d79c9a995959593e3b39", "d7d7d79c9a995959593e3b39" },
{ "Transparent Green LE - inverted", "03ce0900b100003c06000000", "afe4ab03ce0900b100003c06", "afe4ab03ce0900b100003c06" },
{ "Transparent Orange LE - inverted", "e56400ad2d013c1405000000", "ebbf8fe56400ad2d013c1405", "ebbf8fe56400ad2d013c1405" },
{ "Transparent Purple LE - inverted", "a986bb8d6ea73c2e49000000", "e0d6e5a986bb8d6ea73c2e49", "e0d6e5a986bb8d6ea73c2e49" },
{ "Transparent Red LE - inverted", "c51d219910196d1a1a000000", "eab5b1c51d219910196d1a1a", "eab5b1c51d219910196d1a1a" },
{ "Transparent Smoke LE - inverted", "686464363333282421000000", "bfbfbe686464363333282421", "bfbfbe686464363333282421" },
} },
{ name = "R.A.Helllord/PPB", palettes = {
{ "PPB - Amber", "ffb13b8c61216043172d1f0b" },
{ "PPB - Blue", "28ccff16708c0f4b5e07242d" },
{ "PPB - Green", "00ee00008e00005f00002f00" },
{ "PPB - White", "c1ffff6a8c8c496060202b2b" },
{ "PPB inverted - Amber", "2d1f0b6043178c6121ffb13b" },
{ "PPB inverted - Blue", "07242d0f4b5e16708c28ccff" },
{ "PPB inverted - Green", "002f00005f00008e0000ee00" },
{ "PPB inverted - White", "202b2b4960606a8c8cc1ffff" },
} },
{ name = "SMY", palettes = {
{ "Desert", "3e2f30746657a49b83d2d0b1" },
{ "Evening", "012636185569539889aee4a6" },
{ "Fake Crystal", "21293129317b4a73bd9ce7f7" },
{ "Fog", "343c3756727386a39dbfd2c3" },
{ "Magic Eggplant", "36213c842e949d69c7b0e4f1" },
{ "Radioactive Pea", "00522106731f349e16b8ce03" },
{ "Seaweed", "15003f32654278a758dfe095" },
{ "Twilight", "15003f861246bd5462e9d397" },
} },
{ name = "Super GB", palettes = {
{ "1-A (Default)", "311852ad2921de944affefce" },
{ "1-B", "000000b55210ceb573dedec6" },
{ "1-C", "39399c9c3963ef9c52ffc6ff" },
{ "1-D", "521800ff0000c6844affffad" },
{ "1-E", "5a39216b8c427bc67bffdeb5" },
{ "1-F", "004210ad0000e78c52deefff" },
{ "1-G", "ffff5a7b7b0000a5ef000052" },
{ "1-H", "311800844200ffbd8cffefe7" },
{ "2-A", "000000297b00c68c4af7cea5" },
{ "2-B", "52005aff3100ffef52ffffff" },
{ "2-C", "29299c7b31efef8c8cffc6ff" },
{ "2-D", "000052ff310000ff00ffffa5" },
{ "2-E", "10081029106394b5e7ffce84" },
{ "2-F", "180000a50000ff9452d6ffff" },
{ "2-G", "001800e7bd84e752426bbd39" },
{ "2-H", "000000737373bdbdbdffffff" },
{ "3-A", "314a63ff632973c6c6ffd69c" },
{ "3-B", "001010005200e78421dedec6" },
{ "3-C", "21215a00bdffffff7be7adce" },
{ "3-D", "00000008ce00e7ad7bf7ffbd" },
{ "3-E", "524a73b57b21e7b56bffffc6" },
{ "3-F", "424242ffd600ff6bff7b7bce" },
{ "3-G", "390000ce3139ffffff63de52" },
{ "3-H", "0818004a8c187bce39e7ffa5" },
{ "4-A", "00007bd600d67badfff7ad6b" },
{ "4-B", "180808427b39efa563f7eff7" },
{ "4-C", "0800009ca5e7dea5d6ffe7e7" },
{ "4-D", "08214a4a6b7b94ceceffffbd" },
{ "4-E", "0021317b5a8ce7ad7bffdead" },
{ "4-F", "3900008400a5de84debdd6d6" },
{ "4-G", "008463291000bd215ab5e718" },
{ "4-H", "425229848c42bdc65affffce" },
} },
{ name = "Super GB 2 - Vaporwave Edition", palettes = {
{ "SGB2VE 1-A Sunset", "1039adef2994f7a552f7f794" },
{ "SGB2VE 1-B Kimochi WRO", "4a3973bd63a5f7a56bf7efbd" },
{ "SGB2VE 1-C Pachinko", "42427b9c42eff7947bf7deef" },
{ "SGB2VE 1-D Cotton Candy", "1039adffa5ff84e7eff7f7f7" },
{ "SGB2VE 1-E Absynthe", "7b4aada5b5ffadff5affff8c" },
{ "SGB2VE 1-F Arizona", "52529cff4273ffcecedefff7" },
{ "SGB2VE 1-G Hot Vibes", "8c18f7f72973f78c18f7ce18" },
{ "SGB2VE 1-H Sorbet Whip", "63184aff6b8cffc684f7ffc6" },
{ "SGB2VE 2-A Bufotoxin", "104a63009c84ef8c84f7de9c" },
{ "SGB2VE 2-B Dank", "4a184263528463b5bdf7f794" },
{ "SGB2VE 2-C Creampuff", "632963a573f7f79cadffdef7" },
{ "SGB2VE 2-D Modern Computing", "101018ff849c6bffbdf7f7f7" },
{ "SGB2VE 2-E Uranium Glass", "4a08ef9c42ef00deded6ff9c" },
{ "SGB2VE 2-F. Tenshi", "2142e7de5aceefadceadf7c6" },
{ "SGB2VE 2-G 87 Ninjas", "42427bd63994bdb5f7c6f7de" },
{ "SGB2VE 2-H Floral", "08101852b59cff849cefe7e7" },
{ "SGB2VE 3-A Mind Game", "4200f700b5f7f784f7f7f7f7" },
{ "SGB2VE 3-B Sewer Magic", "5a00848c7be784f7c6f7f794" },
{ "SGB2VE 3-C Galactic Rabbit Trip", "4a527352a5cebdceeff7f7ef" },
{ "SGB2VE 3-D Frenemy", "1039ad42d6d6ffd6b5bdffe7" },
{ "SGB2VE 3-E Agent Orange", "632963e76b84f7a56bffe742" },
{ "SGB2VE 3-F Backstreet", "294a8cff8cbdffceb5dedeff" },
{ "SGB2VE 3-G Eccojam", "8c42a5ad7be7946cff9cefff" },
{ "SGB2VE 3-H Sakura", "4a39b5ef73d6efb5bdf7efb5" },
{ "SGB2VE 4-A Vector", "1039ad9400f700b5f7f7d684" },
{ "SGB2VE 4-B Slumber", "944ab58c8cf794ceffd6ffff" },
{ "SGB2VE 4-C Lingonberry Butter", "21529c7b84e7ffadcefff7de" },
{ "SGB2VE 4-D Zangetsu", "104a6331a5c684e7eff7efbd" },
{ "SGB2VE 4-E Icy Hot", "8c00ffff39ced6c6ffbdfff7" },
{ "SGB2VE 4-F Night Driver", "214aff6b00c6ff31ef10006b" },
{ "SGB2VE 4-G Bug Hunt", "0839633952bdf7738cf7e7ad" },
{ "SGB2VE 4-H MGB-101", "004a84107ba500bdc621ffde" },
} },
{ name = "TheWolfBunny64/American Pop Culture Pack", palettes = {
{ "Absorbent and Yellow", "343b00687600aec600fff752" },
{ "BMO Ver.", "394c3d607f6699cca3c0ffcc" },
{ "BRB Pink", "480030790050c10080f200a1" },
{ "Bedrock Caveman Vision", "002f37005e6e009eb8ff7f00" },
{ "BKB Ver.", "6060002098f048f8e0f8f880" },
{ "BLS Pink", "492e357a4d59c47c8ef59bb2" },
{ "BBS Blue", "1e3a45326274509cba64c4e9" },
{ "BTC Green", "39422a5f6e4698b070bedc8d" },
{ "DNP Silver", "33383d555d668895a3abbbcc" },
{ "Fairly OddPalette", "3d1b2d7b365bce5a997bb850" },
{ "GRF Vision", "2d1309964220e59436f5ea8b" },
{ "HGW Goldius", "3631215b523891845ab6a571" },
{ "Invincible Yellow and Blue", "113c4622788d39c9ebfee566" },
{ "NCK Orange", "4c1e007f3300cc5200ff6700" },
{ "NJT Green", "28380b435e126b961d86bc25" },
{ "OPP Palette", "0015330047abd92121d3d3d3" },
{ "PTS Pink", "4c26297f4045cc666fff808b" },
{ "Retro Bogeda", "0000006408ffff6cfffbfd1b" },
{ "Rocket Portable Power", "09122d213f99bdd539fee998" },
{ "RGR Playtime Palette", "662d91ed1c24ffde17ffffff" },
{ "Sailor Spinach Green", "2434123d581e628c307bb03c" },
{ "SCD Mystery Ver.", "2a1a318f59a5f79321c6de31" },
{ "Smurfy Blue", "0d3747165c772394bf2cb9ef" },
{ "SPB Yellow", "4a45157b7424c5ba39f7e948" },
{ "SQW Sea Foam Green", "37403d5c6b6694aca4b9d7cd" },
{ "Swampy Ogre Green", "39400d606b179aab24c1d62e" },
{ "TMT Pink", "3815205e2436963957bc486d" },
{ "UNS Blue", "0020430136710256b4036ce2" },
} },
{ name = "TheWolfBunny64/Anime and Manga Pack", palettes = {
{ "ANX BLUE", "0f2d451a4c74297ab93499e8" },
{ "ALM Hero Palette", "212f79d12021f3db43eff0f0" },
{ "Anime DGV Ver.", "1b241d2d3d3148624f5b7b63" },
{ "Anime Expo Red", "47110f771d19be2f28ee3b33" },
{ "Anime Expo Ver.", "242a2d656e729ba3a6e5eaeb" },
{ "BRS Blood", "3806065d0a0a951010bb1414" },
{ "Blue Stripes Ver.", "2d2e2e5b5d5d999b9c8bd3e1" },
{ "CDC Pink", "430427e10e82eac3d6f2f4f7" },
{ "Colorful Horizons", "373939d1525260bdc7f6cf72" },
{ "CRR Orange", "49230a7a3a11c35d1bf47522" },
{ "DKU Alpha Emerald", "11332f1c564f2d8a7e39ad9e" },
{ "DKU Gamma Palette", "24262b166668b6bec8f0e8d6" },
{ "DKU Vigilante Palette", "13131538534e878b92ada89a" },
{ "DMS Gold", "3734195d572b948c44baaf56" },
{ "DRM Tricolor", "450000e6000000a8f4ffe800" },
{ "DGB Orange", "48270878410ec06817f0831d" },
{ "DYN Hero Palette", "16161629463dc84a31d1d0cc" },
{ "EV-01", "303345765898f99b2254cf54" },
{ "EDN Academy Uniform", "483e39cc4c47dcbc76f7f8f5" },
{ "FRG Pretense Palette", "0000008d0000697d738ea99b" },
{ "GDS VISION", "4723008e4700ee77009fa0a0" },
{ "GR5 Fever", "311d3da662cdc5cedff7f7f7" },
{ "GKU GT Gi", "1221313d6ea5f0ac18dbe3e6" },
{ "GKU Gi", "061222173f72e7612cf9f0e1" },
{ "Grand Ivory", "4140396c6b5fadab98d9d6be" },
{ "GRZ Coat", "3d0b32ce26a9fce72dfbfbfa" },
{ "HKG Orange", "462718754129bb6841ea8352" },
{ "KNS Sherbet", "440021890042e5006ef08200" },
{ "KWB Blue", "00283b004462006c9d0088c5" },
{ "Legendary SSJ", "31411b536d2d84ae48a6da5b" },
{ "LSA Orange!", "461c00752f00bc4b00eb5e01" },
{ "Perfect MJN Emperor", "2718328550a9a3b453fdc1bf" },
{ "Perfected ULI", "393c4060646c99a0acc0c8d8" },
{ "PRC Marble Raspberry", "400a1b6b112eab1b49d6225c" },
{ "PTG Gold", "3633275a5541908868b4aa82" },
{ "SYN Beast Silver", "2d2e344b4d57787c8c969baf" },
{ "SHN Green", "1b3a162d6125489c3b5ac34a" },
{ "STH Red", "4a18127c291ec64130f8523c" },
{ "SSJ 3", "4a3c107c641cc6a02cf8c838" },
{ "SSJ Blue Evolved", "082d3e0d4b681578a71b97d1" },
{ "SSJ Blue", "01383d025d660496a305bccc" },
{ "SSJ God", "40001d6b0131ac024ed70362" },
{ "SSJ Rose", "4a34357b5759c58c8ff7afb3" },
{ "SSJ", "4c4b397f7e60cbc99afefcc1" },
{ "SVC Uniform", "321d1a593d34ac7c59acaba9" },
{ "SWA Cyan", "1a3a432c6171479cb459c3e2" },
{ "TMR Uniform", "474f4d755e88e94e60eeefeb" },
{ "Tri DGV Ver.", "272a2442473c697260848f79" },
{ "UAH Uniform", "0c4856a02929a4aaafededed" },
{ "UTV VISION", "262c48e24465f1a7b5f4f4f4" },
{ "ULI Sign", "1b1f212d34374853585a686f" },
{ "URM Green", "1734282657443d8b6c4dae88" },
{ "VGT Armor", "0619321554a9e3c56cefefef" },
{ "ANM vision", "231815385eaaf9be00ffffff" },
{ "HLO blue", "063f5c3368d349c4f2b0edfa" },
} },
{ name = "TheWolfBunny64/Bandai Namco Pack", palettes = {
{ "1st Vision Pastel", "101c33385ead7faf5aea7cb1" },
{ "765P Ver.", "383a445d6272959cb6bbc4e4" },
{ "765P Pink", "481720792736c23f57f34f6d" },
{ "765P TRICOLOR", "0b143f2743d2e22b30b4e04b" },
{ "ARS BLUE", "0e192e182a4e26447c30559c" },
{ "AQR Blue", "0030450050740080ba00a0e9" },
{ "BDN Tricolor", "001f350069b1df4f61f6b700" },
{ "CND Blue", "0b263c1340641e67a02681c8" },
{ "IDOL WORLD TRICOLOR!!!", "0b263c2681c8f34f6dffc30b" },
{ "LLA Purple!", "31152e52234d84387ca5469b" },
{ "MLV GOLD!", "3d351d665930a48e4dcdb261" },
{ "MLN Yellow!", "4c3a037f6105cc9c08ffc30b" },
{ "Moonlight Vision", "1010103050783890e8f8d868" },
{ "MUS Pink", "44002672003fb60065e4007f" },
{ "NMC Idol Pink", "4c22377f3a5ccc5c93ff74b8" },
{ "NJG Orange", "482d00794c00c27900f39800" },
{ "PCM-PALETTE", "420000dc0000ff8c00ffd800" },
{ "PCM Vision", "0000003732ffffb897ffff00" },
{ "PCM Yellow", "4c44007f7100ccb500ffe300" },
{ "RADIANT SMILE RAMP", "1b1c81e6016b01b3c4fff89b" },
{ "RX-78-2 GDM Mode", "333f3fa73736dbad40dbdcd5" },
{ "RYG Sunset", "2b10399238befd99e1ffe43f" },
{ "SHINY Sky Blue", "2a384c465d7f7095cc8dbbff" },
{ "SNS Red", "3911105f1c1b982d2bbf3936" },
{ "School Idol Blue", "2830663960d787adf5f9f9f8" },
{ "School Idol Mix", "31152ea5469b00a0e9f39800" },
{ "SDM Green", "04392c075f4a0c98760fbe94" },
{ "Sunny Passion Paradise", "0920341e6caee06846f0ba40" },
{ "SYL Rose Pink", "452728744243b9696be88486" },
{ "Tarnished Gold", "392f18604e299a7d42c19d53" },
{ "Vulnerable Blue", "ffb897cc93787f5c4b3732ff" },
{ "Wind Ring Emerald", "0c3627145a4220906a28b585" },
} },
{ name = "TheWolfBunny64/Company Pack", palettes = {
{ "AMZ Vision", "252f3e008296ff9900ffffff" },
{ "AND Green", "1242271e6e4230b0693ddc84" },
{ "APL Silver", "30333351555681888aa2aaad" },
{ "DUO Green", "1a3d002c660146a30158cc02" },
{ "DRC Copper", "3c291b64442ea06d4ac8895d" },
{ "GGL Blue", "13274921427a346ac34285f4" },
{ "GGL Green", "0f32181a542929864234a853" },
{ "GGL Red", "46140f75211abb352aea4335" },
{ "GGL Yellow", "4b38017d5e02c89604fbbc05" },
{ "SPT Green", "09401c0f6b3018ac4c1ed760" },
} },
{ name = "TheWolfBunny64/Food and Drink Pack", palettes = {
{ "7EL Color Combo", "062218147350eb0f2aff6c00" },
{ "BJB Beach", "273d3b4e7a7683ccc5dbe441" },
{ "BJB Storm", "1f3a31346152539b8368c2a4" },
{ "BKR Sundae", "40202178284bc04078f05097" },
{ "BGK Color Combo", "502314d62300ff8732f5ebdc" },
{ "CHT Orange", "442300723b00b75e00e57600" },
{ "CRK Color Combo", "460d0a8d1b15ec2e24f99b2a" },
{ "CCL Vision", "000000f40009d7d7d7ffffff" },
{ "Dew Green", "2d40004b6b0078ac0097d700" },
{ "DMP Vision", "000000006491e31837ffffff" },
{ "DRP Red", "290a0e4511186e1b278a2231" },
{ "DNK Vision", "48032b910756f20c90ff6e0c" },
{ "KFR Red", "33070e550c17881325ab182f" },
{ "KKR Vision", "000000166938cf152dffffff" },
{ "Lemon-Lime Green", "18310e30631d51a631f1c545" },
{ "MTD Ver.", "29673cd82a34a1d23fffffff" },
{ "PCS BLUE", "001b35002e59004a8e015db2" },
{ "PPS Vision", "0000001414c8ff1400ffffff" },
{ "PZH Red", "441012711c1fb52c31e3383e" },
{ "SPR Green", "002e17004d27007c3e009b4e" },
} },
{ name = "TheWolfBunny64/Gaming Pack", palettes = {
{ "AVG Raging Volcano", "1c0000a81000f83800f8b800" },
{ "Blue Bomber Vision", "0000000d4dc4639afce2cda7" },
{ "BBN Blue", "093e4b0f687e18a7ca1fd1fd" },
{ "BBG Green", "1f420e356e1854b0276adc31" },
{ "CHE Green", "20800040980080c800a0e000" },
{ "Classic Blurple", "22294138446d5b6dae7289da" },
{ "DMG Pea Green", "204631527f39aec440d7e894" },
{ "EBT Prolouge", "2c17084a290b854a1dc08226" },
{ "GGR Orange", "45230e743b17ba5e25e9762f" },
{ "Giga Kiwi DMG", "384828607028a0a830d0e040" },
{ "Investigation Yellow", "4c4a077f7c0cccc714fff919" },
{ "Phantom Red", "4b0b117e131cca1e2dfd2639" },
{ "PYO Green", "15431024771b39b42b48e236" },
{ "SES Blue", "073e4c0c687f14a7cc19d1ff" },
{ "SGA Tokyo Blue", "00273f00416a0068a90082d4" },
{ "Scarlett Green", "2e48034d78057cc0089bf00b" },
{ "Slime Blue", "0e2a3d1746662570a32f8ccc" },
{ "SNC Mega Blue", "13274120426c3369ad4084d9" },
{ "STM Gray", "3b3a396261609d9c99c5c3c0" },
{ "TYV Brown", "372c1f5c4a34937654b89469" },
{ "Tropical Cyber Space", "1414394344c1ef58f763fdfb" },
{ "XBX Green", "2b3c1249641f74a03192c83e" },
} },
{ name = "TheWolfBunny64/Holiday Pack", palettes = {
{ "Christmas Gold", "39321660542599873cc0a94b" },
{ "Christmas Silver", "2c32354a545977868e95a8b2" },
{ "Christmas Ver.", "300f0fa0323220a465cbb96a" },
{ "Classy Christmas", "534d579e5c5e8bab95e8e7df" },
{ "Clover Green", "1136161c5a252d903b39b54a" },
{ "Cupid's Love Palette", "5e081eb51a3ae48397e4cdd3" },
{ "Festive Christmas", "0a1609224b21cc1902bb9b60" },
{ "Glacial Winter Blue", "2839434360716c9ab487c1e2" },
{ "Grinchy Green", "3639085b5f0e929816b7be1c" },
{ "Halloween Ver.", "2c13319540a5f68c00ffcc00" },
{ "Haunted Halloween", "2f094c9e1fffffa52ecaff37" },
{ "Independence Boy", "0000000a3161b31942ffffff" },
{ "Irish Green", "143923225f3b37985e45be76" },
{ "Spooky Purple", "2f253f4f3e697e63a89e7cd2" },
{ "Winter Christmas", "341113ae3b4065b08fdddddd" },
} },
{ name = "TheWolfBunny64/J-Pop Pack", palettes = {
{ "A48 Pink", "4923317b3b53c45e84f676a6" },
{ "ATG PARETTO", "030a5b7a5375c375bcf4a7eb" },
{ "BBM KITSUNE RED", "2c00034a000677000995000c" },
{ "H46 Blue", "1b39442d5f724898b75bbee5" },
{ "J-Pop Idol Sherbet", "260c2b8129905bbee5f19db5" },
{ "MKU Blue", "05333f08566a0d8aaa11add5" },
{ "NZU PrePlay Palette", "47120fef3f3500b8b0fff200" },
{ "N46 Purple", "260c2b401448672073812990" },
{ "OOR RED", "370f0c5c1a14942920b93429" },
{ "YSB AMARANTH", "480c1b79142dc12048f2285a" },
} },
{ name = "TheWolfBunny64/Japanese Company Pack", palettes = {
{ "ANA Flight Blue", "00146e223f9a3b8bc000b3f0" },
{ "FMM Vision", "00301300602600a040008cd6" },
{ "LWS BLUE", "001f3600345b0053920068b7" },
{ "NHK Silver Gray", "262626404040666666808080" },
{ "SIT SUPER BLUE", "0b2438133d5e1f6196277abc" },
{ "SIT SUPER GREEN", "0634270b5742118b6a16ae85" },
{ "SKO Timely Vision", "2021210050a54393e6ffbf00" },
{ "SB109 PASTEL", "5503a63dacb8fd87b2f2d53f" },
{ "SNR Pink", "83534de74b5af485a1f9c2d0" },
{ "TKS CLOUDY BLUE", "27363b415a6368909f82b5c7" },
{ "NCO sea green", "073a310c6152149c8319c3a4" },
} },
{ name = "TheWolfBunny64/K-Pop Pack", palettes = {
{ "BGT ARMY PURPLE", "26154040246c6639ac8048d8" },
{ "BKP BLINK PINK", "4932377a535dc38594f4a7ba" },
{ "EER COSMIC LATTE", "4c4a457f7c73ccc6b8fff8e7" },
{ "LSS FEARLESS BLUE", "26314a40527c6784c781a5f9" },
{ "NCZ YELLOW", "43451e717333b4b851e2e766" },
{ "SVT COOL CARAT", "2b323e57647d92a8d1f7cac9" },
{ "SHW GREEN", "22413e396d685cafa673dbd0" },
{ "SWN PINK", "48172579273ec13f63f24f7c" },
{ "SKD STAY PINK", "4c142d7f224ccc367aff4499" },
{ "TROPICAL TWC APRICOT", "4c1c30993961ff5fa2fcc89b" },
} },
{ name = "TheWolfBunny64/Legacy Palette Pack", palettes = {
{ "TDS VC Green", "4a4a4a5a8c429cef29bdff21" },
{ "TDS VC Ver.", "2929186b6b52a5a58ccecead" },
{ "BLL PC Screen", "000000783800b86000f87800" },
{ "DMG Ver.", "2a453b365d48577c447f860f" },
{ "DGV Ver.", "2a2a2246463970705c8c8c73" },
{ "GMT Ver.", "12424c255955437a636ba64a" },
{ "GNW LCD", "3b3c336365569ea28ac6cbad" },
{ "GMK Ver.", "184421405d3b6b9c638cce94" },
{ "GBU Classic Ver.", "26302540503f67806481a17e" },
{ "HGM Ver.", "2d2d2b414f53687f84829fa6" },
{ "Light Ver.", "004f3b00694a009a7100b581" },
{ "LKA DX Ver.", "58382068884078c078f8f8b0" },
{ "MBM Ver.", "303030505050808080a0a0a0" },
{ "MGM V Ver.", "08203040689070a0e0d0d0d0" },
{ "NGP Ver.", "101010707070b0b0b0f0f0f0" },
{ "NKA 3310 Ver.", "2231273953425c846973a684" },
{ "Pocket Tales Ver.", "00000038500088a000d0d860" },
{ "Pocket Ver.", "1f1f1f4d533c8b956dc4cfa1" },
{ "PKS Ver.", "2d2d284b4b4378786c969687" },
{ "PKM Pinball Ver.", "181820786030a0b050e8f8b8" },
{ "PKM Ver.", "181010807098f0b088f8e8f8" },
{ "PKM mini Ver.", "1b2d1b525c52849484a5b9a5" },
{ "PKT Ver.", "10281838503050805070b070" },
{ "Rocky-Valley Holiday", "204008805850d89078c0f0f8" },
{ "T83 Ver.", "2e332a4e55467c88709caa8c" },
{ "TMG Ver.", "3c383878787cc0c0c7f1f0f9" },
{ "TGC Ver.", "0000000f4f2f6f8f4fdfff8f" },
{ "Timing Hero Ver.", "202e004c67188c994ccccc99" },
{ "Travel Wood", "482810705030a08058f8d8b0" },
{ "VMU Ver.", "0814804466546ca38688cca8" },
{ "VTB Ver.", "ff0000aa0000550000000000" },
{ "WTS Ver.", "0c322c2c626454a68c7cc67c" },
{ "WDS Ver.", "2c2c2c686868c0c0c0fefefe" },
} },
{ name = "TheWolfBunny64/Misc. Palette Pack", palettes = {
{ "Camouflage Ver.", "37353879533dac7e54bcab90" },
{ "Cherry Blossom Pink", "482534783f58c0648cf07eb0" },
{ "Emerald Green", "183c2428643c40a06050c878" },
{ "Fool's Gold and Silver", "2d30345a606997a1b0c5c66d" },
{ "Glitchy Blue", "0f254b193f7d2864c8337efb" },
{ "Gold, Silver, and Bronze", "2d1f1499684386949abeb049" },
{ "Golden Trophy", "453e0774680cb9a613e8d018" },
{ "Greenscale Ver.", "0c360c2c62346e870a9cbe0c" },
{ "Leprechaun Green", "10281d1b44302c6c4d378861" },
{ "Nightvision Green", "66bf2f519825335f171e390e" },
{ "Rising Sun Red", "38000d5d0016960024bc002d" },
{ "Treasure Gold", "3c360a655a12a2901ccbb524" },
{ "Wild West Vision", "3a160e924a36c3976ad9d7c7" },
} },
{ name = "TheWolfBunny64/Console Pack", palettes = {
{ "Advanced Indigo", "241f373c355d605494796aba" },
{ "Aegis Cherry", "42111e6e1d32b02f50dd3b64" },
{ "Ancient HSU Brown", "352f2b594f488f7f73b39f90" },
{ "ANC Green", "002e25004d3f007c64009b7e" },
{ "Brilliant Diamond Blue", "2638433f5d706595b47fbbe1" },
{ "COLLECTION of SAG Ver.", "041820345d51769a67b2c0a8" },
{ "Champion's Tunic", "281b13875b40009edde2dcb1" },
{ "CHZ Blue", "1735432759703e8fb44eb3e1" },
{ "DKG Arcade Blue", "15304223516f3881b147a2de" },
{ "DKG Barrel Brown", "3a220e613a179c5c25c3742f" },
{ "DMG-099", "3132313f642f6bad1984b510" },
{ "DMG-GOLD", "30361c505a3080904ca1b560" },
{ "DMG-SWITCH", "214231426b296c94218cad28" },
{ "DRL GB Ver.", "48530e788b1db9d03af6ff70" },
{ "EVE Brown", "3c2a0f644619a07028c88d32" },
{ "FMC Disk Yellow", "483a00796100c29b00f3c200" },
{ "FMC Frenzy", "231916a32135d9be72efecda" },
{ "Frog Coin Green", "00390039840000ef00fff7de" },
{ "Fury Blue", "0c1c2d152f4c224c792b5f98" },
{ "GMC Glimmer", "3e1318cf415111a396b6bed3" },
{ "Golden Wild", "372f1e5c4f32947f50b99f65" },
{ "GMB Brown", "331a11552c1d88472faa593b" },
{ "Green Awakening", "000b1636705898db75f1ffdd" },
{ "Green Banana", "2149003969394a9e0063df08" },
{ "INK Tricolor", "1c114c603bffff505eeaff3d" },
{ "LCD Clock Green", "183626285a4040906650b580" },
{ "LBO Fawn", "4033226b5539ac885cd7aa73" },
{ "MRO Red", "430400700700b40c00e10f00" },
{ "Metallic PLD Brass", "302d0f514c1a817929a29834" },
{ "Neon Blue", "033745055c730894b80ab9e6" },
{ "Neon Green", "0942000f6e0018b0001edc00" },
{ "Neon Orange", "4b30017d5002c88004faa005" },
{ "Neon Pink", "4c0f247f193ccc2860ff3278" },
{ "Neon Purple", "3600455a00739000b8b400e6" },
{ "Neon Red", "4c120c7f1e14cc3020ff3c28" },
{ "Neon Yellow", "454c00737f00b8cc00e6ff00" },
{ "Odyssey Boy", "2224215054457e8e67acbe8c" },
{ "Odyssey Gold", "3a30006150009b8000c2a000" },
{ "POCKET SWT", "3038206372518d9c7bb5c69c" },
{ "PKC Yellow", "4c42007f6e00ccb000ffdc00" },
{ "PKD Red", "461918752a28bb4340ea5450" },
{ "Royal Blue", "151949232a7a3844c44655f5" },
{ "Shining Pearl Pink", "3f2a30694750a87180d28ea0" },
{ "Sky Pop Ivory", "52502586825cbebb95e5e0b8" },
{ "Super FMC Supreme", "0846bad9404044ac71feda5a" },
{ "Super Mushroom Vision", "000000923404cc9e22f7cec3" },
{ "Super Star Pink", "440d2372163bb7235fe52c77" },
{ "Superball Ivory", "646432828250bcbc8aeef0bc" },
{ "Timeless Gold and Red", "37090a6f1215b91e23c8aa50" },
{ "Ultra Black", "17181d2629313d414f4d5263" },
{ "WRW MicroBlue", "05293c0844650d6da11189ca" },
{ "Wonder Purple", "451a42732c6fb846b2e658df" },
{ "Yellow Banana", "734900ad6939de9e00ffdf08" },
{ "YSH Egg Green", "1e3a0e336218519c2666c430" },
} },
{ name = "TheWolfBunny64/Sports Pack", palettes = {
{ "MLB Vision", "041e42bf0d3e057affffffff" },
{ "NASCAR Ver.", "000000007ac2e4002bffd659" },
{ "NBA Vision", "000000253b73c8102effffff" },
{ "NCAA Blue", "002e42004e6f007cb1009cde" },
{ "NFL Vision", "000000013369d50a0affffff" },
{ "OLY Bronze", "3d2618664029a46741cd8152" },
{ "OLY Gold", "3f360a6a5b12aa911cd5b624" },
{ "OLY Silver", "2f312e4f524e7e847c9ea59c" },
{ "PGA Tour Vision", "000000003c80f1373dffffff" },
{ "WWE White and Red", "40070c810e19d7182affffff" },
} },
{ name = "TheWolfBunny64/Traditional JPN Colors Pack", palettes = {
{ "Aquatic Iro", "192f603e62ad2ca9e1a0d8ef" },
{ "Fruity Orange", "241a089f563af08300f3bf88" },
{ "Ghostly Aoi", "0f235019448e5a79ba84a2d4" },
{ "Golden Kiiro", "6a5d21a69425dccb18f8e58c" },
{ "Lime Midori", "4759507b8d42aacf53e0ebaf" },
{ "Oni Aka", "640125a22041d9333fec6d71" },
{ "Sakura Pink", "a25768e7609eeebbcbfdeff2" },
{ "Silver Shiro", "383c3c727171afafb0dcdddd" },
{ "Tea Midori", "33363102876088cb7fd6e9ca" },
{ "Wisteria Murasaki", "2e29307058a3a59acadbd0e6" },
} },
{ name = "Trashuncle/Artistic", palettes = {
{ "TU PSL Park", "9200aeb722d4d442f0ec89ff" },
{ "TU Quicksilver", "555555717286a4a5c3bbbce1" },
} },
{ name = "Trashuncle/Crush", palettes = {
{ "TU Blueberry Crush", "0000002811ff4dffffffffff" },
{ "TU Grape Crush", "0000003d43cb0083ebffffff" },
{ "TU Kiwi Crush", "000000059e322eef7affffff" },
{ "TU Lavender Crush", "000000812482ab6a9bffffff" },
{ "TU Orange Crush", "000000c23615ff632cffffff" },
{ "TU Pineapple Crush", "000000ffff00ffff80ffffff" },
{ "TU Strawberry Crush", "000000ea1211ff5250ffffff" },
} },
{ name = "Trashuncle/DMG", palettes = {
{ "TU DMG Bright", "0c3a1d2a61414c7b2b919707" },
{ "TU DMG Clean", "1749022b5402496503788603" },
{ "TU DMG Weak Alt", "2c4d2054733c597730677f31" },
{ "TU DMG Weak", "364e1a5772405e7a406d833d" },
} },
{ name = "Trashuncle/GBP", palettes = {
{ "TU GBP Bright", "5b5d5a879481abb8a7ffffff" },
{ "TU GBP Clean", "3d493b98a291adbfacedfff4" },
{ "TU GBP Weak Alt", "4c4b3170734e757952848459" },
{ "TU GBP Weak", "434738535a42535f49667159" },
} },
{ name = "Trashuncle/Light", palettes = {
{ "TU Light Bright", "00637400a1b100daea00e3f3" },
{ "TU Light Clean", "007577019fa000aeaf01b4b9" },
{ "TU Light Dark", "0076a80092d10098c500a5f2" },
{ "TU Light Idealized", "006e9d0093c500a6cc00ccd5" },
} },
}
+55 -22
View File
@@ -34,6 +34,12 @@ local RomText = require("src.core.RomText")
local Strings = require("src.core.Strings")
local WideBattle = require("src.battle.WideBattle")
local Chrome2
do
local ok, v = pcall(require, "src.ui.gen2.Chrome")
Chrome2 = ok and v or nil
end
local romText = RomText
local BattleState = {}
@@ -461,7 +467,7 @@ function BattleState:picImage(img)
-- palettes (CYANMON reds 0.678/0.451) already rendered correctly. This mode
-- set mirrors PaletteFX.ensureZones / effectiveColors -- keep them in sync.
local mono = PaletteFX.mode == "og" or PaletteFX.mode == "og_inv"
or PaletteFX.mode == "classic"
or PaletteFX.mode == "classic" or PaletteFX.forcesRawGrays()
if self.grayPics or mono then return grayImage(img) end
-- SET_PAL_BATTLE_BLACK covers every battle palette slot, so the pics go
-- dark with the HP bars while the blackout text is up (#292). The intro
@@ -5989,7 +5995,7 @@ function BattleState:drawZonePass(src, sx, sy)
-- only the other two showed it. Keep this mode set in sync with picImage /
-- PaletteFX.ensureZones / WideBattle.monoMode.
local mono = PaletteFX.mode == "og" or PaletteFX.mode == "og_inv"
or PaletteFX.mode == "classic"
or PaletteFX.mode == "classic" or PaletteFX.forcesRawGrays()
love.graphics.setColor(1, 1, 1, 1)
love.graphics.setShader(shader)
local shaking = sx ~= 0 or sy ~= 0
@@ -6399,8 +6405,35 @@ end
function BattleState:drawTextArea()
if not self:bottomUIVisible() then return end
Font.drawBox(0, 12, 20, 6)
love.graphics.setColor(0, 0, 0, 1)
-- Gold only: routes the command box and its labels through GbcPalette,
-- the same fix TextBox.lua got for dialogue. moveSelect/mimicSelect below
-- aren't migrated yet since they need their own interior-patch handling
-- (see #240).
local gold = self.game and self.game.save
and (self.game.save.generation == 2 or self.game.save.version == "gold")
local Chrome = gold and Chrome2 or nil
local function box(tx, ty, tw, th)
if Chrome then
Chrome.paletteBox(tx, ty, tw, th)
else
Font.drawBox(tx, ty, tw, th)
love.graphics.setColor(0, 0, 0, 1)
end
end
local drawGlyph, finishGlyph = Font.drawCode, nil
if Chrome then
local _, dg, fg = Chrome.paletteGlyphs(Chrome.DEFAULT_BOX_PALETTE)
drawGlyph, finishGlyph = dg, fg
end
local function text(str, x, y)
local pen = x
for _, code in ipairs(Font.encode(str)) do
drawGlyph(code, pen, y)
pen = pen + Font.advanceOf(code)
end
end
box(0, 12, 20, 6)
if self.phase == "messages"
and (self.current or self.animPlaying or self.msgHold) then
-- during the move animation self.current is nil but shown still holds
@@ -6419,48 +6452,47 @@ function BattleState:drawTextArea()
for li, line in ipairs(self.shown or {}) do
local y = (ys[li] or 128) + off
for i = 1, #line do
Font.drawCode(line[i], 8 + (i - 1) * 8, y)
drawGlyph(line[i], 8 + (i - 1) * 8, y)
end
end
-- the blinking down arrow ('▼', glyph $EE) while a \v CONT wait
-- (_ContText) or a typed-out page (PromptText) holds the box; both write
-- it at (18,16), bottom-right, like TextBox / home/text.asm (#317)
if (self.msgWaiting or self.msgPrompt) and self.frame % 60 < 30 then
Font.drawCode(0xEE, (0 + 20 - 2) * 8, (12 + 6 - 1) * 8 - 4)
drawGlyph(0xEE, (0 + 20 - 2) * 8, (12 + 6 - 1) * 8 - 4)
end
elseif self.phase == "menu" and self.demo then
-- the old-man script (DisplayBattleMenu, core.asm:2038-2049): the
-- standard menu, with the '▶' hand drawn by the scripted keystrokes
-- -- next to FIGHT (9,14) for the first 80 frames, then ITEM (9,16)
Font.drawBox(8, 12, 12, 6)
love.graphics.setColor(0, 0, 0, 1)
Font.draw(Strings("FIGHT", "battle"), 80, 112)
Font.drawCode(0xE1, 128, 112); Font.drawCode(0xE2, 136, 112)
Font.draw(Strings("ITEM", "battle"), 80, 128); Font.draw(Strings("RUN", "battle"), 128, 128)
Font.drawCode(0xED, 72, (self.demoTimer or 0) <= 80 and 112 or 128)
box(8, 12, 12, 6)
text(Strings("FIGHT", "battle"), 80, 112)
drawGlyph(0xE1, 128, 112); drawGlyph(0xE2, 136, 112)
text(Strings("ITEM", "battle"), 80, 128); text(Strings("RUN", "battle"), 128, 128)
drawGlyph(0xED, 72, (self.demoTimer or 0) <= 80 and 112 or 128)
elseif self.phase == "menu" then
local col = (self.menuIndex - 1) % 2
local row = math.floor((self.menuIndex - 1) / 2)
if self.safari then
-- SAFARI_BATTLE_MENU_TEMPLATE: full-width box, "BALLx BAIT /
-- THROW ROCK RUN" from (2,14)
Font.drawBox(0, 12, 20, 6)
Font.draw(Strings("BALLx"), 16, 112); Font.draw(Strings("BAIT"), 112, 112)
Font.draw(Strings("THROW ROCK"), 16, 128); Font.draw(Strings("RUN", "battle"), 112, 128)
box(0, 12, 20, 6)
text(Strings("BALLx"), 16, 112); text(Strings("BAIT"), 112, 112)
text(Strings("THROW ROCK"), 16, 128); text(Strings("RUN", "battle"), 112, 128)
-- DisplayBattleMenu .safariLeftColumn / .safariRightColumn print
-- wNumSafariBalls at hlcoord 7,14 with `lb bc, 1, 2` -- one byte, two
-- digits, space padded -- right after the "BALLx" label at columns
-- 2..6 (engine/battle/core.asm:2074-2079, 2107-2112) (#540)
Font.draw(("%2d"):format(self.safari.balls), 56, 112)
Font.drawCode(0xED, (col == 0 and 8 or 104), 112 + row * 16)
text(("%2d"):format(self.safari.balls), 56, 112)
drawGlyph(0xED, (col == 0 and 8 or 104), 112 + row * 16)
else
-- BATTLE_MENU_TEMPLATE: box (8,12)-(19,17), "FIGHT <PK><MN> /
-- ITEM RUN" from (10,14); cursor columns 9 / 15
Font.drawBox(8, 12, 12, 6)
Font.draw(Strings("FIGHT", "battle"), 80, 112)
Font.drawCode(0xE1, 128, 112); Font.drawCode(0xE2, 136, 112)
Font.draw(Strings("ITEM", "battle"), 80, 128); Font.draw(Strings("RUN", "battle"), 128, 128)
Font.drawCode(0xED, (col == 0 and 72 or 120), 112 + row * 16)
box(8, 12, 12, 6)
text(Strings("FIGHT", "battle"), 80, 112)
drawGlyph(0xE1, 128, 112); drawGlyph(0xE2, 136, 112)
text(Strings("ITEM", "battle"), 80, 128); text(Strings("RUN", "battle"), 128, 128)
drawGlyph(0xED, (col == 0 and 72 or 120), 112 + row * 16)
end
elseif self.phase == "moveSelect" then
-- pokered MoveSelectionMenu: move list in a box at (4,12) 16x6,
@@ -6535,6 +6567,7 @@ function BattleState:drawTextArea()
Font.drawCode(0xED, 8, (7 + self.mimicIndex) * 8)
Font.draw(Strings("WHICH TECHNIQUE?"), 8, 112)
end
if finishGlyph then finishGlyph() end
end
function BattleState:draw()
+1 -5
View File
@@ -26,14 +26,10 @@ local WideBattle = {
FIELD_BOTTOM = 104,
}
-- The forced-mono display modes re-threshold the whole finished frame
-- through the shade shader, and picImage hands them raw DMG grays for that
-- (#207). The wide layout has to know: it exposes a matching whole-surface
-- zone and leaves the HP bar fill gray, exactly like the zone pass does in
-- the classic layout (#229). Keep in sync with picImage / ensureZones.
local function monoMode()
local m = PaletteFX.mode
return m == "og" or m == "og_inv" or m == "classic"
or PaletteFX.forcesRawGrays()
end
local function shownHP(battler)
+1
View File
@@ -1258,6 +1258,7 @@ function Game:applyOptions(opts)
if Sound.applyOptions then Sound.applyOptions(opts) end
require("src.render.PaletteFX").applyOptions(opts)
require("src.render.Tilt").applyOptions(opts)
require("src.render.Letterbox").applyOptions(opts)
-- after Tilt, so a persisted world pipeline can switch the tilt level it
-- just restored back off (the two are mutually exclusive)
require("src.render.Pipelines").applyOptions(opts)
+2
View File
@@ -1787,6 +1787,7 @@ function Game2:hotkey(key)
local GbcPalette = require("src.render.GbcPalette")
GbcPalette.setMode(options.color or "gbc")
options.color = GbcPalette.cycle(1)
options.palette = ""
persist()
return true
elseif key == "3" then
@@ -2040,6 +2041,7 @@ function Game2:applyOptions()
Zoom.allowSurvey = caps.survey
if not caps.survey and Zoom.offset < 0 then Zoom.offset = 0 end
require("src.render.Tilt").applyOptions(options)
require("src.render.Letterbox").applyOptions(options)
require("src.render.GbcPalette").applyOptions(options)
-- engine/gfx/load_font.asm:29 LoadFrame, off options.lua's wTextboxFrame.
Font.setFrame(options.frame or 1)
+1
View File
@@ -313,6 +313,7 @@ function SaveData.defaultOptions()
zoom = 0,
-- OVERWORLD beyond-edge fill: trees | water | black
voidFill = "trees",
uiLetterbox = "auto",
-- windowed | borderless (desktop fullscreen); ignored on mobile
videoMode = "windowed",
-- lock the window to an exact 160x144 multiple, 1..4 (0 = OFF); see
+2
View File
@@ -309,6 +309,7 @@ Save.DEFAULT_OPTIONS = {
-- Game Boy. The Gen 1 save's equivalent key is `colors` (SGB packs), which
-- means something different, hence the different name.
color = "gbc",
palette = "",
videoMode = "windowed",
fpsCap = 60,
-- BATTLE BG (#1709): white | black, the surround around the battle screen.
@@ -316,6 +317,7 @@ Save.DEFAULT_OPTIONS = {
-- VOID FILL: fade | water | trees | black. fade is each map header's own
-- border block with the dissolve across a boundary (#1418).
voidFill = "fade",
uiLetterbox = "auto",
musicVol = 7, -- 0-7, like the GB's NR50 master volume
sfxVol = 7, -- 0-7
musicFilter = 0, -- low-pass steps, 0 = off
+1
View File
@@ -605,6 +605,7 @@ local function gen2Rows(opts, hooks, shared)
end
opts.color =
GbcPalette.MODES[wrapIndex(idx - 1 + dir, #GbcPalette.MODES) + 1]
opts.palette = ""
return true
end)
end
+40 -5
View File
@@ -18,8 +18,6 @@
-- colour IS its palettes -- so one substitution at this seam recolours the
-- whole game without a single screen knowing about it:
--
-- GBC the cart's own palettes. The default: this is a Game Boy
-- Color game and its colour is the point.
-- DMG the original grey Game Boy. Every palette collapses to the
-- four hardware shades, and the sheets that are drawn straight
-- (chrome, text) are already those shades, so the screen is
@@ -32,9 +30,12 @@
local GbcPalette = {}
GbcPalette.MODES = { "gbc", "dmg", "classic" }
GbcPalette.MODE_LABELS = { gbc = "GBC", dmg = "DMG", classic = "CLASSIC" }
GbcPalette.MODE_LABELS = { gbc = "GEN 2", dmg = "DMG", classic = "CLASSIC",
custom = "GBC" }
GbcPalette.mode = "gbc"
GbcPalette.CUSTOM_MODE = "custom"
-- rBGP's four shades as the hardware shows them.
local DMG_SHADES = {
{ 255, 255, 255 }, { 170, 170, 170 }, { 85, 85, 85 }, { 0, 0, 0 },
@@ -162,11 +163,22 @@ local function channel(colors, index)
return (c[1] or 0) / 255, (c[2] or 0) / 255, (c[3] or 0) / 255
end
GbcPalette.customRamp = nil
function GbcPalette.setCustomRamp(ramp)
local prev = GbcPalette.customRamp
GbcPalette.customRamp = ramp
if (prev ~= nil) ~= (ramp ~= nil) or prev ~= ramp then
pcall(function() require("src.render.SpriteRenderer").invalidate() end)
end
end
-- What a palette actually draws as under the current COLOR mode. Anything
-- that reads a colour out of a palette directly -- a canvas cleared to BG
-- colour 0, say -- has to go through this too, or the backdrop would keep its
-- cart colour while everything on top of it went grey.
function GbcPalette.resolve(colors)
if GbcPalette.customRamp then return GbcPalette.customRamp end
if GbcPalette.mode == "gbc" then return colors end
return DMG_SHADES
end
@@ -249,6 +261,10 @@ function GbcPalette.presentColors()
end
function GbcPalette.setMode(mode)
if mode == GbcPalette.CUSTOM_MODE then
GbcPalette.mode = mode
return mode
end
for _, name in ipairs(GbcPalette.MODES) do
if name == mode then
GbcPalette.mode = mode
@@ -260,7 +276,7 @@ function GbcPalette.setMode(mode)
end
function GbcPalette.modeLabel(mode)
return GbcPalette.MODE_LABELS[mode or GbcPalette.mode] or "GBC"
return GbcPalette.MODE_LABELS[mode or GbcPalette.mode] or "GEN 2"
end
-- Advance GBC -> DMG -> CLASSIC -> GBC. `delta` may be -1 to step back, so
@@ -273,11 +289,20 @@ function GbcPalette.cycle(delta)
local count = #GbcPalette.MODES
at = (at - 1 + (delta or 1)) % count + 1
GbcPalette.mode = GbcPalette.MODES[at]
GbcPalette.setCustomRamp(nil)
return GbcPalette.mode
end
function GbcPalette.applyOptions(opts)
return GbcPalette.setMode(opts and opts.color or "gbc")
local mode = GbcPalette.setMode(opts and opts.color or "gbc")
local id = opts and opts.palette
if id and id ~= "" then
local ok, Palette = pcall(require, "src.render.Palette")
GbcPalette.setCustomRamp(ok and Palette.ramp(id) or nil)
else
GbcPalette.setCustomRamp(nil)
end
return mode
end
-- Point the shader at one 4-color palette. Colors are 0-255 triples, matching
@@ -431,4 +456,14 @@ function GbcPalette.with(colors, body)
return applied
end
function GbcPalette.withRaw(colors, body)
local previous = love and love.graphics and love.graphics.getShader
and love.graphics.getShader() or nil
local applied = GbcPalette.useRaw(colors)
local ok, err = pcall(body)
if love and love.graphics then love.graphics.setShader(previous) end
if not ok then error(err, 0) end
return applied
end
return GbcPalette
+72
View File
@@ -0,0 +1,72 @@
-- UI LETTERBOX: what fills the window around a 160x144 screen.
--
-- AUTO keeps what each screen was authored with (Gen 1 black, Gen 2 mostly
-- white, the Pokedex and credits black); the other three override every
-- screen uniformly. PALETTE takes the active COLORS ramp's paper shade, so
-- the bars match the panel they frame instead of a fixed grey.
local Letterbox = {}
-- Set from applyOptions on both generations, the same shape GbcPalette.setMode
-- and Tilt.setLevel use, so a draw call needs no options table in hand.
Letterbox.mode = "auto"
Letterbox.MODES = { "auto", "black", "white", "palette" }
local LABELS = {
auto = "AUTO", black = "BLACK", white = "WHITE", palette = "PALETTE",
}
function Letterbox.normalize(mode)
for _, id in ipairs(Letterbox.MODES) do
if mode == id then return id end
end
return "auto"
end
function Letterbox.label(mode)
return LABELS[Letterbox.normalize(mode)]
end
function Letterbox.cycle(mode, dir)
local at = 1
for i, id in ipairs(Letterbox.MODES) do
if id == Letterbox.normalize(mode) then at = i break end
end
local n = #Letterbox.MODES
return Letterbox.MODES[(at - 1 + (dir or 1)) % n + 1]
end
-- r, g, b for `mode`, falling back to the screen's authored colour on AUTO.
--
-- `paper` is supplied by the caller, not looked up here: the two generations
-- keep their picked palette in different modules (Gen 1 in PaletteFX, Gen 2 in
-- GbcPalette) and GbcPalette.available() is true on both, so anything that
-- sniffed for it answered with Gen 2's white default on a Gen 1 game.
function Letterbox.color(mode, r, g, b, paper)
mode = Letterbox.normalize(mode)
if mode == "black" then return 0, 0, 0 end
if mode == "white" then return 1, 1, 1 end
if mode == "palette" and paper then
local pr, pg, pb = paper()
if pr then return pr, pg, pb end
end
return r or 0, g or 0, b or 0
end
function Letterbox.setMode(mode)
Letterbox.mode = Letterbox.normalize(mode)
return Letterbox.mode
end
function Letterbox.applyOptions(options)
return Letterbox.setMode(options and options.uiLetterbox)
end
-- What a drawWidescreen should paint the window with, given the colour that
-- screen was authored to use and its generation's paper reader.
function Letterbox.fill(r, g, b, paper)
return Letterbox.color(Letterbox.mode, r, g, b, paper)
end
return Letterbox
+249
View File
@@ -0,0 +1,249 @@
local Palette = {}
local pack = nil
function Palette.packs()
if pack == nil then
local ok, data = pcall(require, "data.gb_palettes")
pack = ok and data or false
end
return pack or {}
end
local MONO_STEPS = { 1.00, 0.72, 0.38, 0.12 }
Palette.MONO_COLOURS = {
{ "GREEN", 0x9b, 0xbc, 0x0f }, -- DMG
{ "LIME", 0xa8, 0xd8, 0x38 },
{ "OLIVE", 0xa6, 0xac, 0x84 }, -- Pocket
{ "AMBER", 0xff, 0xb0, 0x00 }, -- plasma terminal
{ "ORANGE", 0xff, 0x80, 0x30 },
{ "RED", 0xff, 0x50, 0x50 },
{ "ROSE", 0xff, 0x90, 0xb0 },
{ "MAGENTA", 0xf0, 0x60, 0xd0 },
{ "PURPLE", 0xb0, 0x70, 0xf0 },
{ "INDIGO", 0x80, 0x80, 0xf0 },
{ "BLUE", 0x50, 0xa0, 0xff },
{ "CYAN", 0x40, 0xe0, 0xe0 }, -- VFD
{ "TEAL", 0x40, 0xc0, 0xa0 },
{ "MINT", 0x90, 0xe0, 0xb0 },
{ "SAND", 0xe0, 0xc8, 0x90 },
{ "BROWN", 0xc0, 0x90, 0x50 },
{ "WHITE", 0xff, 0xff, 0xff }, -- plain DMG greys
{ "SILVER", 0xd0, 0xd8, 0xe0 },
}
local function monoRamp(r, g, b)
local out = {}
for i = 1, 4 do
local f = MONO_STEPS[i]
out[i] = { math.floor(r * f + 0.5),
math.floor(g * f + 0.5),
math.floor(b * f + 0.5) }
end
return out
end
Palette.monoRamp = monoRamp -- named for the suite
function Palette.packId(group, name)
return "p:" .. group .. "/" .. name
end
function Palette.monoId(r, g, b)
return ("m:%02x%02x%02x"):format(r, g, b)
end
local function unhex(text)
if type(text) ~= "string" or #text ~= 24 then return nil end
local out = {}
for i = 1, 4 do
local at = (i - 1) * 6
local r = tonumber(text:sub(at + 1, at + 2), 16)
local g = tonumber(text:sub(at + 3, at + 4), 16)
local b = tonumber(text:sub(at + 5, at + 6), 16)
if not (r and g and b) then return nil end
out[i] = { r, g, b }
end
return out
end
Palette.unhex = unhex -- named for the suite
local RAMP_INDEX = 2
local GREY_CHROMA = 12
local HUE_ARC = 50
Palette.GREY_CHROMA = GREY_CHROMA -- named for the suite
Palette.HUE_ARC = HUE_ARC
local function hueChroma(c)
local r, g, b = c[1] or 0, c[2] or 0, c[3] or 0
local mx = math.max(r, g, b)
local mn = math.min(r, g, b)
local chroma = mx - mn
if chroma <= 0 then return 0, 0 end
local h
if mx == r then h = ((g - b) / chroma) % 6
elseif mx == g then h = (b - r) / chroma + 2
else h = (r - g) / chroma + 4 end
return h * 60, chroma
end
Palette.hueChroma = hueChroma -- named for the suite
local function hueArc(hues)
local best = 360
for i = 1, #hues do
local widest = 0
for j = 1, #hues do
local d = (hues[j] - hues[i]) % 360
if d > widest then widest = d end
end
if widest < best then best = widest end
end
return best
end
Palette.hueArc = hueArc -- named for the suite
function Palette.categoryOf(ramp)
if type(ramp) ~= "table" or not ramp[4] then return nil end
local hues = {}
for i = 1, 4 do
local h, chroma = hueChroma(ramp[i])
if chroma > GREY_CHROMA then hues[#hues + 1] = h end
end
if #hues == 0 then return "grey" end
return hueArc(hues) <= HUE_ARC and "single" or "full"
end
Palette.CATEGORIES = {
{ key = "full", label = "FULL COLOUR" },
{ key = "single", label = "SINGLE COLOUR" },
{ key = "grey", label = "GREYSCALE" },
}
local byId, buckets = nil, nil
local function buildIndex()
byId, buckets = {}, {}
for _, category in ipairs(Palette.CATEGORIES) do buckets[category.key] = {} end
for _, p in ipairs(Palette.packs()) do
for _, entry in ipairs(p.palettes or {}) do
local name = entry[1]
if type(name) == "string" then
local id = Palette.packId(p.name, name)
byId[id] = entry
local raw = unhex(entry[RAMP_INDEX] or entry[2])
local bucket = raw and buckets[Palette.categoryOf(raw) or ""] or nil
if bucket then
bucket[#bucket + 1] = { name = name, pack = p.name, id = id }
end
end
end
end
end
local function ensureIndex()
if not byId then buildIndex() end
end
function Palette.categories()
ensureIndex()
local out = {}
for i, category in ipairs(Palette.CATEGORIES) do
out[i] = { key = category.key, name = category.label,
palettes = buckets[category.key] or {} }
end
return out
end
function Palette.category(key)
ensureIndex()
if not buckets[key] then return nil end
for _, category in ipairs(Palette.CATEGORIES) do
if category.key == key then
return { key = key, name = category.label, palettes = buckets[key] }
end
end
return nil
end
function Palette.locate(id)
if type(id) ~= "string" or id == "" then return nil end
ensureIndex()
for _, category in ipairs(Palette.CATEGORIES) do
for i, item in ipairs(buckets[category.key] or {}) do
if item.id == id then return category.key, i end
end
end
return nil
end
local function findPack(group, name)
ensureIndex()
return byId[Palette.packId(group, name)]
end
Palette.findPack = findPack -- named for the suite
function Palette.label(id)
if type(id) ~= "string" or id == "" then return nil end
local hex = id:match("^m:(%x+)$")
if hex then
local r = tonumber(hex:sub(1, 2), 16)
local g = tonumber(hex:sub(3, 4), 16)
local b = tonumber(hex:sub(5, 6), 16)
for _, c in ipairs(Palette.MONO_COLOURS) do
if c[2] == r and c[3] == g and c[4] == b then return c[1] end
end
return "MONO"
end
local name = id:match("^p:.*/([^/]+)$")
return name or id
end
local cacheId, cacheRamp = nil, nil
function Palette.ramp(id)
if type(id) ~= "string" or id == "" then return nil end
if id == cacheId then return cacheRamp end
local out = nil
local hex = id:match("^m:(%x+)$")
if hex and #hex == 6 then
out = monoRamp(tonumber(hex:sub(1, 2), 16) or 0,
tonumber(hex:sub(3, 4), 16) or 0,
tonumber(hex:sub(5, 6), 16) or 0)
else
local group, name = id:match("^p:(.*)/([^/]+)$")
if group then
local entry = findPack(group, name)
local ramp = entry and unhex(entry[RAMP_INDEX] or entry[2])
if ramp then
out = { ramp[4], ramp[3], ramp[2], ramp[1] }
end
end
end
cacheId, cacheRamp = id, out
return out
end
function Palette.invalidate()
cacheId, cacheRamp = nil, nil
byId, buckets = nil, nil
end
function Palette.swatch(id)
local ramp = Palette.ramp(id)
if not ramp then return nil end
return { ramp[4], ramp[3], ramp[2], ramp[1] }
end
return Palette
+50 -22
View File
@@ -797,6 +797,18 @@ function PaletteFX.shadeMap()
return shadeMap
end
local function invalidateColorCaches()
pcall(function() require("src.battle.BattleState").invalidate() end)
pcall(function() require("src.render.SpriteRenderer").invalidate() end)
pcall(function()
require("src.world.MapLoader").invalidateAll()
local Game = require("src.core.Game")
if Game.overworld and Game.overworld.map and Game.overworld.reloadMap then
Game.overworld:reloadMap(Game.overworld.map.id, "colors")
end
end)
end
function PaletteFX.setMode(mode)
local prev = PaletteFX.mode
local ok = false
@@ -808,27 +820,35 @@ function PaletteFX.setMode(mode)
end
end
if not ok then PaletteFX.mode = "gbc" end
-- battle pics and overworld sprites bake the active pack into ImageData;
-- drop those caches when the pack (or any COLORS mode) changes so the
-- next draw re-tints
if prev ~= PaletteFX.mode then
pcall(function() require("src.battle.BattleState").invalidate() end)
pcall(function() require("src.render.SpriteRenderer").invalidate() end)
-- RED++'s baked tileset atlas (TileRenderer.getGbcAtlas) is built once
-- per loaded map, so a mode toggle needs every cached Map/TileRenderer
-- dropped and the currently-visible one rebuilt in place -- otherwise
-- the on-screen map keeps its stale (wrong-mode) atlas until the next
-- map transition happens to reload it.
pcall(function()
require("src.world.MapLoader").invalidateAll()
local Game = require("src.core.Game")
if Game.overworld and Game.overworld.map and Game.overworld.reloadMap then
Game.overworld:reloadMap(Game.overworld.map.id, "colors")
end
end)
if prev ~= PaletteFX.mode then invalidateColorCaches() end
end
PaletteFX.customRamp = nil
function PaletteFX.setCustomRamp(ramp)
local prev = PaletteFX.customRamp
PaletteFX.customRamp = ramp
if (prev ~= nil) ~= (ramp ~= nil) or prev ~= ramp then
invalidateColorCaches()
end
end
function PaletteFX.pickerActive()
local ok, Game = pcall(require, "src.core.Game")
local states = ok and Game.stack and Game.stack.states
local top = states and states[#states]
return top ~= nil and top.screenId == "PaletteScreen"
end
-- Whether the active COLORS state needs a battle pic rendered as raw DMG
-- grays instead of real colour, the same "forced-mono" contract OG/OG
-- INV/CLASSIC already use (issue #207). A custom palette needs this too,
-- since ensureZones re-thresholds the already-drawn frame. Keep this in
-- sync with ensureZones and the mode checks in BattleState and WideBattle.
function PaletteFX.forcesRawGrays()
return PaletteFX.customRamp ~= nil
end
function PaletteFX.cycleMode()
local cur = PaletteFX.mode or "gbc"
local idx = 1
@@ -841,6 +861,13 @@ end
function PaletteFX.applyOptions(opts)
PaletteFX.setMode(opts and opts.colors or "gbc")
local id = opts and opts.palette
if id and id ~= "" then
local ok, Palette = pcall(require, "src.render.Palette")
PaletteFX.setCustomRamp(ok and Palette.ramp(id) or nil)
else
PaletteFX.setCustomRamp(nil)
end
end
function PaletteFX.modeLabel(mode)
@@ -853,12 +880,11 @@ function PaletteFX.modeLabel(mode)
end
-- When a state exposes no SGB zones but COLORS needs a forced palette
-- (OG / OG INV / CLASSIC), invent a whole-screen zone so the shade-remap
-- shader still runs. GBC / RED++ / GBC INV leave nil alone (raw DMG canvas).
function PaletteFX.ensureZones(zones)
if zones and zones[1] then return zones end
local mode = PaletteFX.mode or "gbc"
if mode == "og" or mode == "og_inv" or mode == "classic" then
if mode == "og" or mode == "og_inv" or mode == "classic"
or (PaletteFX.forcesRawGrays() and not PaletteFX.pickerActive()) then
return { PaletteFX.whole(PaletteFX.GRAYS) }
end
return zones
@@ -888,7 +914,9 @@ function PaletteFX.effectiveColors(c)
if not c then return nil end
local mode = PaletteFX.mode or "gbc"
local out = c
if mode == "og" then
if PaletteFX.customRamp and not PaletteFX.pickerActive() then
out = PaletteFX.customRamp
elseif mode == "og" then
out = PaletteFX.GRAYS
elseif mode == "og_inv" then
out = PaletteFX.permute(PaletteFX.GRAYS, INV_MAP)
+7
View File
@@ -972,6 +972,13 @@ function Renderer:endFrame(zones, worldZones)
and not FaithfulRes.scaleCap() then
clearR, clearG, clearB = PaletteFX.paperShade(Game and Game.data)
end
-- UI LETTERBOX overrides whatever the rules above settled on, except
-- under FAITHFUL RATIO's mobile lock, which promises black bars.
if not FaithfulRes.scaleCap() then
local Letterbox = require("src.render.Letterbox")
clearR, clearG, clearB = Letterbox.fill(clearR, clearG, clearB,
function() return PaletteFX.paperShade(Game and Game.data) end)
end
end
if cut then
love.graphics.setColor(0, 0, 0, 1)
+37 -9
View File
@@ -11,6 +11,12 @@ local UIVisibility = require("src.battle.UIVisibility")
local Theme = require("src.ui.Theme")
local Timing = require("src.core.Timing")
local Chrome2
do
local ok, v = pcall(require, "src.ui.gen2.Chrome")
Chrome2 = ok and v or nil
end
local TextBox = {}
TextBox.__index = TextBox
TextBox.isTextBox = true
@@ -470,8 +476,21 @@ function TextBox:draw()
-- (pokegold engine/pokegear/pokegear.asm TownMapPals sends every tile
-- >= $60 to palette 0). Gen 1's Game has no textboxPaper, so it stays nil.
local paper = self.game and self.game.textboxPaper and self.game:textboxPaper()
Font.drawBox(self.boxTx, self.boxTy, self.boxTw, self.boxTh, paper)
love.graphics.setColor(0, 0, 0, 1)
local gold = self.game and self.game.save
and (self.game.save.generation == 2 or self.game.save.version == "gold")
local Chrome = gold and Chrome2 or nil
local drawGlyph, finishGlyph = Font.drawCode, nil
if Chrome then
local base = paper and { paper, paper, paper, { 0, 0, 0 } }
or Chrome.DEFAULT_BOX_PALETTE
Chrome.paletteBox(self.boxTx, self.boxTy, self.boxTw, self.boxTh, base)
local _, dg, fg = Chrome.paletteGlyphs(base)
drawGlyph, finishGlyph = dg, fg
else
Font.drawBox(self.boxTx, self.boxTy, self.boxTw, self.boxTh, paper)
love.graphics.setColor(0, 0, 0, 1)
end
if self.scrollPx and self.scrollPx > 0 then
self.scrollPx = self.scrollPx - 2
if self.scrollPx <= 0 then self.scrollPx = nil end
@@ -491,17 +510,25 @@ function TextBox:draw()
-- measured with; every fixed-width page still lands on the 8px grid
local pen = self.textX
for _, code in ipairs(line) do
Font.drawCode(code, pen, y)
drawGlyph(code, pen, y)
pen = pen + Font.advanceOf(code)
end
end
if self.money then
-- money box (engine/menus/text_box.asm:130): DisplayMoneyBox at
-- hlcoord 11,0, the amount right-aligned on its middle row
Font.drawBox(11, 0, 9, 3)
love.graphics.setColor(0, 0, 0, 1)
if Chrome then
Chrome.paletteBox(11, 0, 9, 3, Chrome.DEFAULT_BOX_PALETTE)
else
Font.drawBox(11, 0, 9, 3)
love.graphics.setColor(0, 0, 0, 1)
end
local money = ("¥%d"):format(self.money() or 0)
Font.draw(money, 152 - Font.width(money), 8)
local pen = 152 - Font.width(money)
for _, code in ipairs(Font.encode(money)) do
drawGlyph(code, pen, 8)
pen = pen + Font.advanceOf(code)
end
end
if (self.waiting or (self.done and not self.choice and not self.auto
and (not self.stay
@@ -509,10 +536,11 @@ function TextBox:draw()
and self.blink < 30 then
-- page-advance cursor: glyph $EE by default, the blinking down arrow
-- the original prints via `ld a, "▼"` (home/text.asm)
Font.drawCode(Theme.moreArrow or 0xEE,
(self.boxTx + self.boxTw - 2) * 8,
(self.boxTy + self.boxTh - 1) * 8 - 4)
drawGlyph(Theme.moreArrow or 0xEE,
(self.boxTx + self.boxTw - 2) * 8,
(self.boxTy + self.boxTh - 1) * 8 - 4)
end
if finishGlyph then finishGlyph() end
love.graphics.setColor(1, 1, 1, 1)
end
+44 -8
View File
@@ -116,6 +116,36 @@ function SyncEngine.overlaps(a, b)
return aStart <= bEnd and bStart <= aEnd
end
-- Inline, under .meta, or under .remoteMeta, depending on the endpoint.
function SyncEngine.metaOf(row)
if type(row) ~= "table" then return nil end
if type(row.meta) == "table" then return row.meta end
if type(row.remoteMeta) == "table" then return row.remoteMeta end
return row
end
-- Gen 2 stores playTime as a table, so meta.playTime is nil on a Gold save
-- and summary.timeText is the only field that survives.
local function playedMinutes(meta)
if type(meta) ~= "table" then return nil end
local summary = type(meta.summary) == "table" and meta.summary or nil
local text = summary and summary.timeText
if type(text) == "string" then
local hours, minutes = text:match("^(%d+):(%d%d)$")
if hours then return tonumber(hours) * 60 + tonumber(minutes) end
end
local seconds = tonumber(meta.playTime)
if seconds then return math.floor(seconds / 60) end
return nil
end
-- Same minute of playtime means the same point in the playthrough, so there
-- is no fork. Unknown playtime on either side is NOT a match.
function SyncEngine.samePlaytime(a, b)
local left, right = playedMinutes(a), playedMinutes(b)
return left ~= nil and left == right
end
function SyncEngine.new(opts)
opts = opts or {}
local eng = setmetatable({}, SyncEngine)
@@ -457,7 +487,11 @@ function SyncEngine:_planFrom(remoteState)
if not row then
self:_queueUpload(entry, key, false)
elseif localChanged and remoteChanged then
self:_addConflict(entry, key, row)
if SyncEngine.samePlaytime(entry.meta, SyncEngine.metaOf(row)) then
self:_queueUpload(entry, key, true)
else
self:_addConflict(entry, key, row)
end
elseif localChanged then
self:_queueUpload(entry, key, false)
elseif remoteChanged and key ~= self.protectedKey then
@@ -477,12 +511,7 @@ function SyncEngine:_planFrom(remoteState)
end
function SyncEngine:_addConflict(entry, key, row)
local remoteMeta = row
if type(row.meta) == "table" then
remoteMeta = row.meta
elseif type(row.remoteMeta) == "table" then
remoteMeta = row.remoteMeta
end
local remoteMeta = SyncEngine.metaOf(row)
self.conflicts[#self.conflicts + 1] = {
key = key,
version = entry.version,
@@ -528,7 +557,14 @@ function SyncEngine:_queueUpload(entry, key, force)
end, function(e, res)
if res.code == 409 then
local row = res.data or {}
e:_addConflict(entry, key, row)
-- Retried with force only once: a forced write that still 409s is a
-- real refusal, and retrying it would spin.
if not force
and SyncEngine.samePlaytime(entry.meta, SyncEngine.metaOf(row)) then
e:_queueUpload(entry, key, true)
else
e:_addConflict(entry, key, row)
end
if not e:busy() then e:_finish() end
return true
end
+49
View File
@@ -0,0 +1,49 @@
-- Scrolling for a menu line too long for its box: hold, step one character
-- at a time, hold on the tail, snap back. Shared by the Gen 1 four-box
-- viewport (src/ui/OptionRows.lua) and the Gen 2 textbox
-- (src/ui/gen2/OptionsMenu.lua) -- different widths, same problem.
local Marquee = {}
Marquee.HOLD = 1.0
Marquee.STEP = 0.25
-- Only ever one row scrolls (the cursor's), so one shared phase keeps that
-- row's label and value stepping together.
local phase = { key = nil, start = 0 }
local function clock()
local timer = love and love.timer and love.timer.getTime
return timer and timer() or 0
end
-- Pure, so a test can step it without a clock.
function Marquee.at(text, maxChars, elapsed)
text = text or ""
local over = #text - maxChars
if over <= 0 then return text end
local span = Marquee.HOLD * 2 + over * Marquee.STEP
local now = elapsed % span
local offset
if now < Marquee.HOLD then
offset = 0
elseif now < Marquee.HOLD + over * Marquee.STEP then
offset = math.floor((now - Marquee.HOLD) / Marquee.STEP)
else
offset = over
end
return text:sub(offset + 1, offset + maxChars)
end
-- `key` identifies the highlighted row; changing it restarts the cycle.
function Marquee.scroll(text, maxChars, key)
if not text or #text <= maxChars then return text or "" end
if phase.key ~= key then phase.key, phase.start = key, clock() end
return Marquee.at(text, maxChars, clock() - phase.start)
end
function Marquee.clip(text, maxChars)
return (text or ""):sub(1, maxChars)
end
return Marquee
+19 -3
View File
@@ -7,12 +7,18 @@
-- rows that open something instead (MODS, CANCEL stays the caller's).
local Font = require("src.render.Font")
local Marquee = require("src.ui.Marquee")
local Theme = require("src.ui.Theme")
local OptionRows = {}
OptionRows.VISIBLE = 4 -- option boxes on screen at once (4 tiles each)
-- Font.drawBox(0, y, 20, 4) spends column 19 on the frame, so a line drawn
-- to the 160px screen edge prints over its own border.
local CONTENT_RIGHT = 152
local function fits(x) return math.floor((CONTENT_RIGHT - x) / 8) end
-- keep the cursor's box inside the viewport; the fixed bottom row shows
-- the tail of the list
function OptionRows.clampScroll(index, scroll, total, bottomRow)
@@ -27,7 +33,7 @@ function OptionRows.clampScroll(index, scroll, total, bottomRow)
end
-- one bordered box per row, label line + value line, with the fixed
-- bottom line below (CANCEL in the options menu, the manager's footer)
-- bottom line below (BACK in the options menu, the manager's footer)
function OptionRows.draw(game, rows, index, scroll, bottomLabel, bottomRow)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("fill", 0, 0, 160, 144)
@@ -37,8 +43,18 @@ function OptionRows.draw(game, rows, index, scroll, bottomLabel, bottomRow)
if not row then break end
Font.drawBox(0, (slot - 1) * 4, 20, 4)
love.graphics.setColor(0, 0, 0, 1)
Font.draw(row.label, 16, ((slot - 1) * 4 + 1) * 8)
Font.draw(row.value and row.value(game) or "", 24, ((slot - 1) * 4 + 2) * 8)
local label = row.label or ""
local value = row.value and row.value(game) or ""
if i == index then
local key = tostring(row.id or row.label) .. "\0" .. tostring(value)
label = Marquee.scroll(label, fits(16), key)
value = Marquee.scroll(value, fits(24), key)
else
label = Marquee.clip(label, fits(16))
value = Marquee.clip(value, fits(24))
end
Font.draw(label, 16, ((slot - 1) * 4 + 1) * 8)
Font.draw(value, 24, ((slot - 1) * 4 + 2) * 8)
if i == index then
Font.drawCode(Theme.cursor, 8, ((slot - 1) * 4 + 1) * 8)
end
+138 -22
View File
@@ -11,10 +11,12 @@
-- bottom line like pokered's.
local PaletteFX = require("src.render.PaletteFX")
local Palette = require("src.render.Palette")
local Pipelines = require("src.render.Pipelines")
local Tilt = require("src.render.Tilt")
local ShaderFX = require("src.render.ShaderFX")
local Zoom = require("src.render.Zoom")
local Letterbox = require("src.render.Letterbox")
local TileRenderer = require("src.render.TileRenderer")
local GameSpeed = require("src.core.GameSpeed")
local GameVersion = require("src.core.GameVersion")
@@ -127,14 +129,6 @@ local function stepVolume(v, dir)
return math.max(0, math.min(7, (v or 7) + dir))
end
local function colorIndex(opts)
local cur = opts.colors or "gbc"
for i, m in ipairs(PaletteFX.MODES) do
if m == cur then return i end
end
return 1
end
local function wrapIndex(i, n)
i = i % n
if i < 0 then i = i + n end
@@ -341,15 +335,14 @@ local function buildRows(game)
end },
{ id = "colors", label = Strings("COLORS"),
value = function(g)
local id = g.save.options.palette
if id and id ~= "" then
return Palette.label(id) or PaletteFX.modeLabel(g.save.options.colors or "gbc")
end
return PaletteFX.modeLabel(g.save.options.colors or "gbc")
end,
step = function(g, dir)
local o = g.save.options
local i = colorIndex(o)
i = wrapIndex(i - 1 + dir, #PaletteFX.MODES) + 1
o.colors = PaletteFX.MODES[i]
PaletteFX.setMode(o.colors)
return true
activate = function(g)
require("src.ui.Screens").push(g, "PaletteScreen")
end },
{ id = "tilt", label = Strings("TILT"),
value = function(g) return Tilt.levelLabel(g.save.options.tilt or 0) end,
@@ -376,6 +369,16 @@ local function buildRows(game)
-- cycling in place on this row.
-- ShaderFXScreen.lua does the actual list/activate; this row only opens
-- it and shows what is currently active.
{ id = "uiLetterbox", label = Strings("UI LETTERBOX"),
value = function(g)
return Strings(Letterbox.label(g.save.options.uiLetterbox))
end,
step = function(g, dir)
local o = g.save.options
o.uiLetterbox = Letterbox.cycle(o.uiLetterbox, dir)
Letterbox.setMode(o.uiLetterbox)
return true
end },
{ id = "shaderfx", label = Strings("SHADER FX"),
value = function(g)
return shaderfxLabel(ShaderFX.activeEntry("main"))
@@ -643,8 +646,90 @@ local function buildRows(game)
return rows
end
-- Grouping runs AFTER the ui.options.rows hook and never touches self.rows,
-- so a mod still sees and edits the flat list it always did; a row in no
-- group stays where it is, which is where a mod's own additions land.
local GROUPS = {
{ id = "group.battle", label = "BATTLE OPTIONS",
members = { "animations", "battleStyle", "battleLayout", "battleFit",
"battleHud", "battleBg" } },
{ id = "group.audio", label = "AUDIO",
members = { "musicVol", "sfxVol", "pikaVol", "musicFilter" } },
{ id = "group.video", label = "VIDEO",
members = { "uiLayout", "videoMode", "orientation", "faithfulRes",
"screenPos", "fpsCap" } },
{ id = "group.speed", label = "SPEED",
members = { "textSpeed", "speedOverworld", "speedBattle", "speedMenu" } },
{ id = "group.graphics", label = "GRAPHICS",
members = { "colors", "uiLetterbox", "shaderfx", "shaderfx2" } },
-- A mod's Pipelines row splices in after TILT and is in no group, so it
-- stays on the top level rather than being swallowed into this page.
{ id = "group.extras", label = "EXTRAS",
members = { "tilt", "zoom", "voidFill" } },
}
-- The top level's order, groups and singles alike. Anything not named here
-- (a mod's row, the touch rows) keeps its flat position after these.
local ORDER = {
"group.speed", "group.video", "group.graphics", "group.audio",
"performance", "ruleset", "group.battle", "group.extras", "mods",
}
-- Each group replaced by one opener.
local function groupRows(game, rows)
local owner, picked = {}, {}
for _, group in ipairs(GROUPS) do
for _, id in ipairs(group.members) do owner[id] = group end
picked[group.id] = {}
end
for _, row in ipairs(rows) do
local group = row.id and owner[row.id]
if group then picked[group.id][#picked[group.id] + 1] = row end
end
local made = {}
for _, group in ipairs(GROUPS) do
local members = picked[group.id]
if #members > 0 then
made[group.id] = {
id = group.id, label = Strings(group.label), group = true,
value = function() return Strings("%d OPTIONS", #members) end,
-- No onCancel: BACK out of a page returns here, it does not close
-- OPTIONS, so the caller's close callback must not fire.
activate = function(g)
g.stack:push(OptionsMenu.new(g, { rows = members }))
end,
}
end
end
local byId, view, taken = {}, {}, {}
for _, row in ipairs(rows) do
if row.id and not owner[row.id] then byId[row.id] = row end
end
for _, id in ipairs(ORDER) do
local row = made[id] or byId[id]
if row then
view[#view + 1] = row
taken[id] = true
end
end
-- Whatever ORDER does not name, in the order the flat list had it: a mod's
-- own rows, and the platform rows that come and go.
for _, row in ipairs(rows) do
local id = row.id
if not (id and (owner[id] or taken[id])) then view[#view + 1] = row end
end
return view
end
-- opts.rows makes a submenu: a fixed row list, no hook and no regrouping, so
-- a group's page is this same screen driving the rows it was handed.
function OptionsMenu.new(game, opts)
opts = opts or {}
if opts.rows then
return setmetatable({ game = game, rows = opts.rows, view = opts.rows,
index = 1, scroll = 0, sub = true,
onCancel = opts.onCancel }, OptionsMenu)
end
local rows = buildRows(game)
local hooked = Runtime.call("ui.options.rows", sameRows, game, rows)
if type(hooked) == "table" then
@@ -653,14 +738,44 @@ function OptionsMenu.new(game, opts)
Logger.error("ui.options.rows returned %s; keeping the vanilla rows",
type(hooked))
end
return setmetatable({ game = game, rows = rows, index = 1, scroll = 0,
onCancel = opts.onCancel }, OptionsMenu)
local self = setmetatable({ game = game, rows = rows, index = 1, scroll = 0,
onCancel = opts.onCancel }, OptionsMenu)
self.view = groupRows(game, rows)
return self
end
-- Cursor onto a row by id, opening its group page first if it lives in one.
-- Returns the screen the row ended up on, so a caller can keep driving it.
function OptionsMenu:focusRow(id)
local rows = self.view or self.rows
for i, row in ipairs(rows) do
if row.id == id then
self.index = i
self.scroll = OptionRows.clampScroll(i, self.scroll or 0, #rows, #rows + 1)
return self
end
end
for i, row in ipairs(rows) do
if row.group and row.activate then
local group
for _, g in ipairs(GROUPS) do if g.id == row.id then group = g end end
for _, member in ipairs(group and group.members or {}) do
if member == id then
self.index = i
row.activate(self.game)
local sub = self.game.stack:top()
return sub.focusRow and sub:focusRow(id) or sub
end
end
end
end
return nil
end
function OptionsMenu:update(dt)
local input = self.game.input
local rows = self.rows
-- CANCEL sits below the hook-built rows so a mod cannot orphan the exit
local rows = self.view or self.rows
-- BACK sits below the hook-built rows so a mod cannot orphan the exit
local cancelRow = #rows + 1
local changed = false
if input:wasPressed("up") then
@@ -702,14 +817,15 @@ function OptionsMenu:update(dt)
end
function OptionsMenu:draw()
-- Through Strings, like every other label on this menu. CANCEL is
-- Through Strings, like every other label on this menu. BACK is
-- appended AFTER the rows hook (see the header), which is what keeps a mod
-- from orphaning the exit -- but it also means a translation mod never sees
-- this string, and cannot: there is no row for it to rewrite. So the one
-- word a Spanish player could not read on a fully translated OPTIONS menu
-- was the way out of it.
OptionRows.draw(self.game, self.rows, self.index, self.scroll or 0,
Strings("CANCEL"), #self.rows + 1)
local rows = self.view or self.rows
OptionRows.draw(self.game, rows, self.index, self.scroll or 0,
Strings("BACK"), #rows + 1)
end
return OptionsMenu
+578
View File
@@ -0,0 +1,578 @@
local Font = require("src.render.Font")
local Theme = require("src.ui.Theme")
local Sound = require("src.core.Sound")
local Strings = require("src.core.Strings")
local Palette = require("src.render.Palette")
local PaletteFX = require("src.render.PaletteFX")
local PaletteScreen = {}
PaletteScreen.__index = PaletteScreen
PaletteScreen.isOpaque = true
local COLS, ROWS = 5, 4
local TILE_W, TILE_H = 28, 20
local GAP = 4
local GRID_X, GRID_Y = 2, 14
local PER_PAGE = COLS * ROWS
local SCREEN_W, SCREEN_H = 160, 144
local BACK = 0
local function sanitize(text)
text = tostring(text or ""):upper():gsub("_", "'")
text = text:gsub("[^A-Z0-9 %.'%-]", " ")
text = text:gsub("%s+", " "):gsub("^ ", ""):gsub(" $", "")
return text
end
PaletteScreen.sanitize = sanitize -- named for the suite
local function wrap(text, width, lines)
local out = {}
local rest = text
for i = 1, lines do
if rest == "" then break end
if #rest <= width then
out[i] = rest
rest = ""
else
local cut = rest:sub(1, width + 1):match("^.*() ")
if not cut or cut < 2 then cut = width + 1 end
out[i] = rest:sub(1, cut - 1)
rest = rest:sub(cut):gsub("^ +", "")
end
end
if rest ~= "" and #out > 0 then
local last = out[#out]
out[#out] = last:sub(1, math.max(0, width - 1)) .. "."
end
return out
end
PaletteScreen.wrap = wrap -- named for the suite
local function packLabel(name, width)
local text = sanitize(name)
if #text > width then
local leaf = tostring(name):match("([^/]+)$")
if leaf then text = sanitize(leaf) end
end
return wrap(text, width, 1)[1] or ""
end
PaletteScreen.packLabel = packLabel -- named for the suite
local function fill(x, y, w, h, c)
love.graphics.setColor(c[1] / 255, c[2] / 255, c[3] / 255, 1)
love.graphics.rectangle("fill", x, y, w, h)
end
local function frame(x, y, w, h, shade)
love.graphics.setColor(shade, shade, shade, 1)
love.graphics.rectangle("line", x + 0.5, y + 0.5, w - 1, h - 1)
end
local function drawSwatch(x, y, w, h, swatch)
if not swatch then
frame(x, y, w, h, 0.5)
return
end
local band = w / 4
for i = 1, 4 do
fill(x + math.floor((i - 1) * band), y,
math.ceil(band), h, swatch[i])
end
end
PaletteScreen.drawSwatch = drawSwatch -- named for the suite
local function drawBack(selected)
if selected then
love.graphics.setColor(0, 0, 0, 1)
love.graphics.rectangle("fill", 0, 0, 10, 9)
love.graphics.setColor(1, 1, 1, 1)
else
love.graphics.setColor(0, 0, 0, 1)
end
for i = 0, 3 do
love.graphics.rectangle("fill", 2 + i, 4 - i, 1, 1 + i * 2)
end
end
PaletteScreen.drawBack = drawBack -- named for the suite
local function engineModes()
local labels = PaletteFX.MODE_LABELS or {}
local out = {}
for i, id in ipairs(PaletteFX.MODES) do
out[i] = { PaletteFX.modeLabel(id) or labels[id] or id:upper(), id }
end
return out
end
local function dropConflictingMode(game)
local mode = PaletteFX.mode
local bakes = PaletteFX.usesGbcPack(mode) or PaletteFX.usesSpriteObp(mode)
if not bakes then return end
local o = game.save.options
o.colors = "gbc"
PaletteFX.setMode("gbc")
end
local function liveOpts(game)
dropConflictingMode(game)
return {
palette = Palette,
get = function() return game.save.options.palette or "" end,
set = function(v)
local o = game.save.options
o.palette = v
if v ~= "" then dropConflictingMode(game) end
PaletteFX.setCustomRamp(v ~= "" and Palette.ramp(v) or nil)
if game.writeOptions then pcall(game.writeOptions, game) end
end,
modes = engineModes(),
getMode = function() return game.save.options.colors or "gbc" end,
setMode = function(v)
local o = game.save.options
o.colors = v
PaletteFX.setMode(v)
if game.writeOptions then pcall(game.writeOptions, game) end
end,
}
end
function PaletteScreen.new(game, opts)
opts = opts or liveOpts(game)
local self = setmetatable({
game = game,
Palette = opts.palette,
get = opts.get, set = opts.set,
modes = opts.modes or {},
getMode = opts.getMode, setMode = opts.setMode,
onChange = opts.onChange,
view = "root",
index = 1,
scroll = 0,
group = nil,
stackTrail = {},
}, PaletteScreen)
self.rows = self:buildRoot()
self:enterFromSelection()
return self
end
function PaletteScreen:buildRoot()
local rows = {}
for _, category in ipairs(self.Palette and self.Palette.categories() or {}) do
if #(category.palettes or {}) > 0 then
rows[#rows + 1] = { label = category.name, folder = true,
view = "tiles", group = category }
end
end
if self.Palette and self.Palette.MONO_COLOURS then
rows[#rows + 1] = { label = "MONOCHROME", folder = true, view = "mono" }
end
for _, mode in ipairs(self.modes) do
rows[#rows + 1] = { label = mode[1], mode = mode[2] }
end
return rows
end
function PaletteScreen:rootIndexFor(view, group)
for i, row in ipairs(self.rows or {}) do
if row.view == view then
if view ~= "tiles" then return i end
if row.group and group and row.group.key == group.key then return i end
end
end
return 1
end
function PaletteScreen:enterFromSelection()
local Palette = self.Palette
if not Palette then return end
local id = self.get and self.get() or ""
if type(id) ~= "string" or id == "" then return end
if id:match("^m:") then
for i, c in ipairs(Palette.MONO_COLOURS) do
if Palette.monoId(c[2], c[3], c[4]) == id then
self:openAt("mono", nil, i)
return
end
end
return
end
local key, at = Palette.locate(id)
if key then self:openAt("tiles", Palette.category(key), at) end
end
function PaletteScreen:openAt(view, group, index)
self.stackTrail = { { view = "root", index = self:rootIndexFor(view, group),
scroll = 0, group = nil } }
self.view, self.group, self.index, self.scroll = view, group, index or 1, 0
local items = self:items()
self:clampScroll(#items, #items + 1, PER_PAGE)
end
function PaletteScreen:items()
local Palette = self.Palette
if self.view == "root" then return self.rows or {} end
if self.view == "tiles" then
return self.group and self.group.palettes or {}
end
if self.view == "mono" then
return Palette and Palette.MONO_COLOURS or {}
end
return {}
end
function PaletteScreen:isGrid()
return self.view == "tiles" or self.view == "mono"
end
function PaletteScreen:idAt(i)
local Palette = self.Palette
if not Palette then return nil end
if self.view == "tiles" then
local entry = self.group and (self.group.palettes or {})[i]
return entry and entry.id or nil
elseif self.view == "mono" then
local c = Palette.MONO_COLOURS[i]
return c and Palette.monoId(c[2], c[3], c[4]) or nil
end
return nil
end
function PaletteScreen:title()
if self.view == "root" then return Strings("COLOUR") end
if self.view == "mono" then return Strings("MONOCHROME") end
return sanitize(self.group and self.group.name or "")
end
function PaletteScreen:update()
local input = self.game.input
local items = self:items()
local total = #items
local cancel = total + 1
if input:wasPressed("b") then
self:back()
return
end
if self:isGrid() then
self:moveGrid(input, total, cancel)
else
self:moveList(input, total, cancel)
end
if input:wasPressed("a") then
self:activate(cancel)
end
end
function PaletteScreen:moveList(input, total, cancel)
if self.index == BACK then
if input:wasPressed("down") or input:wasPressed("right") then
self.index = 1
end
return
end
if input:wasPressed("left") then
self.index = BACK
return
end
if input:wasPressed("up") then
self.index = self.index > 1 and self.index - 1 or BACK
elseif input:wasPressed("down") then
self.index = self.index < cancel and self.index + 1 or 1
end
self:clampScroll(total, cancel, 12)
end
function PaletteScreen:moveGrid(input, total, cancel)
local i = self.index
if i == BACK then
if input:wasPressed("down") or input:wasPressed("right") then
self.index = self.scroll + 1
if self.index > total then self.index = 1 end
end
return
end
if i > total then
if input:wasPressed("up") then
self.index = total
elseif input:wasPressed("down") then
self.index = 1
end
self:clampScroll(total, cancel, PER_PAGE)
return
end
if input:wasPressed("left") then
self.index = ((i - 1) % COLS == 0) and BACK or i - 1
elseif input:wasPressed("right") then
self.index = i < total and i + 1 or 1
elseif input:wasPressed("up") then
self.index = (i - COLS >= 1) and i - COLS or BACK
elseif input:wasPressed("down") then
if i + COLS <= total then
self.index = i + COLS
else
self.index = cancel
end
end
self:clampScroll(total, cancel, PER_PAGE)
end
function PaletteScreen:clampScroll(total, cancel, visible)
if self.index == BACK then return end
if self.index >= cancel then
self.scroll = math.max(0, total - visible)
if self:isGrid() then
self.scroll = math.floor(self.scroll / COLS) * COLS
end
return
end
local step = self:isGrid() and COLS or 1
if self.index <= self.scroll then
self.scroll = math.floor((self.index - 1) / step) * step
elseif self.index > self.scroll + visible then
self.scroll = math.floor((self.index - 1) / step) * step - visible + step
end
if self.scroll < 0 then self.scroll = 0 end
end
function PaletteScreen:activate(cancel)
if self.index == BACK or self.index >= cancel then
self:back()
return
end
if self.view == "root" then
local row = (self.rows or {})[self.index]
if not row then return end
if row.folder then
self:push(row.view, row.group)
elseif row.mode then
if self.set then self.set("") end
if self.setMode then self.setMode(row.mode) end
self:changed()
end
elseif self:isGrid() then
local id = self:idAt(self.index)
if id then
if self.set then self.set(id) end
self:changed()
end
end
end
function PaletteScreen:changed()
if self.game.data and Sound and Sound.play then
pcall(Sound.play, self.game.data, "Press_AB")
end
if self.onChange then self.onChange() end
end
function PaletteScreen:push(view, group)
self.stackTrail[#self.stackTrail + 1] =
{ view = self.view, index = self.index, scroll = self.scroll,
group = self.group }
self.view, self.index, self.scroll = view, 1, 0
self.group = group
self:cursorToSelection()
end
function PaletteScreen:cursorToSelection()
local id = self.get and self.get() or ""
if type(id) ~= "string" or id == "" then return end
local items = self:items()
for i = 1, #items do
if self:idAt(i) == id then
self.index = i
self:clampScroll(#items, #items + 1,
self:isGrid() and PER_PAGE or 12)
return
end
end
end
function PaletteScreen:back()
local prev = table.remove(self.stackTrail)
if prev then
self.view, self.index = prev.view, prev.index
self.scroll, self.group = prev.scroll, prev.group
return
end
self:close()
end
function PaletteScreen:close()
if self.game.data and Sound and Sound.play then
pcall(Sound.play, self.game.data, "Press_AB")
end
self.game.stack:pop()
end
function PaletteScreen:sgbPalettes()
return nil
end
function PaletteScreen:draw()
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle("fill", 0, 0, SCREEN_W, SCREEN_H)
drawBack(self.index == BACK)
love.graphics.setColor(0, 0, 0, 1)
Font.draw(self:title(), 12, 0)
if self:isGrid() then
self:drawGrid()
else
self:drawList()
end
love.graphics.setColor(0, 0, 0, 1)
local items = self:items()
if self.index >= #items + 1 then
Font.draw(Strings("CANCEL"), 16, SCREEN_H - 8)
Font.drawCode(Theme.cursor, 8, SCREEN_H - 8)
end
love.graphics.setColor(1, 1, 1, 1)
end
function PaletteScreen:drawList()
local items = self:items()
local mode = self.getMode and self.getMode() or nil
local visible = 12
for slot = 1, visible do
local i = self.scroll + slot
local row = items[i]
if not row then break end
local y = 16 + (slot - 1) * 8
love.graphics.setColor(0, 0, 0, 1)
Font.draw(Strings(row.label or ""), 16, y)
if i == self.index then
Font.drawCode(Theme.cursor, 8, y)
end
if row.folder then
Font.drawCode(Theme.cursor, 8 + 8 * 17, y)
elseif row.mode and mode and row.mode == mode then
love.graphics.rectangle("fill", 8 + 8 * 17 + 2, y + 2, 4, 4)
end
end
end
function PaletteScreen:drawGrid()
local Palette = self.Palette
local items = self:items()
local total = #items
for slot = 1, PER_PAGE do
local i = self.scroll + slot
if i > total then break end
local col = (slot - 1) % COLS
local row = math.floor((slot - 1) / COLS)
local x = GRID_X + col * (TILE_W + GAP)
local y = GRID_Y + row * (TILE_H + GAP)
local swatch
if self.view == "mono" then
local c = Palette and Palette.MONO_COLOURS[i]
if c then
local ramp = Palette.monoRamp(c[2], c[3], c[4])
swatch = { ramp[4], ramp[3], ramp[2], ramp[1] }
end
else
swatch = Palette and Palette.swatch(self:idAt(i)) or nil
end
drawSwatch(x, y, TILE_W, TILE_H, swatch)
if i == self.index then
frame(x - 2, y - 2, TILE_W + 4, TILE_H + 4, 0)
frame(x - 1, y - 1, TILE_W + 2, TILE_H + 2, 1)
end
end
self:drawFooter(total)
end
function PaletteScreen:drawFooter(total)
local Palette = self.Palette
love.graphics.setColor(0, 0, 0, 1)
local name, pack = "", nil
if self.index >= 1 and self.index <= total then
if self.view == "mono" then
local c = Palette and Palette.MONO_COLOURS[self.index]
name = c and c[1] or ""
else
local entry = self.group and (self.group.palettes or {})[self.index]
if entry then
name = sanitize(entry.name)
pack = entry.pack
end
end
end
local lines = wrap(name, 20, 2)
for i = 1, #lines do
Font.draw(lines[i], 0, 110 + (i - 1) * 8)
end
local at = 0
if total > PER_PAGE then
local page = math.floor(self.scroll / PER_PAGE) + 1
local pages = math.ceil(total / PER_PAGE)
Font.draw(("%d/%d"):format(page, pages), 0, 128)
at = 40
end
if pack then
Font.draw(packLabel(pack, (SCREEN_W - at) / 8), at, 128)
end
local stored = self.get and self.get() or ""
if type(stored) == "string" and stored ~= "" then
for slot = 1, PER_PAGE do
local i = self.scroll + slot
if i > total then break end
if self:idAt(i) == stored then
local col = (slot - 1) % COLS
local row = math.floor((slot - 1) / COLS)
love.graphics.setColor(0, 0, 0, 1)
love.graphics.rectangle(
"fill",
GRID_X + col * (TILE_W + GAP) + TILE_W - 5,
GRID_Y + row * (TILE_H + GAP) + TILE_H - 5, 4, 4)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.rectangle(
"fill",
GRID_X + col * (TILE_W + GAP) + TILE_W - 4,
GRID_Y + row * (TILE_H + GAP) + TILE_H - 4, 2, 2)
break
end
end
end
end
return PaletteScreen
+2 -5
View File
@@ -18,6 +18,7 @@
local bit = require("bit")
local Assets = require("src.render.Assets")
local Chrome = require("src.ui.gen2.Chrome")
local GbcPalette = require("src.render.GbcPalette")
local Palettes = require("src.world.gen2.Palettes")
@@ -164,12 +165,8 @@ local function needsCanvas(runner)
end
-- The battle background is BG colour 0 everywhere the two pic boxes and the
-- text box are not, which Chrome.clear draws as plain white; anything the
-- scanline blit exposes has to be that colour and not a hole.
function BattleAnimView:fillBackground()
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, SCREEN_W, SCREEN_H)
Chrome.paletteFill(0, 0, SCREEN_W, SCREEN_H)
end
-- One reusable quad, re-aimed per scanline. A row shifted by `dx` is drawn
+44 -31
View File
@@ -3520,7 +3520,7 @@ function BattleState:drawHud()
-- And nothing at all before UpdateEnemyHUD has ever run: the intro bands
-- slide in over a blanked tilemap (core.asm:8554/8564).
if showStatus and self.showEnemyHud and not self:hudCleared("enemy") then
Chrome.print(self:name(enemy), 1, 0)
Chrome.printThrough(self:name(enemy), 1, 0, Chrome.DEFAULT_BOX_PALETTE)
-- PrintLevel writes <LV> at the coordinate it is given and then LEFT-aligns
-- the digits after it, so the glyph is pinned to column 6 whether the level
-- is 5 or 100; only a three-digit level moves, and it does so by eating the
@@ -3528,10 +3528,12 @@ function BattleState:drawHud()
-- PlaceNonFaintStatus (engine/pokemon/mon_stats.asm): a statused mon's tag
-- prints where the level goes, and DrawEnemyHUD's `.skip_level` arm drops
-- the level entirely while one is up.
Chrome.print(self:statusTag(enemy, "enemy")
or ("<LV>" .. tostring(enemy.level or 1)), 6, 1)
Chrome.printThrough(self:statusTag(enemy, "enemy")
or ("<LV>" .. tostring(enemy.level or 1)), 6, 1, Chrome.DEFAULT_BOX_PALETTE)
local enemyGender = self:genderSymbol(enemy)
if enemyGender then Chrome.print(enemyGender, 9, 1) end
if enemyGender then
Chrome.printThrough(enemyGender, 9, 1, Chrome.DEFAULT_BOX_PALETTE)
end
-- `ld a, [wBattleMode] / dec a / ret nz`, then CheckCaughtMon puts $5d at
-- (1,1) (engine/battle/trainer_huds.asm:140-152).
if self.battle and self.battle.wild and self.caughtMark then
@@ -3580,17 +3582,20 @@ function BattleState:drawHud()
Font.useBattleExtra(wasBattle)
return
end
Chrome.print(self:name(player), 10, 7)
Chrome.printThrough(self:name(player), 10, 7, Chrome.DEFAULT_BOX_PALETTE)
-- PrintPlayerHUD places the same status tag at (14,8) and skips the level
-- while it is up.
Chrome.print(self:statusTag(player, "player")
or ("<LV>" .. tostring(self.shownLevel or player.level or 1)), 14, 8)
Chrome.printThrough(self:statusTag(player, "player")
or ("<LV>" .. tostring(self.shownLevel or player.level or 1)), 14, 8,
Chrome.DEFAULT_BOX_PALETTE)
local playerGender = self:genderSymbol(player)
if playerGender then Chrome.print(playerGender, 17, 8) end
if playerGender then
Chrome.printThrough(playerGender, 17, 8, Chrome.DEFAULT_BOX_PALETTE)
end
self:drawHpBar(player, "player", 10, 9)
local maxHp = player.maxHp or (player.stats and player.stats.hp) or 0
Chrome.printRight(("%d/%d"):format(self:hudHp(player, "player"), maxHp),
18, 10)
Chrome.printRightThrough(("%d/%d"):format(self:hudHp(player, "player"), maxHp),
18, 10, Chrome.DEFAULT_BOX_PALETTE)
-- Stub on the RIGHT (tile $73), border from (18,10) laid leftward, exp bar
-- at (10,11).
-- The chased fill, not the mon's: AnimateExpBar crawls it, and reading the
@@ -3659,8 +3664,8 @@ end
function BattleState:printMessage()
local lines = Chrome.wrap(self.message or "", TEXT_WIDTH)
for i = 1, math.min(#lines, TEXT_ROWS) do
Chrome.print(lines[i], TEXT_INNER_X,
TEXT_INNER_Y + (i - 1) * TEXT_ROW_STEP)
Chrome.printThrough(lines[i], TEXT_INNER_X,
TEXT_INNER_Y + (i - 1) * TEXT_ROW_STEP, Chrome.DEFAULT_BOX_PALETTE)
end
end
@@ -3670,15 +3675,17 @@ function BattleState:drawMoveInfoBox(move)
if not move then return end
local fighter = self.battle and self.battle.player
if fighter and self.battle:moveDisabled(fighter, move.id) then
Chrome.print("Disabled!", 1, 10)
Chrome.printThrough("Disabled!", 1, 10, Chrome.DEFAULT_BOX_PALETTE)
return
end
local def = self.game and self.game.data and self.game.data.moves
and self.game.data.moves[move.id]
Chrome.print("TYPE/", 1, 9)
Chrome.printThrough("TYPE/", 1, 9, Chrome.DEFAULT_BOX_PALETTE)
local moveType = def and def.type
Chrome.print(moveType and (TYPE_NAMES[moveType] or moveType) or "", 2, 10)
Chrome.print(("%2d/%2d"):format(move.pp or 0, move.maxPp or 0), 5, 11)
Chrome.printThrough(moveType and (TYPE_NAMES[moveType] or moveType) or "",
2, 10, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough(("%2d/%2d"):format(move.pp or 0, move.maxPp or 0),
5, 11, Chrome.DEFAULT_BOX_PALETTE)
end
function BattleState:drawPanel()
@@ -3687,7 +3694,7 @@ function BattleState:drawPanel()
-- required there; everywhere else a missing side is a caller bug.
local hasPlayer = self.battle and (self.battle.player or self.tutorial)
if not (self.battle and hasPlayer and self.battle.enemy) then
Chrome.print("NO BATTLE", 1, 1)
Chrome.printThrough("NO BATTLE", 1, 1, Chrome.DEFAULT_BOX_PALETTE)
return
end
self:drawHud()
@@ -3719,8 +3726,10 @@ function BattleState:drawPanel()
local col = ((i - 1) % 2) * spacing
local row = math.floor((i - 1) / 2) * 2
local tx, ty = boxX + 2 + col, 14 + row
if i == self.menuIndex then Chrome.cursor(tx - 1, ty) end
Chrome.print(label, tx, ty)
if i == self.menuIndex then
Chrome.cursorThrough(tx - 1, ty, Chrome.DEFAULT_BOX_PALETTE)
end
Chrome.printThrough(label, tx, ty, Chrome.DEFAULT_BOX_PALETTE)
end
elseif self.phase == "moves"
or (self.phase == "choose-forget" and (self.messageTimer or 0) <= 0) then
@@ -3740,21 +3749,24 @@ function BattleState:drawPanel()
for i, move in ipairs(moves) do
local ty = 13 + (i - 1)
-- Cursor in the box's own gutter, not clipped against the border.
if i == cursorRow then Chrome.cursor(cursorCol, ty) end
if i == cursorRow then
Chrome.cursorThrough(cursorCol, ty, Chrome.DEFAULT_BOX_PALETTE)
end
-- The held slot's marker. `.battle_player_moves` writes '▷' into the
-- row wSwappingMove names (engine/battle/core.asm:5157-5165) so a move
-- picked up for a swap is visible while the cursor moves off it. It
-- hlcoord 5, 13 is the cursor's own gutter, so PlaceMenuCursor covers
-- the marker on the cursor's row.
if not forgetting and self.moveSwapIndex == i and i ~= cursorRow then
Chrome.print("\u{25B7}", cursorCol, ty)
Chrome.printThrough("\u{25B7}", cursorCol, ty, Chrome.DEFAULT_BOX_PALETTE)
end
local def = self.game and self.game.data and self.game.data.moves
and self.game.data.moves[move.id]
Chrome.print((def and def.name) or move.id, nameCol, ty)
Chrome.printThrough((def and def.name) or move.id, nameCol, ty,
Chrome.DEFAULT_BOX_PALETTE)
if not moveMenu then
Chrome.printRight(("%d/%d"):format(move.pp or 0, move.maxPp or 0),
19, ty)
Chrome.printRightThrough(("%d/%d"):format(move.pp or 0, move.maxPp or 0),
19, ty, Chrome.DEFAULT_BOX_PALETTE)
end
end
if moveMenu then self:drawMoveInfoBox(moves[cursorRow]) end
@@ -3773,13 +3785,14 @@ function BattleState:drawPanel()
local left = (self.phase == "ask-shift" or self.phase == "ask-next-mon")
and 1 or 14
Chrome.box(left, 7, 6, 5)
Chrome.print("YES", left + 2, 8)
Chrome.print("NO", left + 2, 10)
Chrome.printThrough("YES", left + 2, 8, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough("NO", left + 2, 10, Chrome.DEFAULT_BOX_PALETTE)
local index = self.phase == "ask-nickname" and self.nicknameIndex
or self.phase == "ask-shift" and self.shiftIndex
or self.phase == "ask-next-mon" and self.nextMonIndex
or self.forgetChoice
Chrome.cursor(left + 1, index == 1 and 8 or 10)
Chrome.cursorThrough(left + 1, index == 1 and 8 or 10,
Chrome.DEFAULT_BOX_PALETTE)
end
end
if self.phase == "stats-box" and self.statsBoxMon then
@@ -3808,8 +3821,9 @@ function BattleState:drawStatsBox(mon)
Chrome.textbox(9, 0, 9, 10)
for i, row in ipairs(STATS_BOX_ROWS) do
local ty = 1 + (i - 1) * 2
Chrome.print(Strings(row[1]), 11, ty)
Chrome.printRight(("%d"):format(stats[row[2]] or 0), 19, ty + 1)
Chrome.printThrough(Strings(row[1]), 11, ty, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printRightThrough(("%d"):format(stats[row[2]] or 0), 19, ty + 1,
Chrome.DEFAULT_BOX_PALETTE)
end
end
@@ -3893,8 +3907,7 @@ end
function BattleState:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
G.push()
+1 -2
View File
@@ -945,8 +945,7 @@ end
function BoxMenu:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -968,8 +968,7 @@ end
function CardFlip:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -309,8 +309,7 @@ end
function CenterPcMenu:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+156 -41
View File
@@ -27,13 +27,39 @@ Chrome.SCREEN_H = 18
-- charmap.asm "¥", the money field's own prefix tile.
local YEN = "\xc2\xa5"
function Chrome.clear()
function Chrome.paletteFill(px, py, pw, ph, palette)
palette = palette or Chrome.DEFAULT_BOX_PALETTE
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, Chrome.SCREEN_W * 8, Chrome.SCREEN_H * 8)
if GbcPalette.available() then
GbcPalette.with(palette, function() G.rectangle("fill", px, py, pw, ph) end)
else
G.rectangle("fill", px, py, pw, ph)
end
G.setColor(0, 0, 0, 1)
end
-- Every drawWidescreen paints the window around its 160x144 panel through
-- here, so UI LETTERBOX has exactly one place to override. r/g/b is what the
-- screen was authored with and is what AUTO keeps.
local function letterboxPaper()
local c = GbcPalette.resolve(Chrome.DEFAULT_BOX_PALETTE)
c = c and c[1]
if not c then return nil end
return c[1] / 255, c[2] / 255, c[3] / 255
end
function Chrome.letterbox(winW, winH, r, g, b)
local Letterbox = require("src.render.Letterbox")
local G = love.graphics
G.setColor(Letterbox.fill(r or 1, g or 1, b or 1, letterboxPaper))
G.rectangle("fill", 0, 0, winW, winH)
end
function Chrome.clear()
Chrome.paletteFill(0, 0, Chrome.SCREEN_W * 8, Chrome.SCREEN_H * 8)
end
-- The blit scale every `drawWidescreen` paints its 160x144 panel at.
--
-- One GB pixel has to cover a WHOLE number of window pixels or the 8x8 grid
@@ -77,10 +103,24 @@ function Chrome.positionLift(winW, winH, scale)
or Chrome.fitScale(winW, winH)), ScreenPosition.safeTop())
end
Chrome.DEFAULT_BOX_PALETTE = {
{ 255, 255, 255 }, { 255, 255, 255 }, { 255, 255, 255 }, { 0, 0, 0 },
}
function Chrome.paletteBox(tx, ty, tw, th, palette)
palette = palette or Chrome.DEFAULT_BOX_PALETTE
if GbcPalette.available() then
love.graphics.setColor(1, 1, 1, 1)
GbcPalette.with(palette, function() Font.drawBox(tx, ty, tw, th) end)
else
Font.drawBox(tx, ty, tw, th, palette[1])
end
love.graphics.setColor(0, 0, 0, 1)
end
-- A bordered box, tile coords. Leaves the draw color black for text.
function Chrome.box(tx, ty, tw, th)
Font.drawBox(tx, ty, tw, th)
love.graphics.setColor(0, 0, 0, 1)
Chrome.paletteBox(tx, ty, tw, th)
end
-- Gold's Textbox helper takes an interior width/height and draws the border
@@ -90,11 +130,16 @@ function Chrome.textbox(tx, ty, interiorW, interiorH)
Chrome.box(tx, ty, interiorW + 2, interiorH + 2)
end
function Chrome.print(text, tx, ty)
local function flatPrint(text, tx, ty)
love.graphics.setColor(0, 0, 0, 1)
return Font.draw(text, tx * 8, ty * 8)
end
function Chrome.print(text, tx, ty)
if not GbcPalette.available() then return flatPrint(text, tx, ty) end
return Chrome.printThrough(text, tx, ty, Chrome.DEFAULT_BOX_PALETTE)
end
-- Prints a string the way a tilemap screen does: the glyph tiles replace the
-- cells they land on, and both their ink and their blank pixels go through
-- that screen's own BG palette.
@@ -124,32 +169,39 @@ function Chrome.throughPalette(palette, invert)
return GbcPalette.remap(pal, GbcPalette.bgp)
end
-- `palette` is a 4-colour table of 0-255 triples. `invert` is what
-- Chrome.printInverted passes; see there.
function Chrome.printThrough(text, tx, ty, palette, invert)
function Chrome.rawPalette(palette, invert)
if invert then return { palette[4], palette[3], palette[2], palette[1] } end
return palette
end
-- The per-glyph shader/tint switch printThrough needs, factored out so a
-- caller that positions its own glyphs (TextBox's typewriter/scroll
-- animation) gets the same shaded ink without re-deriving the TTF-vs-tile
-- split.
--
-- Returns (pal, drawGlyph, finish):
-- pal the resolved ramp, or nil if there's no shader to draw
-- through; a caller that also draws a paper rect should skip
-- it and take the plain Chrome.print degrade instead.
-- drawGlyph (code, x, y): draws one glyph, shaded through `pal` for a
-- tile glyph or tinted with pal[4] for a TTF one (see the
-- inline comment below, gen1recomp#1642).
-- finish call once after the last glyph to restore the previous
-- shader and draw colour.
function Chrome.paletteGlyphs(palette, invert, raw)
if not (palette and GbcPalette.available()) then
return Chrome.print(text, tx, ty)
return nil, Font.drawCode, function() end
end
-- `pal` is used below and never touches the caller's palette again --
-- including useRaw for the draw, since useRaw skips the fold GbcPalette.use
-- would otherwise apply and folding it a second time would undo this.
local pal = Chrome.throughPalette(palette, invert)
local width = Font.width(text)
local paper = pal[1] or { 255, 255, 255 }
love.graphics.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
love.graphics.rectangle("fill", tx * 8, ty * 8, width, 8)
-- Per glyph, not per string: a TTF-mod build still keeps the multi-byte
-- charmap sequences (the naming screen's <PK>/<MN>, the 'd/'l/'s ligatures)
-- and anything the mod names in ttf.tiles (Font.lua's own note on keeping
-- digits tile-based for column alignment) on their ROM tiles, so a string
-- can freely mix the two kinds of glyph.
local pal = raw and Chrome.rawPalette(palette, invert)
or Chrome.throughPalette(palette, invert)
local ink = pal[4] or { 0, 0, 0 }
local previous = love.graphics.getShader()
local shaded = false
local pen = tx * 8
for _, code in ipairs(Font.encode(text)) do
local function drawGlyph(code, x, y)
if code >= Font.TTF_BASE then
-- The shader below recovers a shade from the RED CHANNEL of an already
-- flat-shaded 2bpp tile sheet -- exactly what a TTF glyph is not.
-- LÖVE's font rasterizer stores glyph coverage as alpha over a plain
-- white texture, which this shader reads back as shade 0 no matter how
@@ -167,11 +219,28 @@ function Chrome.printThrough(text, tx, ty, palette, invert)
GbcPalette.useRaw(pal)
shaded = true
end
Font.drawCode(code, pen, ty * 8)
Font.drawCode(code, x, y)
end
local function finish()
if shaded then love.graphics.setShader(previous) end
love.graphics.setColor(0, 0, 0, 1)
end
return pal, drawGlyph, finish
end
function Chrome.printThrough(text, tx, ty, palette, invert, raw)
local pal, drawGlyph, finish = Chrome.paletteGlyphs(palette, invert, raw)
if not pal then return Chrome.print(text, tx, ty) end
local width = Font.width(text)
local paper = pal[1] or { 255, 255, 255 }
love.graphics.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
love.graphics.rectangle("fill", tx * 8, ty * 8, width, 8)
local pen = tx * 8
for _, code in ipairs(Font.encode(text)) do
drawGlyph(code, pen, ty * 8)
pen = pen + Font.advanceOf(code)
end
if shaded then love.graphics.setShader(previous) end
love.graphics.setColor(0, 0, 0, 1)
finish()
return width
end
@@ -186,12 +255,34 @@ end
-- Right-aligned within a field that ends at tile `txEnd` (exclusive), which is
-- how Gold prints numbers (PrintNum fills from the right).
function Chrome.printRight(text, txEnd, ty)
local function flatPrintRight(text, txEnd, ty)
local width = Font.width(text)
love.graphics.setColor(0, 0, 0, 1)
return Font.draw(text, txEnd * 8 - width, ty * 8)
end
function Chrome.printRight(text, txEnd, ty)
if not GbcPalette.available() then return flatPrintRight(text, txEnd, ty) end
return Chrome.printRightThrough(text, txEnd, ty, Chrome.DEFAULT_BOX_PALETTE)
end
function Chrome.printRightThrough(text, txEnd, ty, palette, invert, raw)
local pal, drawGlyph, finish = Chrome.paletteGlyphs(palette, invert, raw)
if not pal then return Chrome.printRight(text, txEnd, ty) end
local width = Font.width(text)
local tx = txEnd * 8 - width
local paper = pal[1] or { 255, 255, 255 }
love.graphics.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
love.graphics.rectangle("fill", tx, ty * 8, width, 8)
local pen = tx
for _, code in ipairs(Font.encode(text)) do
drawGlyph(code, pen, ty * 8)
pen = pen + Font.advanceOf(code)
end
finish()
return width
end
-- Wrap text to `width` tiles, measuring with the real font so a proportional
-- page wraps where it actually overflows. Nothing on a 160px screen may print
-- past tile 20: text that does is drawn outside the GB frame entirely.
@@ -227,21 +318,27 @@ function Chrome.printWrapped(text, tx, ty, width, rows)
return #lines
end
function Chrome.cursor(tx, ty, hollow)
local function flatCursor(tx, ty, hollow)
love.graphics.setColor(0, 0, 0, 1)
Font.drawCode(hollow and Chrome.CURSOR_HOLLOW or Chrome.CURSOR, tx * 8, ty * 8)
end
function Chrome.cursor(tx, ty, hollow)
if not GbcPalette.available() then return flatCursor(tx, ty, hollow) end
return Chrome.cursorThrough(tx, ty, Chrome.DEFAULT_BOX_PALETTE, nil, hollow)
end
-- The cursor glyph through a palette, the way Chrome.printThrough draws text.
-- A screen whose font page is inverted (the #DEX: Pokedex_LoadInvertedFont
-- xors both bitplanes, so it runs white on black) needs its cursor inverted
-- with everything else -- Chrome.cursor's flat black is invisible against that
-- ground, which is what made the dex's action arrow look absent.
function Chrome.cursorThrough(tx, ty, palette, invert, hollow)
function Chrome.cursorThrough(tx, ty, palette, invert, hollow, raw)
if not (palette and GbcPalette.available()) then
return Chrome.cursor(tx, ty, hollow)
end
local pal = Chrome.throughPalette(palette, invert)
local pal = raw and Chrome.rawPalette(palette, invert)
or Chrome.throughPalette(palette, invert)
local paper = pal[1] or { 255, 255, 255 }
love.graphics.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
love.graphics.rectangle("fill", tx * 8, ty * 8, 8, 8)
@@ -286,8 +383,8 @@ end
-- PRINTNUM_LEADINGZEROS.
function Chrome.coinBalanceBox(coins)
Chrome.textbox(11, 0, 7, 1)
Chrome.print("COIN", 12, 0)
Chrome.print(Chrome.number(coins, 4, true), 13, 1)
Chrome.printThrough("COIN", 12, 0, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough(Chrome.number(coins, 4, true), 13, 1, Chrome.DEFAULT_BOX_PALETTE)
end
-- DisplayMoneyAndCoinBalance: one Textbox at (5,0) with a 13x3 interior
@@ -295,10 +392,10 @@ end
-- "COIN" at (6,3) with the four-digit count at (15,3).
function Chrome.moneyAndCoinBalanceBox(money, coins)
Chrome.textbox(5, 0, 13, 3)
Chrome.print("MONEY", 6, 1)
Chrome.print(Chrome.money(money), 12, 1)
Chrome.print("COIN", 6, 3)
Chrome.print(Chrome.number(coins, 4, true), 15, 3)
Chrome.printThrough("MONEY", 6, 1, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough(Chrome.money(money), 12, 1, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough("COIN", 6, 3, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough(Chrome.number(coins, 4, true), 15, 3, Chrome.DEFAULT_BOX_PALETTE)
end
-- PlaceMoneyTopRight: MoneyTopRightMenuHeader is `menu_coords 11, 0,
@@ -306,7 +403,7 @@ end
-- prints the number at MenuBoxCoord2Tile + SCREEN_WIDTH + 1, i.e. (12,1).
function Chrome.moneyBalanceBox(money)
Chrome.box(11, 0, 9, 3)
Chrome.print(Chrome.money(money), 12, 1)
Chrome.printThrough(Chrome.money(money), 12, 1, Chrome.DEFAULT_BOX_PALETTE)
end
-- A vertical cursor list.
@@ -344,6 +441,7 @@ function List.new(opts)
self.onChoose = opts.onChoose
self.onCancel = opts.onCancel
self.onMove = opts.onMove
self.palette = opts.palette == nil and Chrome.DEFAULT_BOX_PALETTE or opts.palette
self.index = math.max(1, math.min(opts.index or 1, math.max(1, #self.items)))
self.scroll = 0
self:ensureVisible()
@@ -409,15 +507,32 @@ function List:draw()
local item = self.items[i]
if item then
local ty = self.y + (row - 1) * self.spacing
if i == self.index then Chrome.cursor(self.x - 1, ty) end
Chrome.print(item.label, self.x, ty)
if i == self.index then
if self.palette then
Chrome.cursorThrough(self.x - 1, ty, self.palette)
else
flatCursor(self.x - 1, ty)
end
end
if self.palette then
Chrome.printThrough(item.label, self.x, ty, self.palette)
else
flatPrint(item.label, self.x, ty)
end
end
end
-- Scrolling lists get the ▼ hint Gold shows when there is more below.
if self.rows < #self.items and self.scroll + self.rows < #self.items then
love.graphics.setColor(0, 0, 0, 1)
Font.drawCode(Chrome.DOWN_ARROW, (self.x - 1) * 8,
(self.y + self.rows * self.spacing - 1) * 8)
if self.palette then
local _, drawGlyph, finish = Chrome.paletteGlyphs(self.palette)
drawGlyph(Chrome.DOWN_ARROW, (self.x - 1) * 8,
(self.y + self.rows * self.spacing - 1) * 8)
finish()
else
love.graphics.setColor(0, 0, 0, 1)
Font.drawCode(Chrome.DOWN_ARROW, (self.x - 1) * 8,
(self.y + self.rows * self.spacing - 1) * 8)
end
end
end
+1 -2
View File
@@ -176,8 +176,7 @@ end
function ContestMenu:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -841,8 +841,7 @@ end
function Credits:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(0, 0, 0, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 0, 0, 0)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -1275,8 +1275,7 @@ end
function CrystalIntro:drawWidescreen(winW, winH)
local G = love.graphics
local fill = self:surroundColor()
G.setColor(fill[1] / 255, fill[2] / 255, fill[3] / 255, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, fill[1] / 255, fill[2] / 255, fill[3] / 255)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -410,8 +410,7 @@ end
function CrystalSplash:drawWidescreen(winW, winH)
local G = love.graphics
local backdrop = GbcPalette.color(self.bgColors, 1) or BLACK
G.setColor(backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -263,8 +263,7 @@ end
function DecorationMenu:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -180,8 +180,7 @@ function Diploma:drawWidescreen(winW, winH)
-- Nil palette degrades to the DMG ramp's colour 1, i.e. white, matching the
-- Chrome.clear() the panel falls back to when the cache has no gfx/diploma.
local paper = GbcPalette.color(self:palette(), 1)
G.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, paper[1] / 255, paper[2] / 255, paper[3] / 255)
G.setColor(1, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
+1 -2
View File
@@ -440,8 +440,7 @@ end
function EggHatchAnim:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -567,8 +567,7 @@ end
function EvolutionAnim:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -369,8 +369,7 @@ end
function GameFreakPresents:drawWidescreen(winW, winH)
local G = love.graphics
local backdrop = GbcPalette.color(self.bgColors, 1) or BLACK
G.setColor(backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -125,8 +125,7 @@ end
function GenderSelect:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
G.push()
+1 -2
View File
@@ -1029,8 +1029,7 @@ end
function GoldSilverIntro:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(0, 0, 0, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 0, 0, 0)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -668,8 +668,7 @@ end
function HallOfFame:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(0, 0, 0, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 0, 0, 0)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -358,8 +358,7 @@ end
function InitClock:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
G.push()
+1 -2
View File
@@ -598,8 +598,7 @@ end
function ItemPcMenu:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -444,8 +444,7 @@ function MagnetTrainRide:drawsWidescreen() return true end
function MagnetTrainRide:drawWidescreen(winW, winH)
local G = love.graphics
local backdrop = self:backdrop()
G.setColor(backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, backdrop[1] / 255, backdrop[2] / 255, backdrop[3] / 255)
G.setColor(1, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
+1 -2
View File
@@ -336,8 +336,7 @@ end
function MailCompose:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(0.62, 0.62, 0.62, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 0.62, 0.62, 0.62)
G.setColor(1, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
+1 -2
View File
@@ -90,8 +90,7 @@ end
function MailRead:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -215,8 +215,7 @@ end
function MainMenu:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -150,8 +150,7 @@ end
-- edge, at the one integer scale Chrome.fitScale picks.
function MoveTutor:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -239,8 +239,7 @@ end
function NamePick:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -566,8 +566,7 @@ function NamingScreen:drawWidescreen(winW, winH)
local scale = Chrome.fitScale(winW, winH)
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
local paper = self:paperColor()
G.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, paper[1] / 255, paper[2] / 255, paper[3] / 255)
G.setColor(1, 1, 1, 1)
if self.tiles.border then
local tilesX = math.ceil(winW / (8 * scale))
+1 -2
View File
@@ -719,8 +719,7 @@ end
function OakSpeech:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
G.push()
+208 -26
View File
@@ -6,7 +6,7 @@
-- literal "TYPE". The cursor is a ▶ in column 1 at row 2 + index * 2.
--
-- Up/down move between rows, left/right change the value under the cursor, and
-- START or B leaves. CANCEL is a row like any other that simply exits.
-- START or B leaves. BACK is a row like any other that simply exits.
--
-- Values are stored on the save's options table by name (see
-- src/core/gen2/Save.lua DEFAULT_OPTIONS) rather than as the packed wOptions
@@ -20,6 +20,8 @@
local Chrome = require("src.ui.gen2.Chrome")
local Logger = require("src.core.Logger")
local Marquee = require("src.ui.Marquee")
local Palette = require("src.render.Palette")
local Performance = require("src.core.Performance")
local Runtime = require("src.mods.Runtime")
local Save = require("src.core.gen2.Save")
@@ -41,6 +43,45 @@ local function stepVolume(v, delta)
return math.max(0, math.min(7, (v or 7) + delta))
end
local function dropClassic(options)
local GbcPalette = require("src.render.GbcPalette")
if GbcPalette.mode ~= "classic" then return end
local target = (options.palette and options.palette ~= "")
and GbcPalette.CUSTOM_MODE or "gbc"
options.color = target
GbcPalette.setMode(target)
end
local function colorPickerOpts(game)
local GbcPalette = require("src.render.GbcPalette")
local options = game.options
dropClassic(options)
local labels = GbcPalette.MODE_LABELS or {}
local modes = {}
for i, id in ipairs(GbcPalette.MODES) do
modes[i] = { labels[id] or id:upper(), id }
end
return {
palette = Palette,
get = function() return options.palette or "" end,
set = function(v)
options.palette = v
local target = (v ~= "") and GbcPalette.CUSTOM_MODE or "gbc"
options.color = target
GbcPalette.setMode(target)
GbcPalette.setCustomRamp(v ~= "" and Palette.ramp(v) or nil)
if game.persistOptions then pcall(game.persistOptions, game) end
end,
modes = modes,
getMode = function() return options.color or "gbc" end,
setMode = function(v)
options.color = v
GbcPalette.setMode(v)
if game.persistOptions then pcall(game.persistOptions, game) end
end,
}
end
-- Each row: the label, the option key it edits, and the cycle of values with
-- the exact strings the cart prints (trailing spaces included -- they are what
-- blank the longer previous value, e.g. "MID " over "SLOW").
@@ -191,24 +232,30 @@ local ROWS = {
text = function(options)
return require("src.render.Tilt").levelLabel(options.tilt or 0)
end },
-- COLOR is the Gen 2 answer to the Gen 1 screen's COLORS row. Gold is a
-- CGB game whose colour comes from its own palettes, so there are no packs
-- to swap -- what there is instead is the choice to turn that colour OFF,
-- down to the grey Game Boy or the green one. GBC is the default.
{ label = Strings.source("COLOR"), key = "color", port = true,
cycle = function(options, delta)
local GbcPalette = require("src.render.GbcPalette")
GbcPalette.setMode(options.color or "gbc")
options.color = GbcPalette.cycle(delta)
end,
text = function(options)
local name = Palette.label(options.palette)
if name then return name end
return require("src.render.GbcPalette").modeLabel(options.color or "gbc")
end,
activate = function(game)
require("src.ui.Screens").push(game, "PaletteScreen", colorPickerOpts(game))
end },
-- SHADER FX reaches Gen 2 too, not just Gen 1. Same "activate" shape as
-- CONTROLS/TOUCH LAYOUT below (a pushed screen, not a `cycle` ladder) --
-- ShaderFXScreen is the shared list screen both generations push, `id`
-- matching the Gen 1 row's so a mod filtering "shaderfx" on Red also
-- reaches Gold.
{ id = "uiLetterbox", label = Strings.source("UI LETTERBOX"), port = true,
cycle = function(options, delta)
local Letterbox = require("src.render.Letterbox")
options.uiLetterbox = Letterbox.cycle(options.uiLetterbox, delta)
Letterbox.setMode(options.uiLetterbox)
end,
text = function(options)
local Letterbox = require("src.render.Letterbox")
return Strings(Letterbox.label(options.uiLetterbox))
end },
{ id = "shaderfx", label = Strings.source("SHADER FX"), port = true,
text = function(options)
local ShaderFX = require("src.render.ShaderFX")
@@ -294,7 +341,7 @@ local ROWS = {
{ label = Strings.source("BATTLE BG"), key = "battleBg", port = true,
values = { "white", "black" },
display = { white = "WHITE", black = "BLACK" } },
{ label = Strings.source("CANCEL"), cancel = true },
{ label = Strings.source("BACK"), cancel = true },
}
-- The cart's screen is one full-height textbox with every row on it. This one
@@ -303,6 +350,69 @@ local ROWS = {
-- only when the cursor would leave it.
local VISIBLE_ROWS = 7
-- The same grouping the Gen 1 screen uses (src/ui/OptionsMenu.lua GROUPS),
-- with Gold's own rows in it: FRAME is a graphics setting here and SOUND is
-- an audio one. Runs after the ui.options.rows hook and only builds
-- self.view, so self.rows stays the flat list a mod reads and edits.
local GROUPS = {
{ id = "group.speed", label = Strings.source("SPEED"),
members = { "textSpeed", "speed" } },
{ id = "group.video", label = Strings.source("VIDEO"),
members = { "videoMode", "screenPos", "fpsCap" } },
{ id = "group.graphics", label = Strings.source("GRAPHICS"),
members = { "color", "uiLetterbox", "shaderfx", "shaderfx2", "frame" } },
{ id = "group.audio", label = Strings.source("AUDIO"),
members = { "sound", "musicVol", "sfxVol", "musicFilter" } },
{ id = "group.battle", label = Strings.source("BATTLE OPTIONS"),
members = { "battleScene", "battleStyle", "battleBg" } },
{ id = "group.extras", label = Strings.source("EXTRAS"),
members = { "zoom", "voidFill", "tilt" } },
}
local ORDER = {
"group.speed", "group.video", "group.graphics", "group.audio",
"performance", "group.battle", "group.extras",
}
-- A group's page is this same screen driving the rows it was handed, with
-- its own BACK on the bottom.
local function groupView(rows, open)
local owner, picked = {}, {}
for _, group in ipairs(GROUPS) do
for _, id in ipairs(group.members) do owner[id] = group end
picked[group.id] = {}
end
for _, row in ipairs(rows) do
if row.id and owner[row.id] then
local into = picked[owner[row.id].id]
into[#into + 1] = row
end
end
local made, byId = {}, {}
for _, group in ipairs(GROUPS) do
local members = picked[group.id]
if #members > 0 then
-- No value line: the value column is eight characters wide here, and a
-- group has no setting of its own to show in it anyway.
made[group.id] = { id = group.id, label = group.label, group = true,
activate = function(game) open(game, members) end }
end
end
for _, row in ipairs(rows) do
if row.id and not owner[row.id] then byId[row.id] = row end
end
local view, taken = {}, {}
for _, id in ipairs(ORDER) do
local row = made[id] or byId[id]
if row then view[#view + 1] = row; taken[id] = true end
end
for _, row in ipairs(rows) do
local id = row.id
if not (id and (owner[id] or taken[id])) then view[#view + 1] = row end
end
return view
end
-- ui.options.rows identity: an unhooked build hands its own rows back.
local function sameRows(_, rows) return rows end
@@ -323,8 +433,17 @@ local function buildRows()
local showTouch = env == "1"
or (env ~= "0" and (osName == "Android" or osName == "iOS"))
for i, row in ipairs(ROWS) do
if showTouch or (row.id ~= "touchControls" and row.id ~= "touchLayout"
and row.id ~= "haptics") then
-- PRINT is wGBPrinterBrightness, how dark the Game Boy Printer prints
-- (GBPRINTER_LIGHTEST..DARKEST, constants/ram_constants.asm:67-70).
-- There is no printer here (src/script/gen2/Specials.lua answers the
-- print specials with "no Game Boy Printer"), so nothing reads the
-- value and the row is hidden rather than offering a dead setting.
-- The descriptor and the save key stay, so a build that grows a printer
-- only has to drop this test.
local hidden = row.key == "print"
or (not showTouch and (row.id == "touchControls"
or row.id == "touchLayout" or row.id == "haptics"))
if not hidden then
local copy = {}
for key, value in pairs(row) do copy[key] = value end
copy.id = copy.id or copy.key or (copy.cancel and "cancel") or nil
@@ -338,6 +457,22 @@ function OptionsMenu:wantsFillScale() return true end
function OptionsMenu:drawsWidescreen() return true end
-- opts: options (the table to edit in place), onDone(options)
-- The page edits the SAME options table the parent screen does, so a value
-- changed on it lands where the caller's onDone will read it.
local function pushGroup(parent, members)
local page = setmetatable({}, OptionsMenu)
page.game = parent.game
page.rows = members
page.options = parent.options
page.index, page.scroll, page.sub = 1, 0, true
page.view = {}
for i, row in ipairs(members) do page.view[i] = row end
page.view[#page.view + 1] = { label = Strings.source("BACK"), cancel = true,
id = "cancel" }
parent.game.stack:push(page)
return page
end
function OptionsMenu.new(game, opts)
opts = opts or {}
local self = setmetatable({}, OptionsMenu)
@@ -363,21 +498,57 @@ function OptionsMenu.new(game, opts)
self.onDone = opts.onDone
self.index = 1
self.scroll = 0
self.view = groupView(rows, function(_, members)
return pushGroup(self, members)
end)
return self
end
function OptionsMenu:visible()
return self.view or self.rows
end
function OptionsMenu:ensureVisible()
local rows = self:visible()
if self.index <= self.scroll then
self.scroll = self.index - 1
elseif self.index > self.scroll + VISIBLE_ROWS then
self.scroll = self.index - VISIBLE_ROWS
end
self.scroll = math.max(0,
math.min(self.scroll, math.max(0, #self.rows - VISIBLE_ROWS)))
math.min(self.scroll, math.max(0, #rows - VISIBLE_ROWS)))
end
function OptionsMenu:row()
return self.rows[self.index]
return self:visible()[self.index]
end
-- Cursor onto a row by id, opening its group page first if it lives in one.
-- Returns the screen the row ended up on, so a caller can keep driving it.
function OptionsMenu:focusRow(id)
local rows = self:visible()
for i, row in ipairs(rows) do
if row.id == id then
self.index = i
self:ensureVisible()
return self
end
end
for i, row in ipairs(rows) do
if row.group and row.activate then
local group
for _, g in ipairs(GROUPS) do if g.id == row.id then group = g end end
for _, member in ipairs(group and group.members or {}) do
if member == id then
self.index = i
row.activate(self.game)
local page = self.game.stack:top()
return page.focusRow and page:focusRow(id) or page
end
end
end
end
return nil
end
function OptionsMenu:cycle(row, delta)
@@ -437,12 +608,13 @@ function OptionsMenu:update(_dt)
self:leave_()
return
end
local rows = self:visible()
if input:wasPressed("up") then
self.index = self.index > 1 and self.index - 1 or #self.rows
self.index = self.index > 1 and self.index - 1 or #rows
self:ensureVisible()
return
elseif input:wasPressed("down") then
self.index = self.index < #self.rows and self.index + 1 or 1
self.index = self.index < #rows and self.index + 1 or 1
self:ensureVisible()
return
end
@@ -473,35 +645,46 @@ function OptionsMenu:drawPanel()
Chrome.clear()
-- hlcoord 0,0 with b = SCREEN_HEIGHT - 2, c = SCREEN_WIDTH - 2.
Chrome.textbox(0, 0, Chrome.SCREEN_W - 2, Chrome.SCREEN_H - 2)
for slot = 1, math.min(VISIBLE_ROWS, #self.rows) do
-- The textbox spends column 19 on its frame, so a line drawn past column
-- 18 prints over the border: 17 characters from the label's column 2,
-- 8 from the value's column 11. Anything longer scrolls under the cursor.
local rows = self:visible()
for slot = 1, math.min(VISIBLE_ROWS, #rows) do
local i = slot + self.scroll
local row = self.rows[i]
local row = rows[i]
if row then
local labelY = 2 + (slot - 1) * 2
Chrome.print(Strings(row.label), 2, labelY)
local hot = i == self.index
local key = tostring(row.id or row.label)
local function fit(text, col)
local room = 19 - col
if hot then return Marquee.scroll(text, room, key) end
return Marquee.clip(text, room)
end
Chrome.print(fit(Strings(row.label), 2), 2, labelY)
if row.frame then
Chrome.print(Strings(":TYPE"), 10, labelY + 1)
Chrome.print(tostring(self.options.frame or 1), 16, labelY + 1)
elseif row.text then
Chrome.print(":", 10, labelY + 1)
Chrome.print(row.text(self.options), 11, labelY + 1)
Chrome.print(fit(row.text(self.options), 11), 11, labelY + 1)
elseif row.values then
Chrome.print(":", 10, labelY + 1)
local value = self.options[row.key]
local text = row.display and Strings(row.display[value]) or tostring(value)
Chrome.print(text, 11, labelY + 1)
Chrome.print(fit(text, 11), 11, labelY + 1)
elseif type(row.value) == "function" then
-- the Gen 1 row's value reader (src/ui/OptionRows.lua:4), so a mod row
-- shows its setting here instead of drawing a bare label
local ok, text = pcall(row.value, self.game)
Chrome.print(":", 10, labelY + 1)
Chrome.print(ok and tostring(text) or "?", 11, labelY + 1)
Chrome.print(fit(ok and tostring(text) or "?", 11), 11, labelY + 1)
end
end
end
Chrome.cursor(1, 2 + (self.index - self.scroll - 1) * 2)
-- The ▼ hint every scrolling Gen 2 list shows when there is more below.
if self.scroll + VISIBLE_ROWS < #self.rows then
if self.scroll + VISIBLE_ROWS < #rows then
local Font = require("src.render.Font")
love.graphics.setColor(0, 0, 0, 1)
Font.drawCode(Chrome.DOWN_ARROW, 1 * 8, (2 + VISIBLE_ROWS * 2 - 1) * 8)
@@ -514,8 +697,7 @@ end
function OptionsMenu:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+2 -3
View File
@@ -21,6 +21,7 @@
local Assets = require("src.render.Assets")
local GbcPalette = require("src.render.GbcPalette")
local Chrome = require("src.ui.gen2.Chrome")
local PackGfx = {}
PackGfx.__index = PackGfx
@@ -111,9 +112,7 @@ end
-- Everything behind the item list: the header strip, the background column,
-- the bag picture for this pocket, and the pocket plaque.
function PackGfx:draw(pocketId)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, SCREEN_W * 8, SCREEN_H * 8)
Chrome.paletteFill(0, 0, SCREEN_W * 8, SCREEN_H * 8, Chrome.DEFAULT_BOX_PALETTE)
-- ◀▶ POCKET ▼▲ ITEMS: 20 running tiles from $28.
local header = self.gfx.headerFirstTile or 0x28
+19 -18
View File
@@ -973,22 +973,22 @@ function PackMenu:drawList(listX, listY)
local entry = self.rows[i]
-- engine/items/tmhm.asm:355-385 (#1695)
if entry.tmhmLabel then
Chrome.print(entry.tmhmLabel, listX - 3, ty)
Chrome.printThrough(entry.tmhmLabel, listX - 3, ty, Chrome.DEFAULT_BOX_PALETTE)
end
if i == self.index then
self:cursorAt(listX - 1, ty)
elseif i == self.switching then
self:cursorAt(listX - 1, ty, true)
end
Chrome.print((entry.tmhmLabel and entry.teaches) or entry.name,
listX, ty)
Chrome.printThrough((entry.tmhmLabel and entry.teaches) or entry.name,
listX, ty, Chrome.DEFAULT_BOX_PALETTE)
if entry.showCount then
Chrome.print("\xc3\x97" .. Chrome.number(entry.count, 2),
listX + 9, ty + 1)
Chrome.printThrough("\xc3\x97" .. Chrome.number(entry.count, 2),
listX + 9, ty + 1, Chrome.DEFAULT_BOX_PALETTE)
end
elseif i == self:total() then
if i == self.index then self:cursorAt(listX - 1, ty) end
Chrome.print("CANCEL", listX, ty)
Chrome.printThrough("CANCEL", listX, ty, Chrome.DEFAULT_BOX_PALETTE)
end
end
end
@@ -1006,7 +1006,8 @@ function PackMenu:drawDescription(ty)
local top, step = ty, 2
if #lines > 2 then top, step = ty - 1, 1 end
for i, line in ipairs(lines) do
Chrome.print((line:gsub("{PLAYER}", name)), 1, top + (i - 1) * step)
Chrome.printThrough((line:gsub("{PLAYER}", name)), 1, top + (i - 1) * step,
Chrome.DEFAULT_BOX_PALETTE)
end
return
end
@@ -1019,8 +1020,8 @@ function PackMenu:drawDescription(ty)
-- the second line is row 16. '\n' covers hand-written data.
local first, second = description:match("^(.-)<NEXT>(.*)$")
if not first then first, second = description:match("^(.-)\n(.*)$") end
Chrome.print(first or description, 1, ty)
if second then Chrome.print(second, 1, ty + 2) end
Chrome.printThrough(first or description, 1, ty, Chrome.DEFAULT_BOX_PALETTE)
if second then Chrome.printThrough(second, 1, ty + 2, Chrome.DEFAULT_BOX_PALETTE) end
end
-- The submenu box. Every one of the seven headers is `menu_coords 0, top,
@@ -1037,23 +1038,24 @@ function PackMenu:drawSubmenu()
Chrome.box(0, top, 7, bottom - top + 1)
for i, id in ipairs(menu.rows) do
local ty = top + 1 + (i - 1) * 2
if i == menu.index then Chrome.cursor(1, ty) end
Chrome.print(SUBMENU_LABEL[id] or id, 2, ty)
if i == menu.index then Chrome.cursorThrough(1, ty, Chrome.DEFAULT_BOX_PALETTE) end
Chrome.printThrough(SUBMENU_LABEL[id] or id, 2, ty, Chrome.DEFAULT_BOX_PALETTE)
end
end
-- engine/items/buy_sell_toss.asm:133 BuySellToss_UpdateQuantityDisplay
function PackMenu:drawQuantity()
Chrome.box(15, 9, 5, 3)
Chrome.print("\xc3\x97" .. Chrome.number(self.qtyState.qty, 2, true), 16, 10)
Chrome.printThrough("\xc3\x97" .. Chrome.number(self.qtyState.qty, 2, true), 16, 10,
Chrome.DEFAULT_BOX_PALETTE)
end
-- YesNoBox's own coords, the same box every other Gen 2 screen here draws.
function PackMenu:drawYesNo()
Chrome.box(14, 7, 6, 5)
Chrome.print("YES", 16, 8)
Chrome.print("NO", 16, 10)
Chrome.cursor(15, self.confirm.choice == 1 and 8 or 10)
Chrome.printThrough("YES", 16, 8, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough("NO", 16, 10, Chrome.DEFAULT_BOX_PALETTE)
Chrome.cursorThrough(15, self.confirm.choice == 1 and 8 or 10, Chrome.DEFAULT_BOX_PALETTE)
end
function PackMenu:drawOverlays()
@@ -1080,7 +1082,7 @@ function PackMenu:drawPanel()
-- boxes, the layout this screen shipped with.
Chrome.clear()
Chrome.box(0, 0, 20, 3)
Chrome.print(self:pocket().label, 2, 1)
Chrome.printThrough(self:pocket().label, 2, 1, Chrome.DEFAULT_BOX_PALETTE)
Chrome.box(0, 3, 20, 12)
self:drawList(5, 4)
Chrome.box(0, 12, 20, 6)
@@ -1095,8 +1097,7 @@ end
function PackMenu:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -845,8 +845,7 @@ end
function PartyMenu:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -456,8 +456,7 @@ end
function PcMenu:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -202,8 +202,7 @@ function PhotoStudio:drawsWidescreen() return true end
function PhotoStudio:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
G.push()
+1 -2
View File
@@ -1458,8 +1458,7 @@ end
function PokedexMenu:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(0, 0, 0, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 0, 0, 0)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+53 -26
View File
@@ -22,6 +22,7 @@
local Chrome = require("src.ui.gen2.Chrome")
local FieldMoves = require("src.world.gen2.FieldMoves")
local GbcPalette = require("src.render.GbcPalette")
local Gen2Save = require("src.core.gen2.Save")
local Clock = require("src.core.gen2.Clock")
local Font = require("src.render.Font")
@@ -971,6 +972,7 @@ function Pokegear.new(game, opts)
if gfx then
self.sheet = TileSheet.new({
path = gfx.tiles, wide = gfx.tilesWide or 16, firstTile = 0,
raw = true,
paletteFor = function(tile) return self:colorsFor(tile) end,
})
end
@@ -997,10 +999,9 @@ end
-- Every string on a Pokegear card is a run of font tiles laid straight into
-- the tilemap, so it wears BG palette 0 (PokegearPals' first entry) rather
-- than drawing as black ink over whatever was underneath.
function Pokegear:text(str, tx, ty)
local pals = self:pals()
return Chrome.printThrough(str, tx, ty, pals and pals[1])
return Chrome.printThrough(str, tx, ty, pals and pals[1], false, true)
end
-- wPokegearFlags' four card bits are ENGINE flags: EngineFlags rows 0-3 are
@@ -2411,6 +2412,8 @@ function Pokegear:drawPlain()
end
end
Pokegear.CUSTOM_RAMP_FILM = true
function Pokegear:drawPanel()
if not self:styled() then
self:drawPlain()
@@ -2418,29 +2421,54 @@ function Pokegear:drawPanel()
return
end
local G = love.graphics
-- InitPokegearTilemap ByteFills the whole SCREEN_AREA with $4f before the
-- card's tilemap goes down (engine/pokegear/pokegear.asm InitPokegearTilemap),
-- and every cell the card leaves blank is a font-page tile on palette 0, so
-- the ground under a card is the gear's paper, not white. See
-- Pokegear:paperColor.
local ground = self:groundColor()
G.setColor(ground[1] / 255, ground[2] / 255, ground[3] / 255, 1)
G.rectangle("fill", 0, 0, SCREEN_W * 8, SCREEN_H * 8)
local id = self:card() and self:card().id
if id == "map" then
self:drawMap()
elseif id == "radio" then
self:drawRadio()
elseif id == "phone" then
self:drawPhone()
else
self:drawClock()
local function paint()
-- InitPokegearTilemap ByteFills the whole SCREEN_AREA with $4f before the
-- card's tilemap goes down (engine/pokegear/pokegear.asm InitPokegearTilemap),
-- and every cell the card leaves blank is a font-page tile on palette 0, so
-- the ground under a card is the gear's paper, not white. See
-- Pokegear:paperColor.
local ground = self:groundColor()
G.setColor(ground[1] / 255, ground[2] / 255, ground[3] / 255, 1)
G.rectangle("fill", 0, 0, SCREEN_W * 8, SCREEN_H * 8)
local id = self:card() and self:card().id
if id == "map" then
self:drawMap()
elseif id == "radio" then
self:drawRadio()
elseif id == "phone" then
self:drawPhone()
else
self:drawClock()
end
-- Last: the arrow is an OBJ and composites over whatever the card drew.
-- _FlyMap has no card strip and never animates it
-- (engine/pokegear/pokegear.asm:1999); neither does _TownMap
-- (../pokecrystal/engine/pokegear/pokegear.asm:1757).
if not (self.fly or self.townMap) then self:drawModeArrow() end
end
if Pokegear.CUSTOM_RAMP_FILM and GbcPalette.customRamp
and GbcPalette.available() then
if not self.filmCanvas then
self.filmCanvas = G.newCanvas(SCREEN_W * 8, SCREEN_H * 8)
self.filmCanvas:setFilter("nearest", "nearest")
end
local previousCanvas = G.getCanvas()
G.setCanvas(self.filmCanvas)
G.clear(0, 0, 0, 0)
G.push()
G.origin()
local ok, err = pcall(paint)
G.pop()
G.setCanvas(previousCanvas)
if not ok then error(err, 0) end
G.setColor(1, 1, 1, 1)
GbcPalette.with(GbcPalette.customRamp, function()
G.draw(self.filmCanvas, 0, 0)
end)
else
paint()
end
-- Last: the arrow is an OBJ and composites over whatever the card drew.
-- _FlyMap has no card strip and never animates it
-- (engine/pokegear/pokegear.asm:1999); neither does _TownMap
-- (../pokecrystal/engine/pokegear/pokegear.asm:1757).
if not (self.fly or self.townMap) then self:drawModeArrow() end
G.setColor(1, 1, 1, 1)
end
@@ -2454,8 +2482,7 @@ function Pokegear:drawWidescreen(winW, winH)
-- black card sitting inside a cream frame. It follows groundColor for the
-- same reason drawPanel does: $4f is what the screen is filled with.
local ground = self:groundColor()
G.setColor(ground[1] / 255, ground[2] / 255, ground[3] / 255, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, ground[1] / 255, ground[2] / 255, ground[3] / 255)
local scale = Chrome.fitScale(winW, winH)
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
G.push()
+1 -2
View File
@@ -556,8 +556,7 @@ end
function PrizeMenu:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -260,8 +260,7 @@ end
function SaveMenu:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -1531,8 +1531,7 @@ end
function SlotMachine:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -3
View File
@@ -295,9 +295,7 @@ function StartMenu:draw()
if not desc then return end
-- ._DrawMenuAccount ClearBox (0,13) 5 rows by 10, .PrintMenuAccount decoord
-- 0, 14 and the desc's `next` steps two rows (start_menu.asm:366-382).
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 13 * 8, 10 * 8, 5 * 8)
Chrome.paletteFill(0, 13 * 8, 10 * 8, 5 * 8)
Chrome.print(desc[1] or "", 0, 14)
Chrome.print(desc[2] or "", 0, 16)
end
+1 -2
View File
@@ -1266,8 +1266,7 @@ end
function SummaryMenu:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+6 -2
View File
@@ -18,7 +18,6 @@ local TileSheet = {}
TileSheet.__index = TileSheet
-- opts: path, wide, firstTile, palette (a 4-colour table) or palettes + a
-- paletteFor(tile, tx, ty) hook, transparent
function TileSheet.new(opts)
local self = setmetatable({}, TileSheet)
self.path = opts and opts.path
@@ -26,6 +25,7 @@ function TileSheet.new(opts)
self.firstTile = (opts and opts.firstTile) or 0
self.palette = opts and opts.palette
self.paletteFor = opts and opts.paletteFor
self.raw = opts and opts.raw
self.quads = {}
return self
end
@@ -79,7 +79,11 @@ function TileSheet:draw(tile, tx, ty)
if self.paletteFor then colors = self.paletteFor(tile, tx, ty) or colors end
local function body() G.draw(image, quad, tx * 8, ty * 8) end
if colors and GbcPalette.available() then
GbcPalette.with(colors, body)
if self.raw then
GbcPalette.withRaw(colors, body)
else
GbcPalette.with(colors, body)
end
else
body()
end
+1 -4
View File
@@ -316,11 +316,8 @@ function TitleState:update(_dt)
end
end
-- Which of the two baked sets is showing. GBC is the cart's colour; every
-- other COLOR mode wants the grey source, and CLASSIC's green comes from the
-- present pass over the finished frame rather than from a third set of art.
function TitleState:gray()
return GbcPalette.mode ~= "gbc"
return GbcPalette.mode == "dmg" or GbcPalette.mode == "classic"
end
function TitleState:art()
+1 -2
View File
@@ -870,8 +870,7 @@ end
function TradeAnimView:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+41 -34
View File
@@ -271,16 +271,24 @@ end
-- TrainerCard_PrintTopHalfOfCard runs once, in .InitRAM, and no page redraws
-- it -- so the name, ID, money and portrait stay on screen behind the badge
-- pages too.
function TrainerCard:print(text, tx, ty)
Chrome.printThrough(text, tx, ty, self:colorsAt(tx, ty))
end
function TrainerCard:cursor(tx, ty, hollow)
Chrome.cursorThrough(tx, ty, self:colorsAt(tx, ty), false, hollow)
end
function TrainerCard:drawTopHalf()
local player = (self.save or {}).player or {}
self:frame(0, 5)
Chrome.print("NAME/", 2, 2)
Chrome.print(player.name or "GOLD", 7, 2)
self:print("NAME/", 2, 2)
self:print(player.name or "GOLD", 7, 2)
self:tile(self.card, TILE_ID_NO[1], 2, 4)
self:tile(self.card, TILE_ID_NO[2], 3, 4)
Chrome.print(Chrome.number(player.id or 0, 5, true), 5, 4)
Chrome.print("MONEY", 2, 6)
Chrome.print(moneyText(player.money), 7, 6)
self:print(Chrome.number(player.id or 0, 5, true), 5, 4)
self:print("MONEY", 2, 6)
self:print(moneyText(player.money), 7, 6)
for x = 1, 12 do self:tile(self.card, TILE_DIVIDER, x, 3) end
self:tile(self.card, TILE_DIVIDER_END, 13, 3)
self:drawPortrait()
@@ -298,22 +306,22 @@ function TrainerCard:drawCard()
-- `#` is the compression byte for POKé, four tiles, so spelling it out is
-- what the cart actually draws.
Chrome.print("POKéDEX", 2, 10)
Chrome.print("PLAY TIME", 2, 12)
Chrome.print(Chrome.number(self:caughtCount(), 3), 15, 10)
self:print("POKéDEX", 2, 10)
self:print("PLAY TIME", 2, 12)
self:print(Chrome.number(self:caughtCount(), 3), 15, 10)
local time = save.playTime or {}
Chrome.print(Chrome.number(time.hours or 0, 4), 11, 12)
self:print(Chrome.number(time.hours or 0, 4), 11, 12)
-- The colon is $2e, which belongs to CardStatusGFX rather than the card
-- sheet, and TrainerCard_Page1_PrintGameTime xors it with ' ' every 32
-- frames -- which is what makes the clock look like it is running.
if math.floor(self.frames / 32) % 2 == 0 then
self:tile(self.status, TILE_COLON, 15, 12)
end
Chrome.print(Chrome.number(time.minutes or 0, 2, true), 16, 12)
self:print(Chrome.number(time.minutes or 0, 2, true), 16, 12)
Chrome.print("BADGES", 12, 15)
Chrome.cursor(18, 15)
self:print("BADGES", 12, 15)
self:cursor(18, 15)
end
-- TrainerCard_Page2_3_PlaceLeadersFaces: four tiles across the top row, then
@@ -397,22 +405,22 @@ function TrainerCard:drawPlain()
Chrome.clear()
if self.page == 1 then
Chrome.box(0, 0, 20, 9)
Chrome.print("NAME/", 2, 2)
Chrome.print(player.name or "GOLD", 7, 2)
Chrome.print("ID No", 2, 4)
Chrome.print(Chrome.number(player.id or 0, 5, true), 5, 4)
Chrome.print("MONEY", 2, 6)
Chrome.print(moneyText(player.money), 7, 6)
Chrome.printThrough("NAME/", 2, 2, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough(player.name or "GOLD", 7, 2, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough("ID No", 2, 4, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough(Chrome.number(player.id or 0, 5, true), 5, 4, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough("MONEY", 2, 6, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough(moneyText(player.money), 7, 6, Chrome.DEFAULT_BOX_PALETTE)
Chrome.box(0, 8, 20, 10)
Chrome.print("POKéDEX", 2, 10)
Chrome.print(Chrome.number(self:caughtCount(), 3), 15, 10)
Chrome.print("PLAY TIME", 2, 12)
Chrome.printThrough("POKéDEX", 2, 10, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough(Chrome.number(self:caughtCount(), 3), 15, 10, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough("PLAY TIME", 2, 12, Chrome.DEFAULT_BOX_PALETTE)
local time = save.playTime or {}
Chrome.print(Chrome.number(time.hours or 0, 4), 11, 12)
Chrome.print(":", 15, 12)
Chrome.print(Chrome.number(time.minutes or 0, 2, true), 16, 12)
Chrome.print("BADGES", 12, 15)
Chrome.cursor(18, 15)
Chrome.printThrough(Chrome.number(time.hours or 0, 4), 11, 12, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough(":", 15, 12, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough(Chrome.number(time.minutes or 0, 2, true), 16, 12, Chrome.DEFAULT_BOX_PALETTE)
Chrome.printThrough("BADGES", 12, 15, Chrome.DEFAULT_BOX_PALETTE)
Chrome.cursorThrough(18, 15, Chrome.DEFAULT_BOX_PALETTE)
return
end
local names = self.page == 2 and JOHTO_BADGES or KANTO_BADGES
@@ -423,12 +431,14 @@ function TrainerCard:drawPlain()
-- player.badges on both pages to match.
local held = player.badges or {}
Chrome.box(0, 0, 20, 9)
Chrome.print(self.page == 2 and "JOHTO BADGES" or "KANTO BADGES", 2, 2)
Chrome.printThrough(self.page == 2 and "JOHTO BADGES" or "KANTO BADGES", 2, 2,
Chrome.DEFAULT_BOX_PALETTE)
Chrome.box(0, 8, 20, 10)
for i, name in ipairs(names) do
local tx = 2 + ((i - 1) % 4) * 4
local ty = 10 + math.floor((i - 1) / 4) * 3
Chrome.print((held[i] or held[name]) and name:sub(1, 4) or "----", tx, ty)
Chrome.printThrough((held[i] or held[name]) and name:sub(1, 4) or "----", tx, ty,
Chrome.DEFAULT_BOX_PALETTE)
end
end
@@ -438,9 +448,7 @@ function TrainerCard:drawPanel()
love.graphics.setColor(1, 1, 1, 1)
return
end
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, SCREEN_W * 8, SCREEN_H * 8)
Chrome.paletteFill(0, 0, SCREEN_W * 8, SCREEN_H * 8, Chrome.DEFAULT_BOX_PALETTE)
local player = (self.save and self.save.player) or {}
if self.page == 1 then
self:drawCard()
@@ -453,7 +461,7 @@ function TrainerCard:drawPanel()
-- the same table page 2 reads.
self:drawBadges(JOHTO_BADGES, player.badges or {})
end
G.setColor(1, 1, 1, 1)
love.graphics.setColor(1, 1, 1, 1)
end
function TrainerCard:draw()
@@ -462,8 +470,7 @@ end
function TrainerCard:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+1 -2
View File
@@ -191,8 +191,7 @@ function UnownPrinter:drawsWidescreen() return true end
function UnownPrinter:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 1, 1, 1)
local scale = Chrome.fitScale(winW, winH)
local ox, oy = Chrome.fitOrigin(winW, winH, scale)
G.push()
+1 -2
View File
@@ -576,8 +576,7 @@ function UnownPuzzle:drawsWidescreen() return true end
function UnownPuzzle:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(0, 0, 0, 1)
G.rectangle("fill", 0, 0, winW, winH)
Chrome.letterbox(winW, winH, 0, 0, 0)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(Chrome.fitOrigin(winW, winH, scale))
+2 -1
View File
@@ -4,6 +4,7 @@
-- SpecialKabutoChamber, the four routines that open the chambers' walls.
local Assets = require("src.render.Assets")
local Chrome = require("src.ui.gen2.Chrome")
local Font = require("src.render.Font")
local GbcPalette = require("src.render.GbcPalette")
local Palettes = require("src.world.gen2.Palettes")
@@ -189,7 +190,7 @@ function UnownWords:draw()
local wall = self.wall
if not wall then return end
local bx, by, bw, bh = UnownWords.boxRect(wall)
Font.drawBox(bx, by, bw, bh)
Chrome.paletteBox(bx, by, bw, bh)
local tileset, atlas = self:tileset()
if not tileset then return end
local perRow = tileset.tilesPerRow or 16
+7 -5
View File
@@ -8217,7 +8217,8 @@ function World:mapCacheKey(mapId)
local daytime = self.daytime
local flicker = (daytime == "DARK") and self.flickerPhase or 1
return mapId .. "|" .. tostring(daytime)
.. "|" .. tostring(GbcPalette.mode) .. "|" .. tostring(flicker)
.. "|" .. tostring(GbcPalette.mode) .. "|" .. tostring(GbcPalette.customRamp)
.. "|" .. tostring(flicker)
end
function World:imageFor(mapId)
@@ -8440,7 +8441,8 @@ function World:borderImageFor(mapId)
-- stale bake (#1418).
local waterFrame = self:borderWaterFrame(def, tileset)
local cacheKey = BorderFill.cacheKey(mapId .. "|" .. tostring(daytime)
.. "|" .. tostring(GbcPalette.mode) .. "|" .. tostring(flicker)
.. "|" .. tostring(GbcPalette.mode) .. "|" .. tostring(GbcPalette.customRamp)
.. "|" .. tostring(flicker)
.. "|" .. tostring(BorderFill.voidFill or "fade")
.. "|" .. tostring(blockId)
.. "|" .. tostring(waterFrame and waterFrame.row or 0))
@@ -10500,12 +10502,12 @@ end
-- The COLOR option can change under a standing world (the hotkey, or the
-- OPTION screen closing), and the map is a baked canvas rather than a live
-- draw -- so the cached references have to be re-fetched when it does. The
-- bakes themselves are keyed by mode in imageFor, so this is a pointer swap
-- after the first time each mode is seen, not a re-bake.
function World:refreshColorMode()
local mode = GbcPalette.mode
if self.colorMode == mode then return end
local ramp = GbcPalette.customRamp
if self.colorMode == mode and self.colorRamp == ramp then return end
self.colorMode = mode
self.colorRamp = ramp
if not self.map then return end
self.mapImage = self:imageFor(self.map.id)
self:rebuildNeighbors()
@@ -20,8 +20,8 @@ return function(game)
end
assert(layoutRow and hudRow, "battle layout/HUD rows are present")
menu.index = 6
menu.scroll = 3
-- BATTLE HUD lives on the BATTLE OPTIONS page now, so focus it there.
assert(menu:focusRow("battleHud"), "BATTLE HUD row is reachable")
assert(hudRow.value(game) == "EXTENDED", "WIDE displays EXTENDED")
assert(U.shot(game, BEFORE), "WIDE/EXTENDED screenshot was written")
+46
View File
@@ -0,0 +1,46 @@
-- Diagnostic probe: forces a loud custom COLORS ramp (hot pink paper, blue
-- ink) and shoots StartMenu, PartyMenu, and PackMenu, so a gap between
-- those screens and the rest of the UI can be checked by eye.
--
-- POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/custom_ramp_probe.lua love .
local U = require("tests.drivers.util")
local GbcPalette = require("src.render.GbcPalette")
local StartMenu = require("src.ui.gen2.StartMenu")
local PartyMenu = require("src.ui.gen2.PartyMenu")
local PackMenu = require("src.ui.gen2.PackMenu")
return function(game)
local out = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/custom-ramp-probe"
local function shot(name)
U.wait(3)
U.shot(game, ("%s/%s.png"):format(out, name))
end
local function show(name, state)
game.stack:push(state)
shot(name)
game.stack:pop()
end
U.wait(45)
assert(game.world and game.world.map, "gold world did not boot")
print("[driver] GbcPalette.available() = " .. tostring(GbcPalette.available()))
GbcPalette.setCustomRamp({
{ 255, 100, 220 }, { 220, 60, 180 }, { 140, 20, 120 }, { 20, 0, 60 },
})
print("[driver] customRamp set, mode=" .. tostring(GbcPalette.mode))
local save = game.save
save.party = save.party or {}
save.inventory = { POTION = 5, SUPER_POTION = 2, ANTIDOTE = 1 }
show("01-startmenu-custom-ramp", StartMenu.new(game, { save = save }))
show("02-party-custom-ramp", PartyMenu.new(game, { prompt = "choose" }))
show("03-pack-items-custom-ramp", PackMenu.new(game, { pocket = "ITEM" }))
print("[driver] PASS custom ramp probe shots in " .. out)
end
@@ -28,8 +28,8 @@ return function(game)
assert(bgRow.step(game, 1) == false, "AUTO background row is locked")
assert(options.battleBg == "black", "locked AUTO does not overwrite the stored value")
menu.index = bgIndex
menu.scroll = math.max(0, bgIndex - 5)
-- BATTLE BG lives on the BATTLE OPTIONS page now, so focus it there.
assert(menu:focusRow("battleBg"), "BATTLE BG row is reachable")
U.wait(2)
local autoPath = DIR .. "/fill_extended_auto_menu.png"
os.remove(autoPath)
@@ -0,0 +1,30 @@
-- The grouped Gen 2 OPTION screen: the top level, one page, and a long
-- COLOR value scrolling under the cursor.
--
-- POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/gold_options_groups_shots.lua love .
local U = require("tests.drivers.util")
return function(game)
local OptionsMenu = require("src.ui.gen2.OptionsMenu")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
U.wait(5)
local menu = OptionsMenu.new(game, { options = game.options })
game.stack:push(menu)
U.wait(3)
U.shot(game, DIR .. "/g2_0_top.png")
local page = menu:focusRow("color")
U.wait(2)
U.shot(game, DIR .. "/g2_1_graphics.png")
game.options.palette = "1-A (Default) (BGB)"
U.wait(2)
U.shot(game, DIR .. "/g2_2_color_start.png")
U.wait(85)
U.shot(game, DIR .. "/g2_3_color_scrolled.png")
page.index = #page.view
U.wait(3)
U.shot(game, DIR .. "/g2_4_back.png")
U.log("GOLD_OPTIONS_GROUPS_DONE")
game.driverDone = true
end
+33
View File
@@ -0,0 +1,33 @@
-- Driver: the grouped OPTION screen -- the top level with its group openers,
-- one group's page, and the COLORS row scrolling its overlong value.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
U.teleport(game, "PALLET_TOWN", 10, 8, "down")
local OptionsMenu = require("src.ui.OptionsMenu")
local menu = OptionsMenu.new(game)
game.stack:push(menu)
U.wait(5)
U.shot(game, DIR .. "/opt_0_top.png")
menu:focusRow("group.battle")
U.wait(2)
U.shot(game, DIR .. "/opt_1_battle_opener.png")
U.tap(game, "a"); U.wait(3)
U.shot(game, DIR .. "/opt_2_battle_page.png")
U.tap(game, "b"); U.wait(3)
game.save.options.palette = "1-A (Default) (BGB)"
-- COLORS is on the GRAPHICS page, so focusRow hands back that page.
local graphics = menu:focusRow("colors")
U.wait(2)
U.shot(game, DIR .. "/opt_3_colors_start.png")
U.wait(85)
U.shot(game, DIR .. "/opt_4_colors_scrolled.png")
graphics.index = #graphics.rows + 1 -- the page's own BACK
U.wait(3)
U.shot(game, DIR .. "/opt_5_back.png")
U.log("OPTIONS_GROUPS_SHOTS_DONE")
game.driverDone = true
end
+12 -8
View File
@@ -1,18 +1,21 @@
-- Driver: the options screen with the port's audio rows (MUSIC VOL /
-- SFX VOL / MUSIC FILTER) to prove the 4-box viewport, the ▼ scroll
-- marker, and CANCEL fixed on the bottom line. The menu is pushed
-- directly (title-menu row order shifts when a save file exists, so
-- blind taps are unreliable).
-- marker, and BACK fixed on the bottom line. The audio rows live on the
-- AUDIO page now, so the menu focuses its way there rather than tapping
-- down a fixed number of times. The menu is pushed directly (title-menu
-- row order shifts when a save file exists, so blind taps are unreliable).
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
U.teleport(game, "PALLET_TOWN", 10, 8, "down")
local OptionsMenu = require("src.ui.OptionsMenu")
game.stack:push(OptionsMenu.new(game))
local menu = OptionsMenu.new(game)
game.stack:push(menu)
U.wait(5)
U.shot(game, DIR .. "/options_0_top.png")
for _ = 1, 4 do U.tap(game, "down"); U.wait(2) end
U.shot(game, DIR .. "/options_1_musicvol.png") -- scrolled, ▼ visible
local audio = menu:focusRow("musicVol")
U.wait(2)
U.shot(game, DIR .. "/options_1_musicvol.png") -- the AUDIO page
U.tap(game, "left"); U.wait(2)
U.tap(game, "left"); U.wait(2)
U.shot(game, DIR .. "/options_2_musicvol_5.png")
@@ -20,6 +23,7 @@ return function(game)
U.tap(game, "down"); U.wait(2)
U.tap(game, "right"); U.wait(2) -- MUSIC FILTER -> 1X
U.shot(game, DIR .. "/options_3_filter_1x.png")
U.tap(game, "down"); U.wait(2) -- CANCEL, tail rows behind it
U.shot(game, DIR .. "/options_4_cancel.png")
audio.index = #audio.view + 1 -- BACK, the page's rows behind it
U.wait(2)
U.shot(game, DIR .. "/options_4_back.png")
end
+17
View File
@@ -0,0 +1,17 @@
-- The COLORS picker after the palette rename: the root folder list and one
-- pack's contents.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
U.teleport(game, "PALLET_TOWN", 10, 8, "down")
local Screens = require("src.ui.Screens")
Screens.push(game, "PaletteScreen")
U.wait(5)
U.shot(game, DIR .. "/pal_0_root.png")
for _ = 1, 6 do U.tap(game, "down"); U.wait(1) end
U.shot(game, DIR .. "/pal_1_scrolled.png")
U.tap(game, "a"); U.wait(4)
U.shot(game, DIR .. "/pal_2_inside.png")
U.log("PALETTE_PICKER_SHOTS_DONE")
game.driverDone = true
end
@@ -0,0 +1,52 @@
-- BattleAnimView:fillBackground() used to draw a flat literal white
-- rectangle with no GbcPalette involvement, leaving a stark white gap
-- during the battle intro's slide-in under a custom COLORS ramp.
--
-- No real shader runs headless, so this asserts on which seam gets called
-- rather than on rendered pixels, same as every palette-seam test here.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = require("tests.love_stub")
require("src.core.Logger").warn = function() end
local GbcPalette = require("src.render.GbcPalette")
local BattleAnimView = require("src.ui.gen2.BattleAnimView")
local calls
local function spy(name)
local original = GbcPalette[name]
GbcPalette[name] = function(...)
calls[name] = (calls[name] or 0) + 1
return original(...)
end
end
do
GbcPalette.available = function() return true end
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
{ 0, 0, 0 } })
calls = {}
spy("resolve"); spy("use"); spy("with")
local view = BattleAnimView.new({}, {})
view:fillBackground()
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
"BattleAnimView:fillBackground() reaches the GbcPalette seam")
end
-- Unthemed / no-shader boot must still degrade to the plain literal white
-- fill, byte-identical to before this fix.
do
GbcPalette.available = function() return false end
calls = {}
spy("resolve"); spy("use"); spy("with")
local view = BattleAnimView.new({}, {})
local ok = pcall(function() view:fillBackground() end)
T.check(ok, "BattleAnimView:fillBackground() does not error with no shader")
T.eq((calls.resolve or 0) + (calls.use or 0) + (calls.with or 0), 0,
"BattleAnimView:fillBackground() does not touch GbcPalette with no shader")
end
T.finish()
@@ -0,0 +1,58 @@
-- BattleState:drawTextArea drew the FIGHT/ITEM/RUN command box with a flat
-- setColor, the same gap TextBox.lua had. Gen 1 gets colour from
-- PaletteFX's whole-frame remap, but Gen 2 has no such pass outside
-- CLASSIC mode, so the command box needed its own fix.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = require("tests.love_stub")
require("src.core.Logger").warn = function() end
local GbcPalette = require("src.render.GbcPalette")
local BattleState = require("src.battle.BattleState")
GbcPalette.available = function() return true end
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
{ 0, 0, 0 } })
local calls
local function spy(name)
local original = GbcPalette[name]
GbcPalette[name] = function(...)
calls[name] = (calls[name] or 0) + 1
return original(...)
end
end
local function fakeState()
return {
phase = "menu", menuIndex = 1, safari = nil, demo = false,
bottomUIVisible = function() return true end,
game = { save = { generation = 2, version = "gold" } },
}
end
do
calls = {}
spy("resolve"); spy("use"); spy("with")
BattleState.drawTextArea(fakeState())
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
"Gen 2 battle command box reaches the GbcPalette seam")
end
-- Gen 1 recolours through PaletteFX's whole-frame zone remap, not per draw
-- call, so this must stay untouched. Same guard gen2_textbox_palette_test
-- checks for TextBox.
do
calls = {}
spy("resolve"); spy("use"); spy("with")
local state = fakeState()
state.game = { save = { generation = 1, version = "red" } }
BattleState.drawTextArea(state)
T.eq((calls.resolve or 0) + (calls.use or 0) + (calls.with or 0), 0,
"Gen 1 battle command box is untouched, it never calls GbcPalette")
end
T.finish()
@@ -0,0 +1,69 @@
-- src/ui/gen2/BattleState.lua is the real Gen 2 battle screen (wired in
-- through Gen2Compat.lua's facade); it drew every label through flat
-- Chrome.print/cursor/printRight, never touching GbcPalette. Added
-- Chrome.printRightThrough alongside this fix since printRight had no
-- palette-aware counterpart.
--
-- No real shader runs headless, so this asserts on which seam gets called
-- rather than on rendered pixels. Exercises drawMoveInfoBox, drawStatsBox,
-- and drawPanel's no-battle fallback as representative cases.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = require("tests.love_stub")
require("src.core.Logger").warn = function() end
local GbcPalette = require("src.render.GbcPalette")
local BattleState = require("src.ui.gen2.BattleState")
GbcPalette.available = function() return true end
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
{ 0, 0, 0 } })
local calls
local function spy(name)
local original = GbcPalette[name]
GbcPalette[name] = function(...)
calls[name] = (calls[name] or 0) + 1
return original(...)
end
end
local function hits()
return (calls.resolve or 0) + (calls.use or 0) + (calls.with or 0)
end
do
calls = {}
spy("resolve"); spy("use"); spy("with")
BattleState.drawMoveInfoBox({}, { id = "TACKLE", pp = 10, maxPp = 35 })
T.check(hits() > 0, "drawMoveInfoBox's TYPE/PP labels reach the GbcPalette seam")
end
do
calls = {}
spy("resolve"); spy("use"); spy("with")
BattleState.drawMoveInfoBox({}, nil)
T.eq(hits(), 0, "drawMoveInfoBox with no move draws nothing")
end
do
local mon = { species = "RATTATA",
stats = { attack = 10, defense = 9, specialAttack = 8, specialDefense = 8,
speed = 12 } }
calls = {}
spy("resolve"); spy("use"); spy("with")
BattleState.drawStatsBox({}, mon)
T.check(hits() > 0, "drawStatsBox's stat names/numbers reach the GbcPalette seam")
end
do
calls = {}
spy("resolve"); spy("use"); spy("with")
BattleState.drawPanel({ battle = nil, tutorial = false })
T.check(hits() > 0, "drawPanel's NO BATTLE fallback reaches the GbcPalette seam")
end
T.finish()
@@ -0,0 +1,56 @@
-- Chrome.clear() is the base background wipe ~30 opaque Gen 2 menu screens
-- start their frame with. It used to draw a flat literal white rectangle,
-- never touching GbcPalette, so every screen's background stayed white
-- regardless of a picked COLORS palette even though its boxes and text
-- already recoloured correctly.
--
-- No real shader runs headless, so this can't check a rendered pixel, same
-- limitation gen2_textbox_palette_test notes: force GbcPalette.available()
-- true and assert on which seam gets called, not on pixels.
-- tests/drivers/gold_menu_shots.lua is where a real rendered screen gets
-- checked by eye.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = require("tests.love_stub")
require("src.core.Logger").warn = function() end
local GbcPalette = require("src.render.GbcPalette")
local Chrome = require("src.ui.gen2.Chrome")
local calls
local function spy(name)
local original = GbcPalette[name]
GbcPalette[name] = function(...)
calls[name] = (calls[name] or 0) + 1
return original(...)
end
end
do
GbcPalette.available = function() return true end
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
{ 0, 0, 0 } })
calls = {}
spy("resolve"); spy("use"); spy("with")
Chrome.clear()
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
"Chrome.clear() reaches the GbcPalette seam when a shader is available")
end
-- Unthemed / no-shader boot must still degrade to the plain literal white
-- wipe, byte-identical to before this fix, same guard Chrome.paletteBox
-- already takes.
do
GbcPalette.available = function() return false end
calls = {}
spy("resolve"); spy("use"); spy("with")
local ok = pcall(Chrome.clear)
T.check(ok, "Chrome.clear() does not error with no shader available")
T.eq((calls.resolve or 0) + (calls.use or 0) + (calls.with or 0), 0,
"Chrome.clear() does not touch GbcPalette when no shader is available")
end
T.finish()
@@ -0,0 +1,73 @@
-- Chrome.print / Chrome.cursor / Chrome.printRight are the flat,
-- hardcoded-black primitives ~40 Gen 2 screens still call directly, so any
-- glyph through them rendered solid black regardless of COLORS. Made the
-- primitives themselves palette-aware instead of migrating 40 files.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = require("tests.love_stub")
require("src.core.Logger").warn = function() end
local GbcPalette = require("src.render.GbcPalette")
local Chrome = require("src.ui.gen2.Chrome")
local calls
local function spy(name)
local original = GbcPalette[name]
GbcPalette[name] = function(...)
calls[name] = (calls[name] or 0) + 1
return original(...)
end
end
local function reset()
calls = {}
spy("resolve"); spy("use"); spy("useRaw"); spy("with")
end
local function touched()
return (calls.resolve or 0) + (calls.use or 0) + (calls.useRaw or 0) + (calls.with or 0)
end
do
GbcPalette.available = function() return true end
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
{ 0, 0, 0 } })
reset()
Chrome.print("HELLO", 1, 1)
T.check(touched() > 0, "Chrome.print() reaches the GbcPalette seam when a shader is available")
reset()
Chrome.cursor(1, 1)
T.check(touched() > 0, "Chrome.cursor() reaches the GbcPalette seam when a shader is available")
reset()
Chrome.printRight("42", 10, 1)
T.check(touched() > 0, "Chrome.printRight() reaches the GbcPalette seam when a shader is available")
end
-- Unthemed / no-shader boot must still degrade to the plain literal black
-- glyph draw, byte-identical to before this fix.
do
GbcPalette.available = function() return false end
reset()
local ok = pcall(Chrome.print, "HELLO", 1, 1)
T.check(ok, "Chrome.print() does not error with no shader available")
T.eq(touched(), 0, "Chrome.print() does not touch GbcPalette when no shader is available")
reset()
ok = pcall(Chrome.cursor, 1, 1)
T.check(ok, "Chrome.cursor() does not error with no shader available")
T.eq(touched(), 0, "Chrome.cursor() does not touch GbcPalette when no shader is available")
reset()
ok = pcall(Chrome.printRight, "42", 10, 1)
T.check(ok, "Chrome.printRight() does not error with no shader available")
T.eq(touched(), 0, "Chrome.printRight() does not touch GbcPalette when no shader is available")
end
T.finish()
@@ -0,0 +1,64 @@
-- Chrome.List:draw() backs nearly every Gen 2 menu's row text, but drew
-- each row through flat, hardcoded-black Chrome.print/cursor, so rows
-- stayed black-on-white even though the box behind them recoloured. Fixed
-- by giving List a `palette` option that routes rows through
-- printThrough/cursorThrough.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = require("tests.love_stub")
require("src.core.Logger").warn = function() end
local GbcPalette = require("src.render.GbcPalette")
local Chrome = require("src.ui.gen2.Chrome")
GbcPalette.available = function() return true end
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
{ 0, 0, 0 } })
local calls
local function spy(name)
local original = GbcPalette[name]
GbcPalette[name] = function(...)
calls[name] = (calls[name] or 0) + 1
return original(...)
end
end
do
local list = Chrome.List.new({ items = { "POKEMON", "PACK", "QUIT" }, x = 2, y = 2 })
calls = {}
spy("resolve"); spy("use"); spy("with")
list:draw()
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
"Chrome.List:draw() reaches the GbcPalette seam by default")
end
-- A caller that explicitly opts out (palette = false) keeps the old flat
-- black draw untouched by GbcPalette.
do
local list = Chrome.List.new({ items = { "A", "B" }, x = 2, y = 2, palette = false })
calls = {}
spy("resolve"); spy("use"); spy("with")
list:draw()
T.eq((calls.resolve or 0) + (calls.use or 0) + (calls.with or 0), 0,
"Chrome.List:draw() with palette = false does not touch GbcPalette")
end
-- The scroll-hint down arrow, drawn only when the list is longer than its
-- visible rows, must also go through the palette rather than a bare
-- Font.drawCode.
do
local items = {}
for i = 1, 5 do items[i] = "ITEM " .. i end
local list = Chrome.List.new({ items = items, x = 2, y = 2, rows = 2 })
calls = {}
spy("resolve"); spy("use"); spy("with")
list:draw()
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
"Chrome.List:draw()'s scroll-hint arrow reaches the GbcPalette seam")
end
T.finish()
@@ -0,0 +1,109 @@
-- World's per-map colour bake keyed its cache on GbcPalette.mode alone, so
-- two different picked packs (both mode == "custom") kept showing
-- whichever was baked first until a full reload.
-- luajit tests/engine/gen2_color_cache_invalidation_test.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
if not _G.love then _G.love = require("tests.love_stub") end
local T = require("tests.harness")
local check, eq = T.check, T.eq
local GbcPalette = require("src.render.GbcPalette")
local BorderFill = require("src.world.gen2.BorderFill")
local World = require("src.world.gen2.World")
GbcPalette.setMode("gbc")
GbcPalette.setCustomRamp(nil)
-- ------- mapCacheKey: must depend on customRamp identity, not just mode
do
local fakeSelf = { daytime = "DAY", flickerPhase = 1 }
local key1 = World.mapCacheKey(fakeSelf, "map1")
GbcPalette.setCustomRamp(
{ { 1, 1, 1 }, { 2, 2, 2 }, { 3, 3, 3 }, { 4, 4, 4 } })
local key2 = World.mapCacheKey(fakeSelf, "map1")
check(key1 ~= key2,
"BUG FIX: mapCacheKey differs once a custom ramp is active, even " ..
"though GbcPalette.mode ('gbc') never changed")
GbcPalette.setCustomRamp(
{ { 9, 9, 9 }, { 8, 8, 8 }, { 7, 7, 7 }, { 6, 6, 6 } })
local key3 = World.mapCacheKey(fakeSelf, "map1")
check(key2 ~= key3,
"BUG FIX: mapCacheKey differs again for a second, different ramp " ..
"under the same mode")
GbcPalette.setCustomRamp(nil)
eq(World.mapCacheKey(fakeSelf, "map1"), key1,
"clearing the ramp returns to the original no-ramp key")
end
-- ------- refreshColorMode: must refetch on a customRamp-only change
do
local fakeSelf = { daytime = "DAY", flickerPhase = 1 }
local calls = 0
fakeSelf.imageFor = function(_, id) calls = calls + 1; return "img:" .. id end
fakeSelf.rebuildNeighbors = function() end
fakeSelf.map = { id = "map1" }
World.refreshColorMode(fakeSelf)
eq(calls, 1, "first refreshColorMode call bakes/fetches once")
World.refreshColorMode(fakeSelf)
eq(calls, 1, "a second call with nothing changed does not refetch")
GbcPalette.setCustomRamp(
{ { 5, 5, 5 }, { 4, 4, 4 }, { 3, 3, 3 }, { 2, 2, 2 } })
World.refreshColorMode(fakeSelf)
eq(calls, 2,
"BUG FIX: a customRamp change alone (mode unchanged) still refetches")
GbcPalette.setCustomRamp(nil)
World.refreshColorMode(fakeSelf)
eq(calls, 3, "clearing the ramp (mode still unchanged) refetches too")
end
-- ------- World:borderImageFor: the void/border bake had the identical gap
-- in its own separately-inlined cache key (it doesn't call mapCacheKey). A
-- border baked while GEN 2 (no ramp) was active stayed cached forever,
-- since picking a pack never showed up in this key, only the mode did.
do
local def = { id = "map1", tileset = "TILESET_JOHTO", borderBlock = 0 }
local fakeSelf = {
daytime = "DAY", flickerPhase = 1,
maps = { map1 = def },
tilesets = { TILESET_JOHTO = { blocks = { [1] = {} }, tilesPerRow = 16 } },
mapImages = {},
borderWaterFrame = function() return nil end,
atlasFor = function() return nil end,
}
GbcPalette.setCustomRamp(nil)
-- Seed the cache under exactly the key borderImageFor computes today for
-- "no ramp active", with a sentinel, proving the fix without touching
-- the real love.graphics bake pipeline.
local keyNoRamp =
BorderFill.cacheKey("map1|DAY|gbc|nil|1|fade|0|0")
fakeSelf.mapImages[keyNoRamp] = "SENTINEL-no-ramp"
eq(World.borderImageFor(fakeSelf, "map1"), "SENTINEL-no-ramp",
"sanity: the no-ramp key matches what borderImageFor builds today")
GbcPalette.setCustomRamp(
{ { 1, 1, 1 }, { 2, 2, 2 }, { 3, 3, 3 }, { 4, 4, 4 } })
local got = World.borderImageFor(fakeSelf, "map1")
check(got ~= "SENTINEL-no-ramp",
"BUG FIX: borderImageFor must not reuse the no-ramp cached void " ..
"bake once a custom ramp is active")
GbcPalette.setCustomRamp(nil)
end
GbcPalette.setCustomRamp(nil)
GbcPalette.setMode("gbc")
T.finish("gen2 color cache invalidation")
@@ -54,13 +54,41 @@ local function rowIndex(rows, id)
end
end
-- TEXT SPEED and the other cart rows live on a group page now, so the
-- screen a row draws on is the page its group opens, not the top level.
local function pageFor(menu, id)
for _, row in ipairs(menu.view) do
if row.group then
row.activate(menu.game)
local page = menu.game.stack:top()
if rowIndex(page.view, id) then return page end
menu.game.stack:pop()
end
end
end
local function fakeGame()
local stack = { items = {} }
function stack:push(s) self.items[#self.items + 1] = s; return s end
function stack:pop() self.items[#self.items] = nil end
function stack:top() return self.items[#self.items] end
return { stack = stack }
end
-- ---------------------------------------------- vanilla: no mod catalog
do
local menu = OptionsMenu.new({})
local menu = OptionsMenu.new(fakeGame())
drawn = {}
menu:drawPanel()
T.eq(drawnAt(LABEL_X, 2 * 8), "SPEED",
"row 1 is the SPEED group with no mod loaded")
local page = pageFor(menu, "textSpeed")
T.check(page ~= nil, "TEXT SPEED is on a group page")
drawn = {}
page:drawPanel()
T.eq(drawnAt(LABEL_X, 2 * 8), "TEXT SPEED",
"row 1's label draws in English with no mod loaded")
"the page's first label draws in English")
-- Save.DEFAULT_OPTIONS.textSpeed is "MID", the cart's own default.
T.eq(drawnAt(VALUE_X, 3 * 8), "MID ",
"and its cart-original display value too")
@@ -73,13 +101,20 @@ do
["TEXT SPEED"] = "VITESSE TEXTE",
["MID "] = "MOY ",
["CONTROLS"] = "COMMANDES",
["CANCEL"] = "ANNULER",
["SPEED"] = "VITESSE",
["BACK"] = "RETOUR",
},
})
local menu = OptionsMenu.new({})
local menu = OptionsMenu.new(fakeGame())
drawn = {}
menu:drawPanel()
T.eq(drawnAt(LABEL_X, 2 * 8), "VITESSE",
"a mod catalog reaches a group opener's label too")
local page = pageFor(menu, "textSpeed")
drawn = {}
page:drawPanel()
T.eq(drawnAt(LABEL_X, 2 * 8), "VITESSE TEXTE",
"a mod catalog reaches a cart-original row's label")
T.eq(drawnAt(VALUE_X, 3 * 8), "MOY ",
@@ -87,7 +122,7 @@ do
-- CONTROLS is the first port-added row; scroll to it so it lands in the
-- VISIBLE_ROWS=7 window drawPanel actually draws.
local index = rowIndex(menu.rows, "controls")
local index = rowIndex(menu.view, "controls")
T.check(index ~= nil, "CONTROLS is one of the rows")
menu.index = index
menu:ensureVisible()
@@ -97,17 +132,17 @@ do
T.eq(drawnAt(LABEL_X, (2 + (slot - 1) * 2) * 8), "COMMANDES",
"and a port-added row's label is translated too")
-- CANCEL is the last row, built into ROWS like any other -- there is no
-- BACK is the last row, built into ROWS like any other -- there is no
-- separate hook to fall through if this one row is missed.
local cancelMenu = OptionsMenu.new({})
local cancelIndex = #cancelMenu.rows
T.check(cancelMenu.rows[cancelIndex].cancel, "the last row is CANCEL")
local cancelMenu = OptionsMenu.new(fakeGame())
local cancelIndex = #cancelMenu.view
T.check(cancelMenu.view[cancelIndex].cancel, "the last row is BACK")
cancelMenu.index = cancelIndex
cancelMenu:ensureVisible()
drawn = {}
cancelMenu:drawPanel()
local cancelSlot = cancelIndex - cancelMenu.scroll
T.eq(drawnAt(LABEL_X, (2 + (cancelSlot - 1) * 2) * 8), "ANNULER",
T.eq(drawnAt(LABEL_X, (2 + (cancelSlot - 1) * 2) * 8), "RETOUR",
"CANCEL, the way out of the menu, is translated too")
-- Module state is process-global (see tests/gen2_clock_test.lua's own
@@ -0,0 +1,65 @@
-- PackMenu:drawList and PackGfx:draw stayed flat white/black regardless of
-- COLORS, since PackGfx only repaints its left five columns and the
-- item-list columns kept their initial bare-rectangle fill. Fixed by
-- routing that fill through Chrome.paletteFill and PackMenu's prints
-- through printThrough/cursorThrough.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = require("tests.love_stub")
require("src.core.Logger").warn = function() end
local GbcPalette = require("src.render.GbcPalette")
local PackGfx = require("src.ui.gen2.PackGfx")
local PackMenu = require("src.ui.gen2.PackMenu")
GbcPalette.available = function() return true end
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
{ 0, 0, 0 } })
local calls
local function spy(name)
local original = GbcPalette[name]
GbcPalette[name] = function(...)
calls[name] = (calls[name] or 0) + 1
return original(...)
end
end
do
-- An empty gfx.pack table (no header/background/pack-image paths): draw()
-- still runs its header/pattern loops, they just find nothing to blit, so
-- only the base fill itself is exercised.
local gfx = PackGfx.new({ pack = {} })
calls = {}
spy("resolve"); spy("use"); spy("with")
gfx:draw("ITEMS")
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
"PackGfx:draw()'s base fill reaches the GbcPalette seam")
end
local function fakePackMenu()
return {
rows = {
{ id = "POTION", name = "POTION", showCount = true, count = 1 },
},
scroll = 0,
index = 1,
switching = nil,
gfx = PackGfx.new(nil),
cursorAt = PackMenu.cursorAt,
total = function() return 2 end, -- 1 row + CANCEL
}
end
do
calls = {}
spy("resolve"); spy("use"); spy("with")
PackMenu.drawList(fakePackMenu(), 5, 4)
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
"PackMenu:drawList() reaches the GbcPalette seam for item labels")
end
T.finish()
+275
View File
@@ -0,0 +1,275 @@
-- Gen 2's answer to tests/engine/palette_test.lua: the same COLORS picker,
-- wired onto Gold's COLOR row and its own substitution seam
-- (src/render/GbcPalette.lua) instead of Gen 1's PaletteFX.
-- luajit tests/engine/gen2_palette_picker_test.lua
--
-- Covers GbcPalette.customRamp/setCustomRamp (the seam every Gen 2 draw call
-- shares), that a stored palette beats CLASSIC's present pass the same way
-- it beats OG/OG INV/CLASSIC on Gen 1, and that the COLOR row opens the
-- picker while GBC/DMG/CLASSIC stay reachable inside it.
package.path = "./?.lua;./?/init.lua;" .. package.path
if not _G.love then _G.love = require("tests.love_stub") end
local T = require("tests.harness")
local check, eq = T.check, T.eq
local function hex(c)
return type(c) == "table" and ("%02x%02x%02x"):format(c[1], c[2], c[3]) or "?"
end
local Palette = require("src.render.Palette")
local GbcPalette = require("src.render.GbcPalette")
-- ------- GbcPalette seam: a custom ramp wins outright
do
GbcPalette.setCustomRamp(nil)
GbcPalette.setMode("gbc")
local cartColors = { { 9, 9, 9 }, { 8, 8, 8 }, { 7, 7, 7 }, { 6, 6, 6 } }
eq(GbcPalette.resolve(cartColors), cartColors,
"no custom ramp: GBC mode passes the cart's own colours through")
local id = Palette.packId("GalleryWebApp Palettes", "Original GB")
local ramp = Palette.ramp(id)
GbcPalette.setCustomRamp(ramp)
local out = GbcPalette.resolve(cartColors)
for i = 1, 4 do
eq(hex(out[i]), hex(ramp[i]),
"resolve[" .. i .. "] is the custom ramp, not the cart's own colours")
end
-- it should win even over DMG, since DMG's whole effect lives at this seam
GbcPalette.setMode("dmg")
local out2 = GbcPalette.resolve(cartColors)
eq(hex(out2[1]), hex(ramp[1]), "a custom ramp beats DMG too")
GbcPalette.setMode("gbc")
GbcPalette.setCustomRamp(nil)
eq(GbcPalette.resolve(cartColors), cartColors,
"clearing the ramp restores the cart's own colours")
end
-- ------- CLASSIC is a present pass, same as Gen 1: it has to be dropped
do
GbcPalette.setCustomRamp(nil)
GbcPalette.setMode("classic")
check(GbcPalette.presentColors() ~= nil,
"CLASSIC has a present pass before any palette is picked")
local id = Palette.packId("BGB", "Red")
local ramp = Palette.ramp(id)
GbcPalette.setCustomRamp(ramp)
-- resolve() alone can't save CLASSIC: presentColors bypasses resolve
-- entirely, so the picker's opts have to drop the mode itself. Covered
-- below via the real row wiring.
GbcPalette.setMode("gbc")
GbcPalette.setCustomRamp(nil)
end
-- ------- applyOptions: a saved palette re-applies on load, like Gen 1's
do
GbcPalette.setCustomRamp(nil)
local id = Palette.packId("BGB", "Blue")
GbcPalette.applyOptions({ color = "dmg", palette = id })
eq(GbcPalette.mode, "dmg", "applyOptions still sets the mode")
check(GbcPalette.customRamp ~= nil, "applyOptions applied the saved palette")
eq(hex(GbcPalette.customRamp[1]), hex(Palette.ramp(id)[1]),
"and it's the right ramp")
GbcPalette.applyOptions({ color = "gbc", palette = "" })
check(GbcPalette.customRamp == nil,
"applyOptions with no palette clears the custom ramp")
end
-- ------- the COLOR row's real wiring: activate opens the picker
local OptionsMenu = require("src.ui.gen2.OptionsMenu")
local function rowNamed(rows, label)
for _, row in ipairs(rows) do
if row.label == label then return row end
end
return nil
end
do
local colorRow = rowNamed(OptionsMenu.ROWS, "COLOR")
check(colorRow ~= nil, "COLOR is still a row")
check(colorRow.cycle == nil, "COLOR no longer cycles in place")
check(type(colorRow.activate) == "function",
"COLOR opens the picker instead")
GbcPalette.setCustomRamp(nil)
GbcPalette.setMode("classic")
local pushed
local game = {
options = { color = "classic", palette = "" },
stack = { states = {},
push = function(self, inst) pushed = inst end },
persistOptions = function() end,
}
colorRow.activate(game)
check(pushed ~= nil, "activate pushed a screen")
check(pushed.view == "root", "the pushed screen is a real PaletteScreen")
eq(game.options.color, "gbc",
"opening the picker while CLASSIC was active dropped it to GBC")
eq(GbcPalette.mode, "gbc", "and the live mode followed")
-- the root offers the engine's own GBC/DMG/CLASSIC ladder alongside the
-- 699-pack folders and MONOCHROME: "the original options" still work
local sawModes = {}
for _, row in ipairs(pushed.rows) do
if row.mode then sawModes[row.mode] = true end
end
check(sawModes.gbc and sawModes.dmg and sawModes.classic,
"GBC, DMG and CLASSIC are all still reachable from the picker")
local id = Palette.packId("BGB", "Blue")
pushed.set(id)
eq(game.options.palette, id, "storing a palette writes options.palette")
check(GbcPalette.customRamp ~= nil, "and GbcPalette.setCustomRamp actually ran")
eq(hex(GbcPalette.customRamp[1]), hex(Palette.ramp(id)[1]), "with the right ramp")
pushed.set("")
check(GbcPalette.customRamp == nil, "storing empty clears the custom ramp")
eq(game.options.color, "gbc",
"clearing a palette lands back on GEN 2, not whatever mode was active")
eq(GbcPalette.mode, "gbc", "and the live mode followed")
-- picking an engine mode through the picker still works and clears the
-- stored palette, exactly like Gen 1's picker
pushed.set(id)
pushed.setMode("dmg")
eq(game.options.color, "dmg", "setMode still writes options.color")
eq(GbcPalette.mode, "dmg", "and drives the live module")
GbcPalette.setCustomRamp(nil)
GbcPalette.setMode("gbc")
end
-- ------- GBC/GEN 2 relabel: GbcPalette.MODES keeps its id "gbc", but the
-- label the player sees for it moved to "GEN 2", freeing "GBC" up as
-- CUSTOM_MODE's own label, the state a stored palette actually lands on.
do
eq(GbcPalette.MODE_LABELS.gbc, "GEN 2",
"GEN 2 is the engine's own unique-colour mode's display name now")
eq(GbcPalette.MODE_LABELS[GbcPalette.CUSTOM_MODE], "GBC",
"GBC is the label for a stored custom palette's state")
check(not (function()
for _, id in ipairs(GbcPalette.MODES) do
if id == GbcPalette.CUSTOM_MODE then return true end
end
return false
end)(), "CUSTOM_MODE is deliberately not one of the root ladder's own modes")
end
-- ------- the bug this session's relabel fixes: picking a palette from a
-- non-GEN-2 mode used to strand the player there once the palette cleared,
-- because only CLASSIC ever got dropped back. Now ANY mode lands on
-- CUSTOM_MODE the moment a palette is picked, so clearing it always finds
-- its way back to GEN 2's own unique colours.
do
local OptionsMenu = require("src.ui.gen2.OptionsMenu")
local colorRow = nil
for _, r in ipairs(OptionsMenu.ROWS) do
if r.key == "color" then colorRow = r end
end
check(colorRow ~= nil, "found the COLOR row")
GbcPalette.setCustomRamp(nil)
GbcPalette.setMode("dmg")
local pushed
local game = {
options = { color = "dmg", palette = "" },
stack = { states = {}, push = function(self, inst) pushed = inst end },
persistOptions = function() end,
}
colorRow.activate(game)
-- opening the picker from DMG (not CLASSIC) doesn't touch the mode yet:
-- dropClassic only fires for CLASSIC, exactly as before.
eq(game.options.color, "dmg", "opening the picker from DMG leaves it alone")
local id = Palette.packId("BGB", "Blue")
pushed.set(id)
eq(game.options.color, GbcPalette.CUSTOM_MODE,
"picking a palette from DMG now lands on CUSTOM_MODE, not DMG")
eq(GbcPalette.mode, GbcPalette.CUSTOM_MODE, "and the live mode followed")
pushed.set("")
eq(game.options.color, "gbc",
"clearing it lands on GEN 2 (the bug: this used to stay on DMG, " ..
"with no selectable row left to get back to GEN 2's unique colours)")
eq(GbcPalette.mode, "gbc", "and the live mode followed")
GbcPalette.setCustomRamp(nil)
GbcPalette.setMode("gbc")
end
-- ------- TitleState's two baked art sets: GEN 2 and a picked custom
-- palette both want the colour set (there's no live recolour of this
-- screen's art either way); only DMG/CLASSIC want the grey set.
do
local TitleState = require("src.ui.gen2.TitleState")
GbcPalette.setMode("gbc")
check(not TitleState.gray(), "GEN 2 shows the colour art")
GbcPalette.setMode(GbcPalette.CUSTOM_MODE)
check(not TitleState.gray(), "a picked custom palette also shows the colour art")
GbcPalette.setMode("dmg")
check(TitleState.gray(), "DMG shows the grey art")
GbcPalette.setMode("classic")
check(TitleState.gray(), "CLASSIC shows the grey art (then the green present pass)")
GbcPalette.setMode("gbc")
end
-- ------- the "2" hotkey ladder (Game2:hotkey) and GbcPalette.cycle(): the
-- bug this session fixes. Unlike the picker's own row.mode branch (tested
-- above), this ladder used to change GbcPalette.mode/options.color without
-- touching customRamp or options.palette, so resolve() stayed locked onto
-- the old pack's colours no matter how many times "2" landed back on "gbc".
do
local Game2 = require("src.core.Game2")
local cartColors = { { 9, 9, 9 }, { 8, 8, 8 }, { 7, 7, 7 }, { 6, 6, 6 } }
GbcPalette.setCustomRamp(nil)
GbcPalette.setMode("gbc")
local id = Palette.packId("BGB", "Blue")
local ramp = Palette.ramp(id)
local game = {
options = { color = GbcPalette.CUSTOM_MODE, palette = id },
persistOptions = function() end,
}
GbcPalette.setMode(GbcPalette.CUSTOM_MODE)
GbcPalette.setCustomRamp(ramp)
-- cycle() isn't in GbcPalette.MODES so a custom-mode start is treated as
-- index 1: three "2" presses walk dmg -> classic -> gbc.
for _ = 1, 3 do Game2.hotkey(game, "2") end
eq(game.options.color, "gbc", "three presses land back on GEN 2's label")
eq(game.options.palette, "",
"BUG FIX: the hotkey ladder clears options.palette too, not just color")
check(GbcPalette.customRamp == nil,
"BUG FIX: GbcPalette.cycle() drops the live custom ramp")
eq(GbcPalette.resolve(cartColors), cartColors,
"BUG FIX: resolve() answers with the cart's own colours again, not " ..
"the stale custom ramp")
GbcPalette.setCustomRamp(nil)
GbcPalette.setMode("gbc")
end
T.finish("gen2 palette picker")
@@ -0,0 +1,83 @@
-- Follow-up to gen2_pokegear_native_colors_test.lua's "keep the gear
-- vanilla" fix: CLASSIC swept the native-coloured gear for free, so this
-- extends the same whole-panel tint to a picked custom COLORS ramp, with
-- Pokegear.CUSTOM_RAMP_FILM = false as an easy revert.
--
-- No real shader/asset loads headless, so this isolates drawPanel()'s new
-- branch with a minimal fake self, same "stub the rest, assert the seam"
-- approach as the TrainerCard drawPanel() test.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = require("tests.love_stub")
require("src.core.Logger").warn = function() end
local GbcPalette = require("src.render.GbcPalette")
local Pokegear = require("src.ui.gen2.Pokegear")
GbcPalette.available = function() return true end
local calls
local function spy(name)
local original = GbcPalette[name]
GbcPalette[name] = function(...)
calls[name] = (calls[name] or 0) + 1
return original(...)
end
end
local function hits()
return (calls.resolve or 0) + (calls.use or 0) + (calls.useRaw or 0)
+ (calls.with or 0)
end
local function fakeGear()
return {
styled = function() return true end,
groundColor = function() return { 0, 0, 0 } end,
card = function() return { id = "clock" } end,
drawClock = function() end,
drawMap = function() end,
drawRadio = function() end,
drawPhone = function() end,
drawModeArrow = function() end,
}
end
do
Pokegear.CUSTOM_RAMP_FILM = true
GbcPalette.setCustomRamp({ { 227, 249, 227 }, { 180, 200, 180 },
{ 90, 100, 90 }, { 0, 0, 0 } })
calls = {}
spy("resolve"); spy("use"); spy("useRaw"); spy("with")
Pokegear.drawPanel(fakeGear())
T.check(hits() > 0,
"with the film on and a custom ramp active, drawPanel() reaches GbcPalette")
end
do
Pokegear.CUSTOM_RAMP_FILM = false
GbcPalette.setCustomRamp({ { 227, 249, 227 }, { 180, 200, 180 },
{ 90, 100, 90 }, { 0, 0, 0 } })
calls = {}
spy("resolve"); spy("use"); spy("useRaw"); spy("with")
Pokegear.drawPanel(fakeGear())
T.eq(hits(), 0,
"Pokegear.CUSTOM_RAMP_FILM = false is a one-line revert to fully native")
end
do
Pokegear.CUSTOM_RAMP_FILM = true
GbcPalette.setCustomRamp(nil)
calls = {}
spy("resolve"); spy("use"); spy("useRaw"); spy("with")
Pokegear.drawPanel(fakeGear())
T.eq(hits(), 0,
"with no custom ramp picked, the film never engages -- native GEN 2 stays untouched")
end
Pokegear.CUSTOM_RAMP_FILM = true
T.finish()
@@ -0,0 +1,66 @@
-- The Pokegear's six hardware palette banks guarantee a highlighted cell
-- contrasts with its text, a guarantee GbcPalette.resolve() breaks by
-- collapsing every bank onto the same 4 shades. Product decision: the
-- Pokegear stays on native hardware colours always, via a `raw` path
-- (GbcPalette.withRaw, Chrome.*Through's `raw` param, TileSheet's `raw`
-- option) that calls useRaw directly and skips resolve.
--
-- No real shader runs headless, so this asserts on which entry point gets
-- called rather than on rendered pixels.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = require("tests.love_stub")
require("src.core.Logger").warn = function() end
local GbcPalette = require("src.render.GbcPalette")
local Chrome = require("src.ui.gen2.Chrome")
local TileSheet = require("src.ui.gen2.TileSheet")
local Pokegear = require("src.ui.gen2.Pokegear")
GbcPalette.available = function() return true end
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
{ 0, 0, 0 } })
local calls
local function spy(name)
local original = GbcPalette[name]
GbcPalette[name] = function(...)
calls[name] = (calls[name] or 0) + 1
return original(...)
end
end
local BANK_1 = { { 227, 249, 227 }, { 180, 200, 180 }, { 90, 100, 90 },
{ 0, 0, 0 } }
do
calls = {}
spy("resolve"); spy("useRaw")
Pokegear.text({ pals = function() return { BANK_1 } end }, "SUNDAY", 6, 6)
T.eq(calls.resolve or 0, 0,
"Pokegear:text() never substitutes the active COLORS ramp")
T.check((calls.useRaw or 0) > 0,
"Pokegear:text() still shades its own native colours through the shader")
end
do
local sheet = TileSheet.new({ palette = BANK_1, raw = true })
-- No real image asset in the headless harness, so this exercises the
-- option's plumbing (raw stored, read by :draw) rather than a full
-- render; gen2_trainer_card_text_palette_test.lua and friends take the
-- same "assert the seam, not the pixel" approach one level up.
T.check(sheet.raw == true, "TileSheet:new stores the raw option")
end
-- A sheet built without raw must still go through the normal substituting
-- seam, guarding the new option from silently becoming the default for
-- every other Gen 2 screen's tile art.
do
local sheet = TileSheet.new({ palette = BANK_1 })
T.eq(sheet.raw, nil, "TileSheet:new defaults raw to unset")
end
T.finish()
@@ -0,0 +1,66 @@
-- StartMenu:draw drew the OPTION row's description tooltip, the "Change
-- settings" box under the list, with a raw love.graphics.rectangle at a
-- flat literal white, never touching GbcPalette. Every other box on this
-- screen already recoloured correctly with a picked COLORS palette, so
-- this one sub-region stood out as structurally white/black regardless.
--
-- No real shader runs headless, so this can't check a rendered pixel, same
-- limitation every palette test in this directory notes: force
-- GbcPalette.available() true and assert on which seam gets called, not
-- on pixels. tests/drivers/custom_ramp_probe.lua is where a real rendered
-- screen gets checked by eye.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = require("tests.love_stub")
require("src.core.Logger").warn = function() end
local GbcPalette = require("src.render.GbcPalette")
local StartMenu = require("src.ui.gen2.StartMenu")
local calls
local function spy(name)
local original = GbcPalette[name]
GbcPalette[name] = function(...)
calls[name] = (calls[name] or 0) + 1
return original(...)
end
end
-- Minimal fake: draw() only needs #self.items (box height), a list stub,
-- and a current item with a desc, the same shape start_menu.asm carries
-- for every row (see StartMenu.lua's ITEMS table).
local function fakeMenu()
return {
items = { { id = "option" } },
list = {
draw = function() end,
current = function() return { desc = { "Change", "settings" } } end,
},
phase = nil,
showDescription = true,
}
end
do
GbcPalette.available = function() return true end
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
{ 0, 0, 0 } })
calls = {}
spy("resolve"); spy("use"); spy("with")
StartMenu.draw(fakeMenu())
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
"StartMenu's description tooltip reaches the GbcPalette seam")
end
do
GbcPalette.available = function() return false end
calls = {}
spy("resolve"); spy("use"); spy("with")
local ok = pcall(StartMenu.draw, fakeMenu())
T.check(ok, "StartMenu:draw() does not error with no shader available")
end
T.finish()
@@ -0,0 +1,63 @@
-- TextBox drew its box border and glyphs through Font.drawBox/drawCode
-- with a flat setColor, never touching GbcPalette, so dialogue text never
-- picked up a COLORS palette even though every other Chrome.lua-based
-- screen did.
--
-- No real shader runs headless, so this asserts on which seam gets called
-- rather than checking a rendered pixel; tests/drivers/gold_menu_shots.lua
-- is where a real box gets checked by eye.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = require("tests.love_stub")
require("src.core.Logger").warn = function() end
local GbcPalette = require("src.render.GbcPalette")
local TextBox = require("src.render.TextBox")
GbcPalette.available = function() return true end
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
{ 0, 0, 0 } })
local calls
local function spy(name)
local original = GbcPalette[name]
GbcPalette[name] = function(...)
calls[name] = (calls[name] or 0) + 1
return original(...)
end
end
local function newGame(generation)
return {
save = { player = { name = "RED" }, generation = generation,
version = generation == 2 and "gold" or "red", options = {} },
stringBuffer = "", boxNumString = "", boxMonNicks = {},
data = {},
}
end
do
calls = {}
spy("resolve"); spy("use"); spy("with")
local box = TextBox.new(newGame(2), "Hello world!", nil, {})
box:draw()
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
"Gen 2 dialogue box reaches the GbcPalette seam")
end
-- Gen 1 recolours through a whole-frame zone remap upstream of this file
-- (PaletteFX), not per draw call, so TextBox must not start touching
-- GbcPalette for a Gen 1 game.
do
calls = {}
spy("resolve"); spy("use"); spy("with")
local box = TextBox.new(newGame(1), "Hello world!", nil, {})
box:draw()
T.eq((calls.resolve or 0) + (calls.use or 0) + (calls.with or 0), 0,
"Gen 1 dialogue box is untouched, it never calls GbcPalette")
end
T.finish()
@@ -0,0 +1,69 @@
-- TrainerCard had two palette gaps: its own text drew through flat
-- Chrome.print/cursor, fixed with a print/:cursor pair routed through
-- printThrough/cursorThrough via self:colorsAt; and drawPanel() painted
-- the screen with a flat white rectangle before anything else, leaving the
-- NAME/ID/MONEY/STATUS/BADGES interiors white, fixed by routing that fill
-- through Chrome.paletteFill.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = require("tests.love_stub")
require("src.core.Logger").warn = function() end
local GbcPalette = require("src.render.GbcPalette")
local TrainerCard = require("src.ui.gen2.TrainerCard")
GbcPalette.available = function() return true end
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
{ 0, 0, 0 } })
local calls
local function spy(name)
local original = GbcPalette[name]
GbcPalette[name] = function(...)
calls[name] = (calls[name] or 0) + 1
return original(...)
end
end
local function fakeCard()
return {
colorsAt = function() return { { 255, 255, 255 }, { 200, 0, 200 },
{ 100, 0, 100 }, { 0, 0, 0 } } end,
}
end
do
calls = {}
spy("resolve"); spy("use"); spy("with")
TrainerCard.print(fakeCard(), "NAME/", 2, 2)
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
"TrainerCard:print() reaches the GbcPalette seam via self:colorsAt")
end
do
calls = {}
spy("resolve"); spy("use"); spy("with")
TrainerCard.cursor(fakeCard(), 18, 15)
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
"TrainerCard:cursor() reaches the GbcPalette seam via self:colorsAt")
end
-- drawPanel() painted the whole screen with a flat white rectangle before
-- drawing the card, and self:frame only draws border tiles, so panel
-- interiors stayed white regardless of the picked palette. drawCard is
-- stubbed here to isolate that base fill from the print/cursor seam
-- covered above.
do
local fake = { styled = function() return true end, page = 1,
drawCard = function() end }
calls = {}
spy("resolve"); spy("use"); spy("with")
TrainerCard.drawPanel(fake)
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
"TrainerCard:drawPanel()'s base fill reaches the GbcPalette seam")
end
T.finish()
@@ -0,0 +1,46 @@
-- UnownWords:draw already routed its tile art through GbcPalette.with, but
-- drew the frame around it with a plain Font.drawBox, so the puzzle's own
-- glyphs picked up a selected COLORS palette while the box border stayed
-- white/black, the same shape of gap gen2_textbox_palette_test.lua found
-- in TextBox.lua. Fixed by routing the box through Chrome.paletteBox.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
love = require("tests.love_stub")
require("src.core.Logger").warn = function() end
local GbcPalette = require("src.render.GbcPalette")
local UnownWords = require("src.world.gen2.UnownWords")
GbcPalette.available = function() return true end
GbcPalette.setCustomRamp({ { 255, 0, 255 }, { 200, 0, 200 }, { 100, 0, 100 },
{ 0, 0, 0 } })
local calls
local function spy(name)
local original = GbcPalette[name]
GbcPalette[name] = function(...)
calls[name] = (calls[name] or 0) + 1
return original(...)
end
end
-- data/events/unown_walls.asm:15 MenuHeaders_UnownWalls's ESCAPE row, the
-- same fixture tests/gen2_unown_words_test.lua uses for boxRect.
local ESCAPE_WALL = { x1 = 3, y1 = 4, x2 = 16, y2 = 9,
chars = { 0x08, 0x44, 0x04, 0x00, 0x2e, 0x08 } }
do
calls = {}
spy("resolve"); spy("use"); spy("with")
-- no opts.world -> :tileset() returns nil -> draw() exits right after the
-- box, isolating the box's own palette seam from the tile-art body wrap
local words = UnownWords.new({}, { wall = ESCAPE_WALL })
words:draw()
T.check((calls.resolve or 0) > 0 or (calls.use or 0) > 0 or (calls.with or 0) > 0,
"Unown wall box reaches the GbcPalette seam")
end
T.finish()
@@ -0,0 +1,152 @@
-- The OPTION screen's grouped pages and its scrolling text.
--
-- The flat row list outgrew the four-box viewport, so the battle, audio,
-- video and speed rows collapse into one opener each. Grouping happens after
-- the ui.options.rows hook and only builds self.view, so self.rows stays the
-- flat list a mod reads and edits.
--
-- Text longer than its line used to run off the 160px panel with no clipping
-- (COLORS showed "1-A (Default) (B"); the highlighted row now scrolls instead.
-- luajit tests/engine/options_submenu_marquee_test.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local OptionsMenu = require("src.ui.OptionsMenu")
local Marquee = require("src.ui.Marquee")
local function stubGame()
local stack = { items = {} }
function stack:push(s) self.items[#self.items + 1] = s; return s end
function stack:pop() self.items[#self.items] = nil end
function stack:top() return self.items[#self.items] end
return {
data = { rulesets = { gen1_faithful = { name = "GEN 1" } }, constants = {} },
save = { options = {} },
stack = stack,
modStatus = { available = {} },
}
end
local function ids(rows)
local out = {}
for i, row in ipairs(rows) do out[i] = row.id end
return table.concat(out, ",")
end
local function find(rows, id)
for i, row in ipairs(rows) do if row.id == id then return row, i end end
end
-- ------------------------------------------------------------- grouping
local game = stubGame()
local menu = OptionsMenu.new(game)
T.check(find(menu.rows, "battleBg"), "self.rows keeps the flat list for mods")
T.check(find(menu.rows, "musicVol"), "self.rows keeps the audio rows too")
for _, id in ipairs({ "battleBg", "speedBattle", "uiLayout", "tilt", "zoom",
"voidFill", "colors", "shaderfx", "shaderfx2",
"musicVol", "videoMode" }) do
T.check(not find(menu.view, id), id .. " moved onto a page")
end
for _, spec in ipairs({
{ "group.battle", 6 }, { "group.audio", 3 }, { "group.video", 5 },
{ "group.speed", 4 }, { "group.graphics", 4 }, { "group.extras", 3 },
}) do
local row = find(menu.view, spec[1])
T.check(row, spec[1] .. " has an opener row")
T.eq(row.group, true, spec[1] .. " is marked as a group")
end
-- Six battle rows on this build (PIKACHU VOL is Yellow-only, so AUDIO is 3).
T.eq(select(1, find(menu.view, "group.battle")).value(game), "6 OPTIONS",
"BATTLE OPTIONS counts its six rows")
-- The top level is explicitly ordered, groups and singles interleaved.
T.eq(ids(menu.view):gsub("group%.", ""),
"speed,video,graphics,audio,performance,ruleset,battle,extras,mods," ..
"controls,dateFormat,timeFormat",
"the top level reads in the order ORDER names")
T.check(not find(menu.view, "textSpeed"), "TEXT SPEED moved onto the SPEED page")
T.check(#menu.view < #menu.rows, "the view is shorter than the flat list")
-- A row in no group is untouched and keeps its order.
T.check(find(menu.view, "ruleset"), "RULESET stays on the top level")
T.check(find(menu.view, "performance"), "so does PERFORMANCE")
T.check(find(menu.view, "mods"), "and MODS")
-- ------------------------------------------------------------- submenus
local graphics = find(menu.view, "group.graphics")
graphics.activate(game)
T.eq(ids(game.stack:top().view), "colors,uiLetterbox,shaderfx,shaderfx2",
"GRAPHICS carries COLORS, UI LETTERBOX and both SHADER FX slots")
game.stack:pop()
local extras = find(menu.view, "group.extras")
extras.activate(game)
T.eq(ids(game.stack:top().view), "tilt,zoom,voidFill",
"EXTRAS carries TILT, ZOOM and VOID FILL")
game.stack:pop()
local video = find(menu.view, "group.video")
video.activate(game)
T.eq(ids(game.stack:top().view):match("^uiLayout"), "uiLayout",
"UI LAYOUT heads the VIDEO page")
game.stack:pop()
local speed = find(menu.view, "group.speed")
speed.activate(game)
T.eq(ids(game.stack:top().view),
"textSpeed,speedOverworld,speedBattle,speedMenu",
"TEXT SPEED heads the SPEED page")
game.stack:pop()
local battle, at = find(menu.view, "group.battle")
menu.index = at
battle.activate(game)
local sub = game.stack:top()
T.check(sub ~= menu, "opening a group pushes a screen")
T.eq(ids(sub.view),
"animations,battleStyle,battleLayout,battleFit,battleHud,battleBg",
"the page carries exactly its six battle rows, in order")
T.eq(sub.sub, true, "the page knows it is a submenu")
T.eq(sub.onCancel, nil, "BACK out of a page never fires the caller's close hook")
-- focusRow reaches a grouped row by opening its page.
local game2 = stubGame()
local menu2 = OptionsMenu.new(game2)
local landed = menu2:focusRow("battleHud")
T.check(landed and landed ~= menu2, "focusRow opens the page a grouped row lives on")
T.eq(landed.view[landed.index].id, "battleHud", "and lands the cursor on it")
T.eq(menu2:focusRow("ruleset"), menu2, "an ungrouped row focuses in place")
T.eq(menu2.view[menu2.index].id, "ruleset", "on the row asked for")
T.eq(menu2:focusRow("nope"), nil, "an unknown id focuses nothing")
-- ------------------------------------------------------------- marquee
-- 16 characters fit beside a value's x=24 before the box frame at x=152;
-- the label's x=16 fits 17.
local long = "1-A (Default) (BGB)"
T.check(#long > 16, "the COLORS value really is too long for the line")
-- 19 characters, 16 fit: three steps of overflow. Hold 1.0s, one char per
-- 0.25s, hold 1.0s on the tail, then repeat -- a 2.75s cycle.
T.eq(Marquee.at(long, 16, 0), long:sub(1, 16), "it holds at the start")
T.eq(Marquee.at(long, 16, 1.3), long:sub(2, 17), "then steps one char")
T.eq(Marquee.at(long, 16, 1.6), long:sub(3, 18), "then another")
T.eq(Marquee.at(long, 16, 2.4), long:sub(4, 19), "then holds on the tail")
T.eq(Marquee.at(long, 16, 2.8), long:sub(1, 16), "before wrapping back")
T.eq(Marquee.at(long, 16, 2.4):sub(-4), "BGB)",
"so the tail the panel used to clip becomes readable")
for _, at2 in ipairs({ 0, 1.3, 1.6, 2.4, 2.8 }) do
T.eq(#Marquee.at(long, 16, at2), 16,
"every frame fills the line exactly")
end
T.eq(Marquee.at("SHORT", 16, 2.0), "SHORT",
"text that fits is never scrolled")
T.finish("options submenu + marquee")
+447
View File
@@ -0,0 +1,447 @@
-- Tests the COLORS picker's 699-palette pack (src/render/Palette.lua) and
-- its substitution seam in src/render/PaletteFX.lua. ROM-free, since the
-- pack is committed data rather than ROM-imported.
-- luajit tests/engine/palette_test.lua
--
-- Covers the ramp ordering flip in Palette.ramp() (the pack stores darkest
-- first, PaletteFX wants lightest first), the substitution seam itself (a
-- custom palette has to win outright and get a zone invented for it, same
-- as OG/OG INV/CLASSIC), and that BattleState/WideBattle's mono checks stay
-- in sync with that seam.
package.path = "./?.lua;./?/init.lua;" .. package.path
if not _G.love then _G.love = require("tests.love_stub") end
local T = require("tests.harness")
local check, eq = T.check, T.eq
local function hex(c)
return type(c) == "table" and ("%02x%02x%02x"):format(c[1], c[2], c[3]) or "?"
end
local Palette = require("src.render.Palette")
local PaletteFX = require("src.render.PaletteFX")
-- ------- the pack
do
eq(#Palette.packs(), 32, "32 groups")
local total = 0
for _, group in ipairs(Palette.packs()) do
total = total + #(group.palettes or {})
end
eq(total, 699, "699 palettes")
end
-- ------- the folders
do
local seen, total = {}, 0
for _, category in ipairs(Palette.categories()) do
for _, item in ipairs(category.palettes) do
total = total + 1
seen[item.id] = category.key
end
end
eq(total, 699, "every palette is filed")
local counted = 0
for _ in pairs(seen) do counted = counted + 1 end
eq(counted, 699, "no palette is filed twice")
-- the DMG ramp is the case HUE_ARC was tuned around
eq(seen[Palette.packId("GalleryWebApp Palettes", "Original GB")],
"single", "the DMG green is one colour")
eq(seen[Palette.packId("BGB", "Grey")], "grey", "BGB Grey is grey")
eq(seen[Palette.packId("GB Color (Unique Palettes)", "GBC Palette 119 (World)")],
"full", "a yellow and red cart palette is full colour")
-- the GBC boot palette for Red is white/pink/dark red/black: a red ramp
eq(seen[Palette.packId("GB Color (Unique Palettes)",
"GBC Palette 093 (USA, Europe)")],
"single", "a tinted grey ramp is one colour")
local first = Palette.categories()[1].palettes[1]
check(type(first.id) == "string" and Palette.ramp(first.id) ~= nil,
"a bucket entry carries a resolvable id")
check(type(first.pack) == "string" and first.pack ~= "",
"a bucket entry remembers its source pack")
local key, at = Palette.locate("p:BGB/Blue")
eq(key, "single", "locate answers the bucket")
eq(Palette.categories()[2].palettes[at].id, "p:BGB/Blue",
"and the tile")
check(Palette.locate("p:Nope/Nothing") == nil, "an unknown id is nowhere")
end
-- ------- classifying a ramp
do
local function cat(a, b, c, d) return Palette.categoryOf({ a, b, c, d }) end
eq(cat({ 255, 255, 255 }, { 170, 170, 170 }, { 85, 85, 85 }, { 0, 0, 0 }),
"grey", "four greys are greyscale")
eq(cat({ 155, 188, 15 }, { 139, 172, 15 }, { 48, 98, 48 }, { 15, 56, 15 }),
"single", "one hue at four strengths is a single colour")
eq(cat({ 255, 255, 255 }, { 255, 132, 132 }, { 148, 58, 58 }, { 0, 0, 0 }),
"single", "greys among one hue do not make it full colour")
eq(cat({ 255, 0, 0 }, { 0, 255, 0 }, { 0, 0, 255 }, { 0, 0, 0 }),
"full", "three hues is full colour")
eq(cat({ 255, 40, 20 }, { 255, 10, 40 }, { 128, 5, 20 }, { 40, 2, 6 }),
"single", "a ramp straddling 0 degrees is one colour")
check(Palette.categoryOf(nil) == nil, "nothing classifies as nothing")
end
-- ------- hex decoding
do
local c = Palette.unhex("0f380f30623077a1129bbc0f")
check(c and hex(c[1]) == "0f380f", "first colour is the string's first")
check(c and hex(c[4]) == "9bbc0f", "last colour is the string's last")
check(Palette.unhex("abc") == nil, "short string rejected")
check(Palette.unhex("zz380f30623077a1129bbc0f") == nil, "non-hex rejected")
check(Palette.unhex(nil) == nil, "nil rejected")
end
-- ------- ordering
--
-- #9BBC0F is the PAPER and #0F380F is the INK, so ramp()[1] must be pale.
do
local id = Palette.packId("GalleryWebApp Palettes", "Original GB")
local ramp = Palette.ramp(id)
check(ramp ~= nil, "Original GB resolves")
eq(hex(ramp[1]), "9bbc0f", "ramp[1] is the lightest (paper)")
eq(hex(ramp[4]), "0f380f", "ramp[4] is the darkest (ink)")
local sw = Palette.swatch(id)
eq(hex(sw[1]), "0f380f", "swatch[1] is the darkest")
eq(hex(sw[4]), "9bbc0f", "swatch[4] is the lightest")
end
-- cross-check against PaletteFX.GBC_BG, the same palette hardcoded there
do
local ramp = Palette.ramp(Palette.packId(
"GB Color (Unique Palettes)", "GBC Palette 093 (USA, Europe)"))
for i = 1, 4 do
eq(hex(ramp[i]), hex(PaletteFX.GBC_BG[i]),
"pack Red ramp[" .. i .. "] agrees with PaletteFX.GBC_BG")
end
end
-- ------- monochrome
do
local id = Palette.monoId(0x9b, 0xbc, 0x0f)
eq(id, "m:9bbc0f", "id encodes as hex")
local ramp = Palette.ramp(id)
eq(hex(ramp[1]), "9bbc0f", "lightest is the chosen colour")
check(ramp[4][1] < ramp[1][1], "darkest is darker than lightest")
eq(Palette.label(id), "GREEN", "a known colour gets its name")
eq(Palette.label("m:123456"), "MONO", "an unknown one falls back")
end
-- ------- ids
do
check(Palette.label("") == nil, "empty id has no label")
eq(Palette.label("p:BGB/Blue"), "Blue", "pack label is the name")
check(Palette.ramp("") == nil, "empty id resolves to nothing")
check(Palette.ramp("p:Nope/Nothing") == nil, "missing palette resolves to nothing")
check(Palette.ramp("garbage") == nil, "malformed id resolves to nothing")
end
-- ------- every palette in the pack parses
do
local bad, checked = {}, 0
for _, group in ipairs(Palette.packs()) do
for _, entry in ipairs(group.palettes or {}) do
local ramp = Palette.ramp(Palette.packId(group.name, entry[1]))
checked = checked + 1
if not ramp then bad[#bad + 1] = group.name .. "/" .. tostring(entry[1]) end
end
end
eq(checked, 699, "checked every palette")
eq(#bad, 0, "all resolve (" .. table.concat(bad, ", ", 1, math.min(#bad, 3)) .. ")")
end
-- ------- the picker's state machine
--
-- update() touches no love API, so we can drive it with a stub game.
local PaletteScreen = require("src.ui.PaletteScreen")
do
local pressed = {}
local function open(stored)
local state = { stored = stored or "", popped = 0, mode = "redpp" }
state.screen = PaletteScreen.new({
input = { wasPressed = function(_, k) return pressed[k] == true end },
stack = { states = {},
pop = function() state.popped = state.popped + 1 end },
}, {
palette = Palette,
get = function() return state.stored end,
set = function(v) state.stored = v end,
modes = { { "SGB", "gbc" }, { "ADVANCED", "redpp" } },
getMode = function() return state.mode end,
setMode = function(v) state.mode = v end,
})
return state
end
local function press(state, key)
pressed = { [key] = true }
state.screen:update()
pressed = {}
end
local fresh = open()
eq(fresh.screen.view, "root", "a fresh profile opens at the root")
eq(#fresh.screen.rows, 6, "four folders and two modes")
eq(fresh.screen.rows[4].label, "MONOCHROME")
check(fresh.screen.rows[4].folder, "MONOCHROME is the fourth folder")
eq(fresh.screen.rows[5].mode, "gbc")
check(not fresh.screen.rows[5].folder, "an engine mode is a row and not a folder")
press(fresh, "a")
eq(fresh.screen.view, "tiles", "A on a folder opens its grid one level down")
eq(#fresh.screen.stackTrail, 1)
press(fresh, "b")
eq(fresh.screen.view, "root", "B steps back up to the root")
eq(fresh.popped, 0)
press(fresh, "b")
eq(fresh.popped, 1, "B at the root closes the screen")
-- a stored selection should open straight into its grid
local stored = open("p:BGB/Blue")
eq(stored.screen.view, "tiles", "a stored palette opens in its folder")
eq(stored.screen.group.key, "single", "and in the RIGHT folder")
eq(stored.screen:idAt(stored.screen.index), "p:BGB/Blue",
"with the cursor on the stored tile")
eq(#stored.screen.stackTrail, 1, "the skipped level is on the trail")
press(stored, "b")
eq(stored.screen.view, "root", "B steps up instead of closing")
eq(stored.popped, 0)
eq(stored.screen.index, 2, "and lands on the folder it came from")
-- an engine mode and a palette share the same slot
local modes = open("p:BGB/Blue")
press(modes, "b")
for _ = 1, 3 do press(modes, "down") end
eq(modes.screen.rows[modes.screen.index].mode, "gbc", "walked to the SGB row")
press(modes, "a")
eq(modes.stored, "", "picking an engine mode clears the palette")
eq(modes.mode, "gbc", "and sets the mode")
local pick = open()
press(pick, "a")
press(pick, "right")
local wanted = pick.screen:idAt(pick.screen.index)
press(pick, "a")
eq(pick.stored, wanted, "A on a tile stores that tile's id")
check(wanted ~= nil)
end
-- ------- the picker's REAL wiring: PaletteScreen.new(game) with no opts
--
-- Everything above injects its own opts to keep update() engine-free, so
-- none of it would catch a bug in liveOpts/dropConflictingMode. This
-- builds the screen the way OptionsMenu actually does, with no opts at all.
do
local game = {
input = { wasPressed = function() return false end },
stack = { states = {}, pop = function() end },
save = { options = { colors = "redpp" } },
}
PaletteFX.mode = "redpp"
local screen = PaletteScreen.new(game)
check(screen.view == "root", "a live-opts screen opens at the root")
check(type(screen.get) == "function" and type(screen.set) == "function",
"liveOpts wired real get/set functions")
eq(game.save.options.colors, "gbc",
"opening the picker while ADVANCED was active dropped it to SGB "
.. "(dropConflictingMode ran)")
eq(PaletteFX.mode, "gbc", "and the live mode followed")
local id = Palette.packId("BGB", "Blue")
screen.set(id)
eq(game.save.options.palette, id, "storing a palette writes save.options.palette")
eq(hex(PaletteFX.customRamp[1]), hex(Palette.ramp(id)[1]),
"and PaletteFX.setCustomRamp actually ran")
screen.set("")
check(PaletteFX.customRamp == nil, "storing empty clears the custom ramp")
PaletteFX.mode = "gbc"
end
-- ------- PaletteFX seam: a custom ramp wins outright
do
PaletteFX.setCustomRamp(nil)
PaletteFX.mode = "gbc"
check(not PaletteFX.forcesRawGrays(), "no custom ramp: forcesRawGrays is false")
local id = Palette.packId("GalleryWebApp Palettes", "Original GB")
local ramp = Palette.ramp(id)
PaletteFX.setCustomRamp(ramp)
check(PaletteFX.forcesRawGrays(), "a stored custom ramp: forcesRawGrays is true")
local someZoneColors = { { 9, 9, 9 }, { 8, 8, 8 }, { 7, 7, 7 }, { 6, 6, 6 } }
local out = PaletteFX.effectiveColors(someZoneColors)
for i = 1, 4 do
eq(hex(out[i]), hex(ramp[i]),
"effectiveColors[" .. i .. "] is the custom ramp, not the zone's own colours")
end
-- it should win even over a mode with its own fixed palette
PaletteFX.mode = "classic"
local out2 = PaletteFX.effectiveColors(someZoneColors)
eq(hex(out2[1]), hex(ramp[1]), "a custom ramp beats CLASSIC too")
PaletteFX.mode = "gbc"
end
-- ------- picker immunity
--
-- The picker's swatches are real RGB, so effectiveColors and ensureZones
-- both have to back off while it's on top or it would recolour its own
-- swatches.
do
local Game = require("src.core.Game")
Game.stack = nil
check(not PaletteFX.pickerActive(), "no stack: pickerActive is false")
Game.stack = { states = { { screenId = "OptionsMenu" } } }
check(not PaletteFX.pickerActive(), "some other screen on top: pickerActive is false")
Game.stack = { states = { { screenId = "OptionsMenu" },
{ screenId = "PaletteScreen" } } }
check(PaletteFX.pickerActive(), "PaletteScreen on top of the stack: pickerActive is true")
local id = Palette.packId("BGB", "Red")
local ramp = Palette.ramp(id)
PaletteFX.setCustomRamp(ramp)
PaletteFX.mode = "gbc"
local someZoneColors = { { 9, 9, 9 }, { 8, 8, 8 }, { 7, 7, 7 }, { 6, 6, 6 } }
local out = PaletteFX.effectiveColors(someZoneColors)
eq(hex(out[1]), hex(someZoneColors[1]),
"with the picker on top, effectiveColors does NOT substitute the custom ramp")
check(PaletteFX.ensureZones(nil) == nil,
"with the picker on top, ensureZones invents nothing for the custom ramp")
-- closing the picker should restore the substitution immediately
Game.stack = { states = { { screenId = "OptionsMenu" } } }
check(not PaletteFX.pickerActive(), "closing the picker: pickerActive is false again")
local out2 = PaletteFX.effectiveColors(someZoneColors)
eq(hex(out2[1]), hex(ramp[1]), "and the custom ramp substitutes again")
local invented = PaletteFX.ensureZones(nil)
check(invented and #invented == 1, "and ensureZones invents its zone again")
Game.stack = nil
PaletteFX.setCustomRamp(nil)
end
-- ------- PaletteFX seam: the invented zone, end to end
--
-- A state with no SGB zones of its own gets one invented by ensureZones,
-- and effectiveColors then has to turn that zone's colours into the
-- chosen ramp.
do
PaletteFX.setCustomRamp(nil)
check(PaletteFX.ensureZones(nil) == nil,
"no custom ramp, no mono mode: ensureZones invents nothing")
local id = Palette.packId("BGB", "Red")
local ramp = Palette.ramp(id)
PaletteFX.setCustomRamp(ramp)
local invented = PaletteFX.ensureZones(nil)
check(invented and #invented == 1, "a custom ramp invents one zone")
eq(invented[1].w, 160, "the invented zone is whole-screen (w)")
eq(invented[1].h, 144, "the invented zone is whole-screen (h)")
for i = 1, 4 do
eq(hex(invented[1].colors[i]), hex(PaletteFX.GRAYS[i]),
"the invented zone is plain DMG grays, not the ramp itself")
end
-- run it back through effectiveColors, the way Renderer.sendColors would
local final = PaletteFX.effectiveColors(invented[1].colors)
for i = 1, 4 do
eq(hex(final[i]), hex(ramp[i]),
"the invented zone resolves to the custom ramp at send time")
end
local realZones = { { colors = { { 1, 1, 1 } }, x = 0, y = 0, w = 40, h = 40 } }
check(PaletteFX.ensureZones(realZones) == realZones,
"a state with real zones is left alone")
PaletteFX.setCustomRamp(nil)
check(not PaletteFX.forcesRawGrays(), "clearing the ramp clears forcesRawGrays")
end
-- ------- the four sync points
--
-- BattleState:picImage, BattleState:drawZonePass, WideBattle's monoMode and
-- PaletteFX.ensureZones all gate on the same "forced raw grays" condition.
-- This is a structural check (source-level, no ROM) that none of the four
-- got missed when forcesRawGrays was added.
do
local function read(path)
local f = assert(io.open(path, "r"))
local src = f:read("*a")
f:close()
return src
end
local battleSrc = read("src/battle/BattleState.lua")
local picImage = battleSrc:match("function BattleState:picImage%(img%)(.-)\nend")
check(picImage and picImage:find("forcesRawGrays", 1, true) ~= nil,
"picImage's mono check includes forcesRawGrays")
local drawZonePass = battleSrc:match(
"function BattleState:drawZonePass%(src, sx, sy%)(.-)\nend")
check(drawZonePass and drawZonePass:find("forcesRawGrays", 1, true) ~= nil,
"drawZonePass's mono check includes forcesRawGrays")
local wideSrc = read("src/battle/WideBattle.lua")
local monoMode = wideSrc:match("local function monoMode%(%)(.-)\nend")
check(monoMode and monoMode:find("forcesRawGrays", 1, true) ~= nil,
"WideBattle's monoMode includes forcesRawGrays")
local fxSrc = read("src/render/PaletteFX.lua")
local ensureZones = fxSrc:match("function PaletteFX.ensureZones%(zones%)(.-)\nend")
check(ensureZones and ensureZones:find("forcesRawGrays", 1, true) ~= nil,
"ensureZones itself includes forcesRawGrays")
end
-- ------- COLORS row wiring
--
-- OptionsMenu's colors row should open the picker instead of cycling in place.
do
local optSrc = (function()
local f = assert(io.open("src/ui/OptionsMenu.lua", "r"))
local s = f:read("*a")
f:close()
return s
end)()
-- bounded to the row itself, or the scan would run into TILT's own step
local block = optSrc:match('{ id = "colors",(.-)\n { id =')
check(block ~= nil, "colors row block found")
check(block:find("activate = function%(g%)") ~= nil,
"colors row has an activate")
check(block:find('Screens"%).push%(g, "PaletteScreen"%)', 1, false) ~= nil
or block:find('Screens").push(g, "PaletteScreen")', 1, true) ~= nil,
"colors row's activate pushes PaletteScreen")
check(block:find("step = function") == nil,
"colors row no longer has a step")
end
T.finish("palette")
+52
View File
@@ -180,6 +180,58 @@ do
T.eq(eng.phase, "idle", "and the sync ends idle")
end
-- Identical playtime is not a fork. The Gold prompt the player saw offered
-- "GOLD - 8 badges - 3:46 - 9 seen" against "GOLD - 8 badges - 3:46 - 9 seen":
-- the same minute of the same playthrough, two saves that differ only in
-- their savedAt stamp. Nothing to choose between, so nothing to ask.
do
T.eq(SyncEngine.samePlaytime(
{ summary = { timeText = "3:46" } },
{ summary = { timeText = "3:46" } }), true,
"the same H:MM is the same point in the playthrough")
T.eq(SyncEngine.samePlaytime(
{ summary = { timeText = "3:46" } },
{ summary = { timeText = "3:47" } }), false,
"one minute apart is a real fork")
T.eq(SyncEngine.samePlaytime({ playTime = 13560 }, { playTime = 13599 }),
true, "Gen 1's seconds count compares to the minute, not the second")
T.eq(SyncEngine.samePlaytime({ playTime = 13560 }, { playTime = 13620 }),
false, "and a whole minute apart still forks")
-- Gen 2 stores playTime as a table, so meta.playTime is nil on a Gold
-- save and only timeText survives; a missing time must never read as a
-- match, or an unknowable conflict would be silently discarded.
T.eq(SyncEngine.samePlaytime({}, {}), false,
"two unknown playtimes are not a match")
T.eq(SyncEngine.samePlaytime({ summary = { timeText = "3:46" } }, {}), false,
"and neither is one unknown side")
end
do
local state = linkedState()
SyncState.setRev(state, "red/abc", 7, 500)
local entry = saveEntry("red", "abc", 700, 650)
entry.meta.summary.timeText = "3:46"
local eng, transport = engine({
["GET /sync/state"] = { code = 200,
body = '{"saves":{"red/abc":{"rev":9,"meta":{"savedAt":760,' ..
'"sessionStart":600,"summary":{"name":"ASH","timeText":"3:46"}}}}}' },
["PUT /sync/save"] = { code = 200, body = '{"ok":true,"rev":10}' },
}, { entry }, state)
eng:syncNow()
pump(eng)
T.eq(#eng.conflicts, 0, "matching playtime raises no conflict")
T.eq(#eng.state.pendingConflicts, 0, "and leaves nothing pending")
T.neq(eng.phase, "conflict", "so the player is never prompted")
local put
for _, req in ipairs(transport.sent) do
if req.method == "PUT" then put = req end
end
T.check(put ~= nil, "this device's copy is pushed instead")
T.check(put and put.body:find('"force":true', 1, true) ~= nil,
"forced past the moved rev, since there is nothing to preserve")
end
local function conflictEngine()
local state = linkedState()
SyncState.setRev(state, "red/abc", 7, 500)
+64
View File
@@ -0,0 +1,64 @@
-- UI LETTERBOX (save.options.uiLetterbox): what fills the window around a
-- 160x144 screen. AUTO keeps what each screen was authored with, so the
-- default is byte-identical to the look before the option existed.
-- luajit tests/engine/ui_letterbox_test.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local Letterbox = require("src.render.Letterbox")
T.eq(require("src.core.SaveData").newGame().options.uiLetterbox, "auto",
"Gen 1 starts on AUTO")
T.eq(require("src.core.gen2.Save").DEFAULT_OPTIONS.uiLetterbox, "auto",
"so does Gen 2")
-- AUTO hands back the colour the caller was authored with, untouched: the
-- Pokedex stays black, a menu stays white, the naming screen stays paper.
Letterbox.setMode("auto")
T.same({ Letterbox.fill(0, 0, 0) }, { 0, 0, 0 }, "AUTO keeps an authored black")
T.same({ Letterbox.fill(1, 1, 1) }, { 1, 1, 1 }, "AUTO keeps an authored white")
T.same({ Letterbox.fill(0.5, 0.25, 0.75) }, { 0.5, 0.25, 0.75 },
"AUTO keeps an authored paper colour")
-- PALETTE reads the caller's paper, because the two generations keep a picked
-- ramp in different modules. Gen 1 used to come back white here: anything that
-- sniffed for GbcPalette found it present on Gen 1 too and answered with Gen
-- 2's white default instead of PaletteFX's actual paper.
Letterbox.setMode("palette")
T.same({ Letterbox.fill(0, 0, 0, function() return 0.25, 0.5, 1 end) },
{ 0.25, 0.5, 1 }, "PALETTE takes the caller's paper")
T.same({ Letterbox.fill(0, 0, 0) }, { 0, 0, 0 },
"with no paper reader it falls back to the authored colour, never to white")
T.same({ Letterbox.fill(1, 1, 1, function() return nil end) }, { 1, 1, 1 },
"and a reader that cannot answer falls back too")
Letterbox.setMode("black")
T.same({ Letterbox.fill(1, 1, 1) }, { 0, 0, 0 }, "BLACK overrides a white screen")
Letterbox.setMode("white")
T.same({ Letterbox.fill(0, 0, 0) }, { 1, 1, 1 }, "WHITE overrides a black screen")
-- An unknown or absent value degrades to AUTO rather than to a colour, so a
-- save written before the option existed keeps its look.
T.eq(Letterbox.normalize(nil), "auto", "no stored value is AUTO")
T.eq(Letterbox.normalize("nonsense"), "auto", "and so is a value we cannot read")
Letterbox.setMode(nil)
T.same({ Letterbox.fill(0, 0, 0) }, { 0, 0, 0 }, "so an old save is unchanged")
-- The ladder wraps both ways, like every other option row.
T.eq(Letterbox.cycle("auto", 1), "black", "right steps off AUTO")
T.eq(Letterbox.cycle("palette", 1), "auto", "and wraps at the end")
T.eq(Letterbox.cycle("auto", -1), "palette", "left wraps back")
for _, id in ipairs(Letterbox.MODES) do
T.check(Letterbox.label(id) ~= nil, id .. " has a label")
end
T.eq(Letterbox.label("uiLetterbox"), "AUTO", "an unknown id still labels")
-- applyOptions is the seam both Game:applyOptions and Game2:applyOptions call.
Letterbox.applyOptions({ uiLetterbox = "white" })
T.eq(Letterbox.mode, "white", "applyOptions installs the saved mode")
Letterbox.applyOptions({})
T.eq(Letterbox.mode, "auto", "and an options table without one is AUTO")
Letterbox.setMode("auto")
T.finish("ui letterbox")
+52 -36
View File
@@ -360,33 +360,51 @@ local options = OptionsMenu.new(optionsGame, {
-- speed, display, SHADER FX + SHADER FX 2 (the second slot added alongside
-- the dual-shader feature), video mode, screen position, the mobile-gated
-- touch three (buildRows), MAX FPS, BATTLE BG and CANCEL.
check("twenty-seven rows", #OptionsMenu.ROWS, 27)
check("twenty-eight rows", #OptionsMenu.ROWS, 28)
check("the cart's rows come first", OptionsMenu.ROWS[7].key, "frame")
check("then the rebind screen", OptionsMenu.ROWS[8].id, "controls")
check("then the port's audio group", OptionsMenu.ROWS[9].key, "musicVol")
check("last row is CANCEL", OptionsMenu.ROWS[#OptionsMenu.ROWS].cancel, true)
check("starts on TEXT SPEED", options:row().key, "textSpeed")
check("default text speed", options.options.textSpeed, "MID")
check("last row is BACK", OptionsMenu.ROWS[#OptionsMenu.ROWS].cancel, true)
-- PRINT is wGBPrinterBrightness and there is no Game Boy Printer here, so
-- buildRows hides it: the descriptor and the save key survive, the row does
-- not reach the screen.
local function hasRow(rows, key)
for _, row in ipairs(rows) do if row.key == key then return true end end
return false
end
check("PRINT is still a descriptor", hasRow(OptionsMenu.ROWS, "print"), true)
check("but never reaches the screen", hasRow(options.rows, "print"), false)
check("and the save keeps its value",
Save.defaultOptions().print ~= nil, true)
-- The rows are grouped into pages now, so the top level opens on the SPEED
-- group and TEXT SPEED is the first row of the page it opens.
check("starts on the SPEED group", options:row().id, "group.speed")
local speedPage = options:focusRow("textSpeed")
check("TEXT SPEED is on a page", speedPage ~= options, true)
check("and the cursor lands on it", speedPage:row().key, "textSpeed")
check("default text speed", speedPage.options.textSpeed, "MID")
optionsInput:press("right")
options:update(0)
speedPage:update(0)
check("right cycles forward", options.options.textSpeed, "SLOW")
optionsInput:press("right")
options:update(0)
speedPage:update(0)
check("right wraps", options.options.textSpeed, "FAST")
optionsInput:press("left")
options:update(0)
speedPage:update(0)
check("left wraps back", options.options.textSpeed, "SLOW")
check("a page edits the caller's own options table",
speedPage.options == options.options, true)
optionsInput:press("down")
options:update(0)
check("down moves a row", options:row().key, "battleScene")
local battlePage = options:focusRow("battleScene")
check("BATTLE SCENE is on the battle page", battlePage:row().key, "battleScene")
optionsInput:press("right")
options:update(0)
battlePage:update(0)
check("battle scene toggles off", options.options.battleScene, false)
-- FRAME is 1-8 and wraps.
options.index = 7
check("frame row", options:row().frame, true)
local framePage = options:focusRow("frame")
check("frame row", framePage:row().frame, true)
options = framePage
options.options.frame = 8
optionsInput:press("right")
options:update(0)
@@ -406,10 +424,10 @@ local exiting = OptionsMenu.new(exitGame, {
})
-- The screen's own rows, not ROWS: buildRows drops the touch three off a
-- desktop, so the raw descriptor count overshoots CANCEL.
exiting.index = #exiting.rows
exiting.index = #exiting.view
exitInput:press("a")
exiting:update(0)
check("CANCEL leaves", savedOptions ~= nil, true)
check("BACK leaves", savedOptions ~= nil, true)
-- --------------------------------------------------------- start menu
@@ -934,16 +952,13 @@ local colorRow = select(2, rowNamed("COLOR"))
check("COLOR is a row", colorRow ~= nil, true)
check("and it defaults to the cart's own colour",
Save.DEFAULT_OPTIONS.color, "gbc")
scrollOptions.options.color = "gbc"
scrollOptions:cycle(colorRow, 1)
check("right steps to DMG", scrollOptions.options.color, "dmg")
scrollOptions:cycle(colorRow, 1)
check("then CLASSIC", scrollOptions.options.color, "classic")
scrollOptions:cycle(colorRow, 1)
check("and wraps back to GBC", scrollOptions.options.color, "gbc")
scrollOptions:cycle(colorRow, -1)
check("left walks the ladder the other way", scrollOptions.options.color,
"classic")
-- COLOR no longer cycles in place (tests/engine/gen2_palette_picker_test.lua
-- covers the picker it opens instead, end to end); the ladder itself still
-- steps via the `2` hotkey (src/core/Game2.lua:hotkey), untouched here.
check("COLOR no longer cycles in place", colorRow.cycle, nil)
check("COLOR opens the picker instead", type(colorRow.activate), "function")
scrollOptions.options.color = "classic"
GbcPalette.setMode("classic")
check("CLASSIC is the only mode with a present pass",
GbcPalette.presentColors() ~= nil, true)
GbcPalette.setMode("dmg")
@@ -965,16 +980,16 @@ check("VOID FILL follows ZOOM", OptionsMenu.ROWS[zoomIndex + 1].label,
"VOID FILL")
check("and TILT follows VOID FILL", OptionsMenu.ROWS[zoomIndex + 2].label,
"TILT")
check("SHADER FX follows COLOR follows TILT", OptionsMenu.ROWS[tiltIndex + 2].label,
"SHADER FX")
check("SHADER FX 2 follows SHADER FX", OptionsMenu.ROWS[tiltIndex + 3].label,
"SHADER FX 2")
check("VIDEO MODE follows SHADER FX 2", OptionsMenu.ROWS[tiltIndex + 4].label,
"VIDEO MODE")
check("and SCREEN POS follows it", OptionsMenu.ROWS[tiltIndex + 5].label,
"SCREEN POS")
check("and TOUCH PAD follows that", OptionsMenu.ROWS[tiltIndex + 6].label,
"TOUCH PAD")
-- By sequence rather than by offset, so inserting a row in the display block
-- moves the whole run instead of breaking six separate index assertions.
do
local run = { "TILT", "COLOR", "UI LETTERBOX", "SHADER FX",
"SHADER FX 2", "VIDEO MODE", "SCREEN POS", "TOUCH PAD" }
for at, want in ipairs(run) do
check("display block order: " .. want,
OptionsMenu.ROWS[tiltIndex + at - 1].label, want)
end
end
local videoRow = select(2, rowNamed("VIDEO MODE"))
check("VIDEO MODE is a row", videoRow ~= nil, true)
@@ -2518,7 +2533,8 @@ local function modRowChecks()
local og, oi = newGame(nil)
local om = OptionsMenu.new(og, { options = Save.defaultOptions() })
check("gen2 OPTION takes the hook's row", om.rows[#om.rows].id, "modrow")
om.index = #om.rows
-- A hook row is in no group, so it stays on the top level.
check("and keeps it reachable there", om:focusRow("modrow"), om)
oi:press("a")
om:update(0)
check("A on a mod row calls activate", fired, 1)
+17 -2
View File
@@ -146,8 +146,23 @@ Chrome.cursor = function(tx, ty) cursorAt = { x = tx, y = ty } end
Chrome.clear = function() end
Chrome.textbox = function() end
local options = OptionsMenu.new({}, { options = {} })
options:drawPanel()
-- TEXT SPEED and FRAME sit on group pages now; the pages are the same
-- screen and draw at the same StringOptions coordinates, which is what this
-- pins. A stack, because opening a page pushes one.
local optStack = { items = {} }
function optStack:push(s) self.items[#self.items + 1] = s; return s end
function optStack:pop() self.items[#self.items] = nil end
function optStack:top() return self.items[#self.items] end
local options = OptionsMenu.new({ stack = optStack }, { options = {} })
options:focusRow("textSpeed"):drawPanel()
local framePage = options:focusRow("frame")
framePage.index = 1
for i, row in ipairs(framePage:visible()) do
if row.frame then framePage.index = i end
end
framePage:ensureVisible()
framePage:drawPanel()
Chrome.print, Chrome.cursor = realPrint, realCursor
Chrome.clear, Chrome.textbox = realClear, realBox
+2 -3
View File
@@ -293,9 +293,8 @@ do
end },
stack = { pop = function() end } }
local menu = OptionsMenu.new(game)
for i, row in ipairs(menu.rows) do
if row.id == "ruleset" then menu.index = i end
end
-- RULESET is in no group, so this focuses it on the top level.
check(menu:focusRow("ruleset") == menu, "RULESET focuses in place")
local function press(key)
pressed = { [key] = true }
menu:update(1 / 60)
+8 -8
View File
@@ -285,7 +285,7 @@ local WANT_IDS = { "textSpeed", "animations", "battleStyle", "battleLayout",
"battleFit", "battleHud", "battleBg", "uiLayout",
"ruleset", "musicVol", "sfxVol", "musicFilter",
"performance", "colors",
"tilt", "shaderfx", "shaderfx2", "zoom", "voidFill",
"tilt", "uiLetterbox", "shaderfx", "shaderfx2", "zoom", "voidFill",
"videoMode", "faithfulRes", "screenPos", "fpsCap",
"speedOverworld", "speedBattle", "speedMenu",
"mods", "controls", "dateFormat", "timeFormat" }
@@ -369,10 +369,10 @@ check(om.game.save.options.voidFill == "trees", "VOID FILL wraps to TREES")
-- mirrors it one row down, opening the same shared screen on
-- "secondary" instead.
local ShaderFX = require("src.render.ShaderFX")
check(om.rows[16].id == "shaderfx", "row 16 is the SHADER FX row")
check(om.rows[16].value(om.game) == "OFF", "SHADER FX shows OFF with no presets")
check(om.rows[16].step == nil, "SHADER FX row has no step() any more")
om.rows[16].activate(om.game)
local sfx = orow(om, "shaderfx")
check(sfx.value(om.game) == "OFF", "SHADER FX shows OFF with no presets")
check(sfx.step == nil, "SHADER FX row has no step() any more")
sfx.activate(om.game)
local sfxScreen = om.game.stack:top()
check(sfxScreen and sfxScreen.title == "SHADER FX",
"SHADER FX row.activate() pushes a ShaderFXScreen")
@@ -383,9 +383,9 @@ sfxScreen.onChoose(sfxScreen.items[1])
check(ShaderFX.active("main") == false, "choosing OFF on an empty list stays a safe no-op")
check(om.game.stack:top() == nil, "ShaderFXScreen pops itself after onChoose")
check(om.rows[17].id == "shaderfx2", "row 17 is the SHADER FX 2 row")
check(om.rows[17].value(om.game) == "OFF", "SHADER FX 2 shows OFF with no presets")
om.rows[17].activate(om.game)
local sfx2 = orow(om, "shaderfx2")
check(sfx2.value(om.game) == "OFF", "SHADER FX 2 shows OFF with no presets")
sfx2.activate(om.game)
local sfx2Screen = om.game.stack:top()
check(sfx2Screen and sfx2Screen.title == "SHADER FX 2",
"SHADER FX 2 row.activate() pushes the shared ShaderFXScreen on the secondary slot")