mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-27 00:48:32 +02:00
big palette energy
This commit is contained in:
@@ -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
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user