From cd33858020567ed6ee4d9aa47db694bbfeb04752 Mon Sep 17 00:00:00 2001 From: Marcus Pereira Date: Tue, 28 Jul 2026 23:08:22 -0300 Subject: [PATCH 1/3] Respect the device rotation lock on Android The manifest asks for android:screenOrientation="fullUser", but SDL overrides it at window creation: SDLActivity.setOrientationBis, given a resizable window and no SDL_HINT_ORIENTATIONS -- which is what conf.lua produces on Android -- requests SCREEN_ORIENTATION_FULL_SENSOR. The *_SENSOR constants follow the accelerometer even when the player has turned auto-rotate off, so the game rotated anyway on a locked device. Override setOrientationBis in GameActivity to remap SDL's request onto the matching *_USER constant after super has run. The same orientations stay allowed and SDL keeps deciding which ones those are; only the tie-break changes, from the sensor to the system rotation setting. --- conf.lua | 5 +- mobile/ANDROID.md | 2 +- .../java/org/love2d/android/GameActivity.java | 49 +++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/conf.lua b/conf.lua index 04dd2997..28e16776 100644 --- a/conf.lua +++ b/conf.lua @@ -54,7 +54,10 @@ function love.conf(t) -- the game follow the device. The renderer letterboxes the 160x144 -- viewport into whatever size results, and the on-screen touch controls -- re-lay themselves out from the new window size, so both orientations - -- just work. iOS follows the Info.plist orientations + -- just work. FULL_SENSOR ignores the device's rotation lock, so + -- GameActivity.setOrientationBis remaps it to FULL_USER after SDL has + -- run: same orientations allowed, but auto-rotate being off now wins. + -- iOS follows the Info.plist orientations -- (see mobile/ios/overlays/love-ios.plist, now portrait + landscape). t.window.resizable = true -- Starting size is a tall portrait hint; the OS resizes to the real diff --git a/mobile/ANDROID.md b/mobile/ANDROID.md index 1d4d6849..3c88c3f8 100644 --- a/mobile/ANDROID.md +++ b/mobile/ANDROID.md @@ -79,7 +79,7 @@ scripts, tests, and mobile build sources are excluded. | --- | --- | | `app.application_id` | `com.theboisclub.pokemonred` | | `app.name` | Pokemon Red | -| `app.orientation` | `portrait` | +| `app.orientation` | `fullUser`. This is only the manifest default: SDL requests FULL_SENSOR at window creation (resizable window, no `SDL_HINT_ORIENTATIONS`), and `GameActivity.setOrientationBis` remaps that to FULL_USER so the device's rotation lock is honoured. | | `app.version_name` / `app.version_code` | set from `--version X.Y.Z` (code = major*10000 + minor*100 + patch); left as-is if `--version` is omitted | | Permissions | INTERNET / RECORD_AUDIO / WRITE_EXTERNAL_STORAGE stripped; VIBRATE + BLUETOOTH kept | diff --git a/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java b/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java index 96f4aee5..db4405d3 100644 --- a/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java +++ b/mobile/android/love/src/main/java/org/love2d/android/GameActivity.java @@ -41,6 +41,7 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.res.AssetManager; import android.media.AudioManager; @@ -299,6 +300,54 @@ public class GameActivity extends SDLActivity { super.onResume(); } + /** + * SDL decides the activity's requested orientation at window creation + * (SDLActivity.setOrientationBis). With a resizable window and no + * SDL_HINT_ORIENTATIONS -- exactly what conf.lua produces on Android -- + * it asks for SCREEN_ORIENTATION_FULL_SENSOR, and that request overrides + * the android:screenOrientation="fullUser" set in the manifest. The + * *_SENSOR constants follow the accelerometer even when the player has + * turned auto-rotate off, so the game kept rotating on a device whose + * rotation was locked. + * + * Remap SDL's choice onto the matching *_USER constant, which allows the + * same orientations but defers to the system rotation setting. Applied + * after super so SDL keeps deciding *which* orientations the window may + * take; this only changes who breaks the tie, the sensor or the player. + */ + @Override + public void setOrientationBis(int w, int h, boolean resizable, String hint) { + super.setOrientationBis(w, h, resizable, hint); + + // The *_USER constants only exist from API 18; below that the sensor + // ones are all there is, so leave SDL's request alone. + if (android.os.Build.VERSION.SDK_INT < 18) { + return; + } + + int requested = getRequestedOrientation(); + int userRequested; + switch (requested) { + case ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR: + userRequested = ActivityInfo.SCREEN_ORIENTATION_FULL_USER; + break; + case ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE: + userRequested = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE; + break; + case ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT: + userRequested = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT; + break; + default: + // SENSOR / plain LANDSCAPE / PORTRAIT etc: either already + // explicit or never produced by setOrientationBis. + return; + } + + Log.d("GameActivity", "requestedOrientation " + requested + " -> " + userRequested + + " (honour the device rotation lock)"); + setRequestedOrientation(userRequested); + } + @Keep public void setImmersiveMode(boolean immersive_mode) { if (android.os.Build.VERSION.SDK_INT >= 28) { From 0a8bca1a6db774a621a3e766a3ee9bbaa63c2bb3 Mon Sep 17 00:00:00 2001 From: johnjohto Date: Thu, 30 Jul 2026 17:49:02 -0400 Subject: [PATCH 2/3] Fix Cerulean badge menu exit The selected badge list stayed on the stack while its description opened. Cancelling the next list only closed that newer menu, leaving the NPC conversation open. --- data/scripts/flavor/cerulean_badge_house.lua | 1 + tests/parity_cerulean_badge_house.lua | 63 ++++++++++++++++++++ tests/run_tests.lua | 1 + 3 files changed, 65 insertions(+) create mode 100644 tests/parity_cerulean_badge_house.lua diff --git a/data/scripts/flavor/cerulean_badge_house.lua b/data/scripts/flavor/cerulean_badge_house.lua index 106fc0c8..85052404 100644 --- a/data/scripts/flavor/cerulean_badge_house.lua +++ b/data/scripts/flavor/cerulean_badge_house.lua @@ -42,6 +42,7 @@ local function middleAgedMan(game, ow, npc, done) end local menu = ListMenu.new(game, "", items, { onChoose = function(item) + game.stack:pop() push(game, t[BADGE_TEXT[item.value]], loop) end, onCancel = function() diff --git a/tests/parity_cerulean_badge_house.lua b/tests/parity_cerulean_badge_house.lua new file mode 100644 index 00000000..b2d5827c --- /dev/null +++ b/tests/parity_cerulean_badge_house.lua @@ -0,0 +1,63 @@ +-- Parity: leaving the Cerulean Badge House badge-description menu must +-- return control to the overworld after viewing a badge (#454). +package.path = "./?.lua;./?/init.lua;" .. package.path +if not _G.love then _G.love = require("tests.love_stub") end + +local S = require("tests.harness").suite("parity Cerulean Badge House") +local eq = S.eq +local script = require("data.scripts.flavor.cerulean_badge_house") + .CERULEAN_BADGE_HOUSE.talk.TEXT_CERULEANBADGEHOUSE_MIDDLE_AGED_MAN + +local states = {} +local game = { + data = { + items = { + BOULDERBADGE = { name = "BOULDERBADGE" }, + CASCADEBADGE = { name = "CASCADEBADGE" }, + THUNDERBADGE = { name = "THUNDERBADGE" }, + RAINBOWBADGE = { name = "RAINBOWBADGE" }, + SOULBADGE = { name = "SOULBADGE" }, + MARSHBADGE = { name = "MARSHBADGE" }, + VOLCANOBADGE = { name = "VOLCANOBADGE" }, + EARTHBADGE = { name = "EARTHBADGE" }, + }, + text = { + _CeruleanBadgeHouseMiddleAgedManText = "", + _CeruleanBadgeHouseMiddleAgedManWhichBadgeText = "", + _CeruleanBadgeHouseBoulderBadgeText = "", + _CeruleanBadgeHouseMiddleAgedManVisitAnyTimeText = "", + }, + }, + save = { player = { name = "RED" } }, + input = { wasPressed = function(_, key) return key == "b" end }, + stack = { + push = function(_, state) states[#states + 1] = state end, + pop = function() return table.remove(states) end, + top = function() return states[#states] end, + }, +} + +local done = false +script(game, nil, nil, function() done = true end) + +local function dismissText() + local text = game.stack:pop() + text.onDone() +end + +-- Greeting -> prompt -> first menu -> badge description -> prompt -> second menu. +dismissText() +dismissText() +local firstMenu = game.stack:top() +firstMenu.onChoose(firstMenu.items[1]) +dismissText() +dismissText() + +-- B follows ListMenu's real cancellation path: pop menu, then show goodbye. +game.stack:top():update(0) +dismissText() + +eq(#states, 0, "backing out after viewing a badge leaves no menu behind") +eq(done, true, "backing out after viewing a badge completes the NPC script") + +S.finish() diff --git a/tests/run_tests.lua b/tests/run_tests.lua index 1590c258..ff5f7ef0 100644 --- a/tests/run_tests.lua +++ b/tests/run_tests.lua @@ -3274,6 +3274,7 @@ runSuites(orderedGlob("tests/parity_*.lua", { "tests/parity_C.lua", "tests/parity_K.lua", "tests/parity_L.lua", "tests/parity_H.lua", "tests/parity_G.lua", "tests/parity_I_M.lua", "tests/parity_B.lua", "tests/parity_J.lua", "tests/parity_A.lua", + "tests/parity_cerulean_badge_house.lua", "tests/parity_flavor.lua", "tests/parity_trainer_sight.lua", "tests/parity_static.lua", "tests/parity_trashcans.lua", "tests/parity_hof.lua", "tests/parity_trade_gift.lua", From bebfc87cd4601f344151244fc8eae25787f211bc Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Fri, 31 Jul 2026 08:07:23 -0400 Subject: [PATCH 3/3] yellow palette / pikachu yellow --- data/palettes_yellow.lua | 686 ++++++++++++++++++++++++++++++ docs/new-features.md | 18 +- src/battle/BattleState.lua | 4 +- src/render/PaletteFX.lua | 136 ++++-- src/world/OverworldController.lua | 22 +- tools/extract/palettes.py | 101 ++++- 6 files changed, 918 insertions(+), 49 deletions(-) create mode 100644 data/palettes_yellow.lua diff --git a/data/palettes_yellow.lua b/data/palettes_yellow.lua new file mode 100644 index 00000000..a7728db4 --- /dev/null +++ b/data/palettes_yellow.lua @@ -0,0 +1,686 @@ +-- Generated by tools/build_data.py. DO NOT EDIT. +-- Pokemon Yellow palettes (COLORS=OG YELLOW): SuperPalettes + +-- CGBBasePalettes (authentic GBC look) as 4 8-bit RGB colors +-- per palette (color 0 first), plus MonsterPalettes. +return { + cgbBase = { + ["0F"] = { + { 255, 255, 255 }, + { 107, 8, 255 }, + { 0, 74, 255 }, + { 8, 8, 8 }, + }, + BADGE = { + { 255, 255, 255 }, + { 189, 66, 0 }, + { 140, 115, 90 }, + { 25, 25, 25 }, + }, + BLACK = { + { 255, 255, 255 }, + { 25, 25, 25 }, + { 25, 25, 25 }, + { 25, 25, 25 }, + }, + BLUEMON = { + { 255, 255, 255 }, + { 132, 148, 255 }, + { 0, 8, 206 }, + { 25, 25, 25 }, + }, + BROWNMON = { + { 255, 255, 255 }, + { 239, 148, 82 }, + { 140, 74, 41 }, + { 25, 25, 25 }, + }, + CAVE = { + { 255, 255, 255 }, + { 189, 66, 0 }, + { 140, 115, 90 }, + { 25, 25, 25 }, + }, + CELADON = { + { 255, 255, 255 }, + { 41, 255, 41 }, + { 90, 189, 255 }, + { 25, 25, 25 }, + }, + CERULEAN = { + { 255, 255, 255 }, + { 41, 66, 255 }, + { 90, 189, 255 }, + { 25, 25, 25 }, + }, + CINNABAR = { + { 255, 255, 255 }, + { 255, 66, 66 }, + { 90, 189, 255 }, + { 25, 25, 25 }, + }, + CYANMON = { + { 255, 255, 255 }, + { 132, 214, 255 }, + { 0, 140, 255 }, + { 25, 25, 25 }, + }, + FUCHSIA = { + { 255, 255, 255 }, + { 255, 123, 123 }, + { 90, 189, 255 }, + { 25, 25, 25 }, + }, + GAMEFREAK = { + { 255, 255, 255 }, + { 255, 156, 0 }, + { 156, 156, 0 }, + { 25, 25, 25 }, + }, + GRAYMON = { + { 255, 255, 255 }, + { 165, 189, 82 }, + { 90, 90, 41 }, + { 25, 25, 25 }, + }, + GREENBAR = { + { 255, 255, 255 }, + { 255, 255, 0 }, + { 0, 255, 0 }, + { 25, 25, 25 }, + }, + GREENMON = { + { 255, 255, 255 }, + { 140, 255, 90 }, + { 8, 181, 49 }, + { 25, 25, 25 }, + }, + INDIGO = { + { 255, 255, 255 }, + { 90, 66, 255 }, + { 90, 189, 255 }, + { 25, 25, 25 }, + }, + LAVENDER = { + { 255, 255, 255 }, + { 206, 33, 255 }, + { 90, 189, 255 }, + { 25, 25, 25 }, + }, + LOGO1 = { + { 255, 255, 255 }, + { 255, 255, 0 }, + { 255, 0, 0 }, + { 255, 0, 0 }, + }, + LOGO2 = { + { 255, 255, 255 }, + { 255, 255, 0 }, + { 58, 58, 206 }, + { 0, 0, 140 }, + }, + MEWMON = { + { 255, 255, 255 }, + { 255, 255, 0 }, + { 255, 8, 8 }, + { 25, 25, 25 }, + }, + PALLET = { + { 255, 255, 255 }, + { 189, 140, 255 }, + { 90, 189, 255 }, + { 25, 25, 25 }, + }, + PEWTER = { + { 255, 255, 255 }, + { 148, 148, 123 }, + { 90, 189, 255 }, + { 25, 25, 25 }, + }, + PIKACHUS_BEACH = { + { 255, 255, 255 }, + { 255, 255, 0 }, + { 90, 189, 255 }, + { 25, 25, 25 }, + }, + PIKACHUS_BEACH_TITLE = { + { 255, 255, 255 }, + { 74, 74, 74 }, + { 255, 173, 0 }, + { 25, 25, 25 }, + }, + PIKACHU_PORTRAIT = { + { 255, 255, 255 }, + { 255, 148, 0 }, + { 156, 58, 8 }, + { 25, 25, 25 }, + }, + PINKMON = { + { 255, 255, 255 }, + { 255, 123, 148 }, + { 255, 0, 49 }, + { 25, 25, 25 }, + }, + PURPLEMON = { + { 255, 255, 255 }, + { 206, 123, 255 }, + { 156, 0, 181 }, + { 25, 25, 25 }, + }, + REDBAR = { + { 255, 255, 255 }, + { 255, 255, 0 }, + { 255, 0, 0 }, + { 25, 25, 25 }, + }, + REDMON = { + { 255, 255, 255 }, + { 255, 140, 0 }, + { 255, 0, 0 }, + { 25, 25, 25 }, + }, + ROUTE = { + { 255, 255, 255 }, + { 132, 255, 33 }, + { 90, 189, 255 }, + { 25, 25, 25 }, + }, + SAFFRON = { + { 255, 255, 255 }, + { 255, 255, 0 }, + { 90, 189, 255 }, + { 25, 25, 25 }, + }, + SLOTS1 = { + { 255, 255, 255 }, + { 206, 8, 255 }, + { 255, 0, 0 }, + { 25, 25, 25 }, + }, + SLOTS2 = { + { 255, 255, 255 }, + { 255, 33, 156 }, + { 255, 255, 0 }, + { 25, 25, 25 }, + }, + SLOTS3 = { + { 255, 255, 255 }, + { 66, 255, 0 }, + { 255, 255, 0 }, + { 25, 25, 25 }, + }, + SLOTS4 = { + { 255, 255, 255 }, + { 0, 255, 255 }, + { 255, 255, 0 }, + { 25, 25, 25 }, + }, + TOWNMAP = { + { 255, 255, 255 }, + { 0, 173, 255 }, + { 82, 230, 0 }, + { 8, 8, 8 }, + }, + VERMILION = { + { 255, 255, 255 }, + { 255, 156, 0 }, + { 90, 189, 255 }, + { 25, 25, 25 }, + }, + VIRIDIAN = { + { 255, 255, 255 }, + { 156, 255, 0 }, + { 90, 189, 255 }, + { 25, 25, 25 }, + }, + YELLOWBAR = { + { 255, 255, 255 }, + { 255, 255, 0 }, + { 255, 148, 0 }, + { 25, 25, 25 }, + }, + YELLOWMON = { + { 255, 255, 255 }, + { 255, 255, 0 }, + { 230, 115, 0 }, + { 25, 25, 25 }, + }, + }, + order = { + "ROUTE", + "PALLET", + "VIRIDIAN", + "PEWTER", + "CERULEAN", + "LAVENDER", + "VERMILION", + "CELADON", + "FUCHSIA", + "CINNABAR", + "INDIGO", + "SAFFRON", + "TOWNMAP", + "LOGO1", + "LOGO2", + "0F", + "MEWMON", + "BLUEMON", + "REDMON", + "CYANMON", + "PURPLEMON", + "BROWNMON", + "GREENMON", + "PINKMON", + "YELLOWMON", + "GRAYMON", + "SLOTS1", + "SLOTS2", + "SLOTS3", + "SLOTS4", + "BLACK", + "GREENBAR", + "YELLOWBAR", + "REDBAR", + "BADGE", + "CAVE", + "GAMEFREAK", + "PIKACHUS_BEACH", + "PIKACHU_PORTRAIT", + "PIKACHUS_BEACH_TITLE", + }, + palettes = { + ["0F"] = { + { 255, 255, 247 }, + { 197, 165, 247 }, + { 90, 165, 247 }, + { 25, 16, 16 }, + }, + BADGE = { + { 255, 255, 247 }, + { 165, 123, 90 }, + { 181, 173, 165 }, + { 49, 49, 49 }, + }, + BLACK = { + { 255, 255, 247 }, + { 49, 49, 49 }, + { 49, 49, 49 }, + { 49, 49, 49 }, + }, + BLUEMON = { + { 255, 255, 247 }, + { 173, 181, 255 }, + { 74, 82, 165 }, + { 49, 49, 49 }, + }, + BROWNMON = { + { 255, 255, 247 }, + { 214, 189, 148 }, + { 148, 115, 82 }, + { 49, 49, 49 }, + }, + CAVE = { + { 255, 255, 247 }, + { 165, 123, 90 }, + { 181, 173, 165 }, + { 49, 49, 49 }, + }, + CELADON = { + { 255, 255, 247 }, + { 181, 255, 181 }, + { 189, 222, 255 }, + { 49, 49, 49 }, + }, + CERULEAN = { + { 255, 255, 247 }, + { 181, 189, 255 }, + { 189, 222, 255 }, + { 49, 49, 49 }, + }, + CINNABAR = { + { 255, 255, 247 }, + { 255, 123, 115 }, + { 189, 222, 255 }, + { 49, 49, 49 }, + }, + CYANMON = { + { 255, 255, 247 }, + { 214, 230, 255 }, + { 58, 197, 230 }, + { 49, 49, 49 }, + }, + FUCHSIA = { + { 255, 255, 247 }, + { 255, 214, 214 }, + { 189, 222, 255 }, + { 49, 49, 49 }, + }, + GAMEFREAK = { + { 255, 255, 247 }, + { 230, 197, 115 }, + { 165, 165, 90 }, + { 49, 49, 49 }, + }, + GRAYMON = { + { 255, 255, 247 }, + { 206, 206, 148 }, + { 132, 132, 115 }, + { 49, 49, 49 }, + }, + GREENBAR = { + { 255, 255, 247 }, + { 255, 255, 156 }, + { 0, 173, 0 }, + { 49, 49, 49 }, + }, + GREENMON = { + { 255, 255, 247 }, + { 197, 230, 148 }, + { 107, 173, 123 }, + { 49, 49, 49 }, + }, + INDIGO = { + { 255, 255, 247 }, + { 140, 140, 206 }, + { 189, 222, 255 }, + { 49, 49, 49 }, + }, + LAVENDER = { + { 255, 255, 247 }, + { 222, 189, 239 }, + { 189, 222, 255 }, + { 49, 49, 49 }, + }, + LOGO1 = { + { 255, 255, 247 }, + { 247, 247, 140 }, + { 173, 0, 33 }, + { 173, 0, 33 }, + }, + LOGO2 = { + { 255, 255, 247 }, + { 247, 247, 140 }, + { 148, 148, 197 }, + { 58, 58, 132 }, + }, + MEWMON = { + { 255, 255, 247 }, + { 255, 247, 181 }, + { 222, 132, 132 }, + { 49, 49, 49 }, + }, + PALLET = { + { 255, 255, 247 }, + { 230, 222, 255 }, + { 189, 222, 255 }, + { 49, 49, 49 }, + }, + PEWTER = { + { 255, 255, 247 }, + { 189, 189, 181 }, + { 189, 222, 255 }, + { 49, 49, 49 }, + }, + PIKACHUS_BEACH = { + { 255, 255, 247 }, + { 255, 247, 181 }, + { 189, 222, 255 }, + { 49, 49, 49 }, + }, + PIKACHUS_BEACH_TITLE = { + { 255, 255, 247 }, + { 132, 132, 132 }, + { 255, 206, 74 }, + { 49, 49, 49 }, + }, + PIKACHU_PORTRAIT = { + { 255, 255, 247 }, + { 230, 189, 74 }, + { 148, 115, 82 }, + { 49, 49, 49 }, + }, + PINKMON = { + { 255, 255, 247 }, + { 255, 197, 214 }, + { 255, 148, 173 }, + { 49, 49, 49 }, + }, + PURPLEMON = { + { 255, 255, 247 }, + { 222, 181, 247 }, + { 181, 123, 189 }, + { 49, 49, 49 }, + }, + REDBAR = { + { 255, 255, 247 }, + { 255, 255, 156 }, + { 214, 74, 49 }, + { 49, 49, 49 }, + }, + REDMON = { + { 255, 255, 247 }, + { 255, 197, 90 }, + { 214, 74, 49 }, + { 49, 49, 49 }, + }, + ROUTE = { + { 255, 255, 247 }, + { 189, 214, 156 }, + { 189, 222, 255 }, + { 49, 49, 49 }, + }, + SAFFRON = { + { 255, 255, 247 }, + { 255, 255, 156 }, + { 189, 222, 255 }, + { 49, 49, 49 }, + }, + SLOTS1 = { + { 255, 255, 247 }, + { 222, 181, 247 }, + { 214, 74, 49 }, + { 49, 49, 49 }, + }, + SLOTS2 = { + { 255, 255, 247 }, + { 255, 189, 214 }, + { 239, 239, 66 }, + { 49, 49, 49 }, + }, + SLOTS3 = { + { 255, 255, 247 }, + { 189, 255, 165 }, + { 239, 239, 66 }, + { 49, 49, 49 }, + }, + SLOTS4 = { + { 255, 255, 247 }, + { 189, 239, 255 }, + { 239, 239, 66 }, + { 49, 49, 49 }, + }, + TOWNMAP = { + { 255, 255, 247 }, + { 165, 214, 255 }, + { 140, 189, 82 }, + { 25, 16, 16 }, + }, + VERMILION = { + { 255, 255, 247 }, + { 255, 206, 132 }, + { 189, 222, 255 }, + { 49, 49, 49 }, + }, + VIRIDIAN = { + { 255, 255, 247 }, + { 214, 255, 173 }, + { 189, 222, 255 }, + { 49, 49, 49 }, + }, + YELLOWBAR = { + { 255, 255, 247 }, + { 255, 255, 156 }, + { 230, 189, 74 }, + { 49, 49, 49 }, + }, + YELLOWMON = { + { 255, 255, 247 }, + { 255, 255, 156 }, + { 230, 189, 74 }, + { 49, 49, 49 }, + }, + }, + pokemon = { + ABRA = "YELLOWMON", + AERODACTYL = "GRAYMON", + ALAKAZAM = "YELLOWMON", + ARBOK = "PURPLEMON", + ARCANINE = "REDMON", + ARTICUNO = "BLUEMON", + BEEDRILL = "YELLOWMON", + BELLSPROUT = "GREENMON", + BLASTOISE = "CYANMON", + BULBASAUR = "GREENMON", + BUTTERFREE = "CYANMON", + CATERPIE = "GREENMON", + CHANSEY = "PINKMON", + CHARIZARD = "REDMON", + CHARMANDER = "REDMON", + CHARMELEON = "REDMON", + CLEFABLE = "PINKMON", + CLEFAIRY = "PINKMON", + CLOYSTER = "GRAYMON", + CUBONE = "GRAYMON", + DEWGONG = "BLUEMON", + DIGLETT = "BROWNMON", + DITTO = "GRAYMON", + DODRIO = "BROWNMON", + DODUO = "BROWNMON", + DRAGONAIR = "BLUEMON", + DRAGONITE = "BROWNMON", + DRATINI = "GRAYMON", + DROWZEE = "YELLOWMON", + DUGTRIO = "BROWNMON", + EEVEE = "GRAYMON", + EKANS = "PURPLEMON", + ELECTABUZZ = "YELLOWMON", + ELECTRODE = "YELLOWMON", + EXEGGCUTE = "PINKMON", + EXEGGUTOR = "GREENMON", + FARFETCHD = "BROWNMON", + FEAROW = "BROWNMON", + FLAREON = "REDMON", + GASTLY = "PURPLEMON", + GENGAR = "PURPLEMON", + GEODUDE = "GRAYMON", + GLOOM = "REDMON", + GOLBAT = "BLUEMON", + GOLDEEN = "REDMON", + GOLDUCK = "CYANMON", + GOLEM = "GRAYMON", + GRAVELER = "GRAYMON", + GRIMER = "PURPLEMON", + GROWLITHE = "BROWNMON", + GYARADOS = "BLUEMON", + HAUNTER = "PURPLEMON", + HITMONCHAN = "BROWNMON", + HITMONLEE = "BROWNMON", + HORSEA = "CYANMON", + HYPNO = "YELLOWMON", + IVYSAUR = "GREENMON", + JIGGLYPUFF = "PINKMON", + JOLTEON = "YELLOWMON", + JYNX = "MEWMON", + KABUTO = "BROWNMON", + KABUTOPS = "BROWNMON", + KADABRA = "YELLOWMON", + KAKUNA = "YELLOWMON", + KANGASKHAN = "BROWNMON", + KINGLER = "REDMON", + KOFFING = "PURPLEMON", + KRABBY = "REDMON", + LAPRAS = "CYANMON", + LICKITUNG = "PINKMON", + MACHAMP = "GRAYMON", + MACHOKE = "GRAYMON", + MACHOP = "GRAYMON", + MAGIKARP = "REDMON", + MAGMAR = "REDMON", + MAGNEMITE = "GRAYMON", + MAGNETON = "GRAYMON", + MANKEY = "BROWNMON", + MAROWAK = "GRAYMON", + MEOWTH = "YELLOWMON", + METAPOD = "GREENMON", + MEW = "MEWMON", + MEWTWO = "MEWMON", + MOLTRES = "REDMON", + MR_MIME = "PINKMON", + MUK = "PURPLEMON", + NIDOKING = "PURPLEMON", + NIDOQUEEN = "BLUEMON", + NIDORAN_F = "BLUEMON", + NIDORAN_M = "PURPLEMON", + NIDORINA = "BLUEMON", + NIDORINO = "PURPLEMON", + NINETALES = "YELLOWMON", + ODDISH = "GREENMON", + OMANYTE = "BLUEMON", + OMASTAR = "BLUEMON", + ONIX = "GRAYMON", + PARAS = "REDMON", + PARASECT = "REDMON", + PERSIAN = "YELLOWMON", + PIDGEOT = "BROWNMON", + PIDGEOTTO = "BROWNMON", + PIDGEY = "BROWNMON", + PIKACHU = "YELLOWMON", + PINSIR = "BROWNMON", + POLIWAG = "BLUEMON", + POLIWHIRL = "BLUEMON", + POLIWRATH = "BLUEMON", + PONYTA = "REDMON", + PORYGON = "GRAYMON", + PRIMEAPE = "BROWNMON", + PSYDUCK = "YELLOWMON", + RAICHU = "YELLOWMON", + RAPIDASH = "REDMON", + RATICATE = "GRAYMON", + RATTATA = "GRAYMON", + RHYDON = "GRAYMON", + RHYHORN = "GRAYMON", + SANDSHREW = "BROWNMON", + SANDSLASH = "BROWNMON", + SCYTHER = "GREENMON", + SEADRA = "CYANMON", + SEAKING = "REDMON", + SEEL = "BLUEMON", + SHELLDER = "GRAYMON", + SLOWBRO = "PINKMON", + SLOWPOKE = "PINKMON", + SNORLAX = "PINKMON", + SPEAROW = "BROWNMON", + SQUIRTLE = "CYANMON", + STARMIE = "GRAYMON", + STARYU = "REDMON", + TANGELA = "BLUEMON", + TAUROS = "GRAYMON", + TENTACOOL = "CYANMON", + TENTACRUEL = "CYANMON", + VAPOREON = "CYANMON", + VENOMOTH = "PURPLEMON", + VENONAT = "PURPLEMON", + VENUSAUR = "GREENMON", + VICTREEBEL = "GREENMON", + VILEPLUME = "REDMON", + VOLTORB = "YELLOWMON", + VULPIX = "REDMON", + WARTORTLE = "CYANMON", + WEEDLE = "YELLOWMON", + WEEPINBELL = "GREENMON", + WEEZING = "PURPLEMON", + WIGGLYTUFF = "PINKMON", + ZAPDOS = "YELLOWMON", + ZUBAT = "BLUEMON", + }, + source = "pokeyellow data/sgb/sgb_palettes.asm + data/pokemon/palettes.asm", +} diff --git a/docs/new-features.md b/docs/new-features.md index 42d72087..3f33a59d 100644 --- a/docs/new-features.md +++ b/docs/new-features.md @@ -86,14 +86,20 @@ Game Boy equivalent: ## Colors mode The `2` key (and the Options menu COLORS row) cycles the display mode -through **OG RED → SGB → ADVANCED → OG → OG INV → SGB INV → CLASSIC → OG RED**. +through **OG RED → SGB → ADVANCED → OG → OG INV → SGB INV → CLASSIC → OG RED** +(on Blue the first slot labels **OG BLUE**; on Yellow, **OG YELLOW**). The first three are the real colorizations; the rest are DMG-shade novelties: -- **OG RED**: the Game Boy Color boot-ROM look for Pokemon Red -- one global - red BG palette + one green OBJ palette, every map, no per-map variation - (Pokemon Red has no CGB code, so on a GBC the boot ROM colors it globally). - The player/NPCs stay green over the red terrain via the OBP bake + - post-zone redraw (`PaletteFX.GBC_BG` / `GBC_OBJ`). +- **OG RED** / **OG BLUE**: the Game Boy Color boot-ROM look for that cart -- + one global BG palette + one OBJ palette, every map, no per-map variation + (Red/Blue ship no CGB code, so on a GBC the boot ROM colors them globally). + The player/NPCs keep the boot-ROM OBJ color over the terrain via the OBP + bake + post-zone redraw (`PaletteFX.GBC_BG` / `GBC_OBJ`, or Blue's blue/pink + pair). +- **OG YELLOW** (Yellow playthrough, same `ogred` save id): Pokemon Yellow's + authentic GBC look from `CGBBasePalettes` (`data/palettes_yellow.lua`, + sourced from pret/pokeyellow). Per-map / per-species colors, not a single + boot-ROM ramp -- Yellow was CGB-enhanced. - **SGB** (default): the per-map Super Game Boy region palettes (`data/sgb/sgb_palettes.asm`). Sprites tint with the region palette, as on real SGB. (This is the mode formerly mislabeled "GBC".) diff --git a/src/battle/BattleState.lua b/src/battle/BattleState.lua index a2ae60d5..e9f96024 100644 --- a/src/battle/BattleState.lua +++ b/src/battle/BattleState.lua @@ -4483,7 +4483,9 @@ function BattleState:sgbBattlePals() -- true white, which is what drew a white box around each pic on the pink -- field. Snap every zone's color 0/3 to the global GBC white/black; the -- mid shades (and the green bar the user prefers) stay untouched. - if PaletteFX.mode == "ogred" then + -- Boot-ROM OG only (Red/Blue): snap zone paper to the global GBC white/black. + -- OG YELLOW keeps each CGBBasePalettes endpoint (already near-white / near-black). + if PaletteFX.mode == "ogred" and not require("src.core.GameVersion").isYellow() then local white, black = PaletteFX.GBC_BG[1], PaletteFX.GBC_BG[4] for i = 0, 3 do local c = out[i] diff --git a/src/render/PaletteFX.lua b/src/render/PaletteFX.lua index eceb6e9f..7990f88b 100644 --- a/src/render/PaletteFX.lua +++ b/src/render/PaletteFX.lua @@ -5,11 +5,13 @@ -- then drawn once per zone through a shader that remaps the four DMG -- shades to that zone's palette. -- --- Port display option: COLORS (OG RED / SGB / ADVANCED / OG / OG INV / --- SGB INV / CLASSIC) transforms every zone's palette at send time via --- effectiveColors. ADVANCED (the `redpp` id below) swaps the named-palette --- pack for pokered-gbc SuperPalettes (data/palettes_gbc.lua), including --- per-species mon colors. +-- Port display option: COLORS (OG RED / OG BLUE / OG YELLOW / SGB / +-- ADVANCED / OG / OG INV / SGB INV / CLASSIC) transforms every zone's +-- palette at send time via effectiveColors. ADVANCED (the `redpp` id +-- below) swaps the named-palette pack for pokered-gbc SuperPalettes +-- (data/palettes_gbc.lua), including per-species mon colors. OG YELLOW +-- (Yellow playthrough + `ogred` id) uses pokeyellow CGBBasePalettes +-- (data/palettes_yellow.lua / ROM cgbBase). local GameVersion = require("src.core.GameVersion") @@ -17,10 +19,11 @@ local PaletteFX = {} local shader -- false = unavailable (headless / no shader support) local gbcPack -- false = missing; nil = not loaded yet +local yellowPack -- false = missing; nil = not loaded yet -- Cycle order matches OptionsMenu / hotkey 2. The three real colorizations --- come first (OG RED = GBC hardware, SGB = per-map Super Game Boy, ADVANCED = --- pokered-gbc per-tile), then the DMG-shade novelty modes. +-- come first (OG RED/BLUE/YELLOW = GBC hardware, SGB = per-map Super Game Boy, +-- ADVANCED = pokered-gbc per-tile), then the DMG-shade novelty modes. PaletteFX.MODES = { "ogred", "gbc", "redpp", "og", "og_inv", "gbc_inv", "classic" } -- `gbc`/`gbc_inv`/`redpp` keep their save-value ids for back-compat while -- their LABELS say what the mode actually is: "SGB"/"SGB INV" because the old @@ -117,14 +120,19 @@ PaletteFX.GBC_OBJ_BLUE = { } -- The active game's OG boot-ROM background palette: blue for a Blue --- playthrough, red otherwise. White (index 1) and black (index 4) are --- identical across versions, so callers that only touch the endpoints --- (e.g. BattleState's zone white/black snap) need no version branch. --- Yellow is CGB-enhanced (pokeyellow CGBBasePalettes) and has no extracted --- boot-ROM auto-palette here; keep the Red ramp -- never Blue's GBC_BG_BLUE. +-- playthrough, red for Red. White (index 1) and black (index 4) are +-- identical across Red/Blue, so callers that only touch the endpoints +-- (e.g. BattleState's zone white/black snap) need no version branch there. +-- Yellow is CGB-enhanced (pokeyellow CGBBasePalettes): named zones go through +-- pal() / usesYellowCgb(), and ogBg() falls back to CGBBase PAL_ROUTE for any +-- remaining whole-screen callers -- never Blue's GBC_BG_BLUE. function PaletteFX.ogBg() if GameVersion.isBlue() then return PaletteFX.GBC_BG_BLUE end - if GameVersion.isYellow() then return PaletteFX.GBC_BG end + if GameVersion.isYellow() then + local y = PaletteFX.yellowPack() + local route = y and y.cgbBase and y.cgbBase.ROUTE + if route then return route end + end return PaletteFX.GBC_BG end @@ -133,14 +141,12 @@ end -- version-distinct cache-group string, because SpriteRenderer.getObpImage keys -- its baked-image cache by (image path, group): a shared group would collide a -- Red bake with a Blue one and one version would show the other's colors. --- Yellow: same Red OBJ green as above until Yellow-specific tables land. +-- Yellow OG does not bake a boot-ROM OBJ (usesSpriteObp is false); this path +-- is unused there and kept as Red green only as a safe leftover. function PaletteFX.ogObj() if GameVersion.isBlue() then return PaletteFX.darkObp(PaletteFX.GBC_OBJ_BLUE, "gbcobj_blue") end - if GameVersion.isYellow() then - return PaletteFX.darkObp(PaletteFX.GBC_OBJ, "gbcobj") - end return PaletteFX.darkObp(PaletteFX.GBC_OBJ, "gbcobj") end @@ -271,13 +277,31 @@ function PaletteFX.gbcPack() return gbcPack or nil end +-- pokeyellow SuperPalettes + CGBBasePalettes (committed; not wiped by import). +-- ROM import may also stamp palettes.cgbBase; yellowCgbNamedPal prefers that. +function PaletteFX.yellowPack() + if yellowPack == nil then + local ok, pack = pcall(require, "data.palettes_yellow") + yellowPack = ok and pack or false + end + return yellowPack or nil +end + function PaletteFX.usesGbcPack(mode) mode = mode or PaletteFX.mode return mode == "redpp" end +-- Yellow's authentic GBC look is CGBBasePalettes (per-map), not a boot-ROM +-- auto-palette. The shared `ogred` save id wears that table on a Yellow +-- playthrough and labels itself "OG YELLOW". +function PaletteFX.usesYellowCgb(mode) + mode = mode or PaletteFX.mode + return GameVersion.isYellow() and mode == "ogred" +end + -- Whether the active mode bakes a per-OBJ palette onto overworld sprites --- (the OBP bake + post-zone redraw path). OG RED alone does: the Game Boy +-- (the OBP bake + post-zone redraw path). OG RED / OG BLUE do: the Game Boy -- Color boot ROM hands the game one global object palette (PaletteFX.ogObj -- -- green over Red's red background, pink over Blue's blue background), so on -- that machine the player and NPCs carry a fixed object color instead of @@ -288,6 +312,10 @@ end -- regression. Terrain is unaffected -- pal() below still hands SGB its per-map -- BG palette; only OG RED short-circuits BG to the one global red palette. -- +-- OG YELLOW does NOT. Yellow ships CGB code and colors regions via +-- CGBBasePalettes the same way SGB mode colors SuperPalettes, so sprites tint +-- with the map zone (usesSpriteObp stays off). +-- -- SGB does NOT. The Super Game Boy colorizes the composited DMG picture it is -- handed and cannot tell an OBJ pixel from a BG one; pokered never sends the -- OBJ_TRN packet that would enable SGB sprite mode (data/sgb/sgb_packets.asm @@ -306,7 +334,7 @@ end -- usesGbcPack() path in SpriteRenderer instead. function PaletteFX.usesSpriteObp(mode) mode = mode or PaletteFX.mode - return mode == "ogred" + return mode == "ogred" and not GameVersion.isYellow() end -- ------- post-zone sprite redraw (OG RED) @@ -363,8 +391,8 @@ end -- read these from the ROM-imported table or the title ribbon stays red -- and the Game Corner reels keep Red's pink (issue #128). -- Yellow is intentionally NOT in BLUE_VERSIONED: skip Blue LOGO1/SLOTS* --- recolors. When CGBBasePalettes were imported (palettes.cgbBase), Yellow --- prefers those over SGB SuperPalettes for named zones. +-- recolors. OG YELLOW (usesYellowCgb) reads CGBBasePalettes; SGB mode on +-- Yellow keeps SuperPalettes. local BLUE_VERSIONED = { LOGO1 = true, SLOTS2 = true, SLOTS3 = true, SLOTS4 = true, } @@ -376,31 +404,42 @@ end local function yellowCgbNamedPal(data, name) local p = data and data.palettes - return p and p.cgbBase and p.cgbBase[name] + local fromRom = p and p.cgbBase and p.cgbBase[name] + if fromRom then return fromRom end + local y = PaletteFX.yellowPack() + return y and y.cgbBase and y.cgbBase[name] or nil end -- named palette from the active pack (nil on stale builds / missing name). -- RED++ falls back to the ROM pack for names the gbc table omits (rare). --- OG RED short-circuits EVERY name to the one global GBC boot-ROM BG palette --- (the hardware had a single BGP for the whole game), so terrain zones, --- battle HP bars / text, and menu boxes all come out red -- everything a --- background tile drew. Objects do not come through here (they bake --- GBC_OBJ green), so this stays a BG-only hook. +-- OG RED / OG BLUE short-circuit EVERY name to the one global GBC boot-ROM +-- BG palette (the hardware had a single BGP for the whole game), so terrain +-- zones, battle HP bars / text, and menu boxes all come out red/blue -- +-- everything a background tile drew. Objects do not come through here +-- (they bake GBC_OBJ), so this stays a BG-only hook. +-- OG YELLOW instead resolves each name through CGBBasePalettes. function PaletteFX.pal(data, name) - if PaletteFX.mode == "ogred" then return PaletteFX.ogBg() end + if PaletteFX.mode == "ogred" and not GameVersion.isYellow() then + return PaletteFX.ogBg() + end -- Blue-only ROM override for versioned SuperPals. Yellow (isYellow) and -- Red keep the active pack / Red-like path -- do not apply Blue recolors. if GameVersion.isBlue() and BLUE_VERSIONED[name] then local fromRom = romNamedPal(data, name) if fromRom then return fromRom end end - if GameVersion.isYellow() then + if PaletteFX.usesYellowCgb() then local fromCgb = yellowCgbNamedPal(data, name) if fromCgb then return fromCgb end end local p = PaletteFX.pack(data) local c = p and p.palettes[name] if c then return c end + if GameVersion.isYellow() then + local y = PaletteFX.yellowPack() + local yc = y and y.palettes and y.palettes[name] + if yc then return yc end + end if PaletteFX.usesGbcPack() then return romNamedPal(data, name) end @@ -413,14 +452,20 @@ end -- Transformed mon's pic is tinted gray, not the copied species' own -- SGB color). RED++ uses per-species pals from mon_palettes.asm. function PaletteFX.monPal(data, species, transformed) - -- OG RED: a battle mon pic is a BG tile on the Game Boy Color (drawn into - -- the tilemap, colored by BGP), so it wears the global red BG palette, not - -- a per-species one -- matching the hardware capture where both mons are - -- red/pink on the white field. - if PaletteFX.mode == "ogred" then return PaletteFX.ogBg() end + -- OG RED / OG BLUE: a battle mon pic is a BG tile on the Game Boy Color + -- (drawn into the tilemap, colored by BGP), so it wears the global boot-ROM + -- BG palette, not a per-species one -- matching the hardware capture where + -- both mons are red/pink (or blue/pink) on the white field. + -- OG YELLOW keeps per-species CGBBasePalettes (Yellow had real CGB code). + if PaletteFX.mode == "ogred" and not GameVersion.isYellow() then + return PaletteFX.ogBg() + end local p = PaletteFX.pack(data) if not p then return nil end if transformed then + if PaletteFX.usesYellowCgb() then + return PaletteFX.pal(data, "GRAYMON") + end return p.palettes.GRAYMON or (data and data.palettes and data.palettes.palettes.GRAYMON) end @@ -430,17 +475,32 @@ function PaletteFX.monPal(data, species, transformed) -- so fall back to it when the pack itself doesn't carry the name. local def = data and data.pokemon and data.pokemon[species] if def and def.palette then + if PaletteFX.usesYellowCgb() then + local yc = PaletteFX.pal(data, def.palette) + if yc then return yc end + end local pal = p.palettes[def.palette] or (data and data.palettes and data.palettes.palettes[def.palette]) if pal then return pal end end local name = p.pokemon[species] or "MEWMON" + if PaletteFX.usesYellowCgb() then + local yc = PaletteFX.pal(data, name) + if yc then return yc end + end local c = p.palettes[name] if c then return c end if PaletteFX.usesGbcPack() and data and data.palettes then name = data.palettes.pokemon[species] or "MEWMON" return data.palettes.palettes[name] end + if GameVersion.isYellow() then + local y = PaletteFX.yellowPack() + if y and y.pokemon then + name = y.pokemon[species] or "MEWMON" + return y.palettes and y.palettes[name] + end + end return nil end @@ -707,12 +767,10 @@ end function PaletteFX.modeLabel(mode) mode = mode or PaletteFX.mode - -- The GBC boot-ROM mode wears the running game's name: it is red for Red and - -- blue for Blue (see ogBg), so a Blue playthrough shows "OG BLUE". - -- Yellow still uses the Red boot-ROM ramp (no Yellow table yet), so keep - -- the "OG RED" label rather than inventing an "OG YELLOW" without colors. + -- The GBC hardware mode wears the running game's name: OG RED / OG BLUE + -- (boot-ROM auto-palette) or OG YELLOW (pokeyellow CGBBasePalettes). if mode == "ogred" and GameVersion.isBlue() then return "OG BLUE" end - if mode == "ogred" and GameVersion.isYellow() then return "OG RED" end + if mode == "ogred" and GameVersion.isYellow() then return "OG YELLOW" end return PaletteFX.MODE_LABELS[mode] or "GBC" end diff --git a/src/world/OverworldController.lua b/src/world/OverworldController.lua index 0ea2ac3e..1bd90ef0 100644 --- a/src/world/OverworldController.lua +++ b/src/world/OverworldController.lua @@ -550,9 +550,26 @@ end -- UI-pass palette (text boxes and menus tint with the current map). OG RED -- resolves every name to the one global red BG palette inside PaletteFX.pal, -- so this needs no mode-specific branch. +-- +-- TalkToPikachu's framed frontpic is the one exception: pokeyellow +-- LoadOverworldPikachuFrontpicPalettes loads the map pal as slot 0 and +-- PAL_PIKACHU_PORTRAIT as slot 1, then ATTR_BLK's the 5x5 pic at +-- (7,6)-(11,10) onto slot 1 (engine/gfx/palettes.asm:345-391). Without +-- that zone the pic wears the route/town palette and looks washed out. function OverworldState:sgbPalettes() local PaletteFX = require("src.render.PaletteFX") - return PaletteFX.wholeNamed(Game.data, self:paletteNameFor(self.map)) + local mapName = self:paletteNameFor(self.map) + if self.emote and self.emote.pikaPic then + local base = PaletteFX.pal(Game.data, mapName) + if not base then return nil end + local zones = { PaletteFX.whole(base) } + local portrait = PaletteFX.pal(Game.data, "PIKACHU_PORTRAIT") + if portrait then + zones[#zones + 1] = PaletteFX.zone(portrait, 7, 6, 11, 10) + end + return zones + end + return PaletteFX.wholeNamed(Game.data, mapName) end -- World-pass palette zones in world-canvas pixels: each visible map @@ -4479,7 +4496,8 @@ function OverworldState:drawUI() -- per-emotion frame gfx (gfx/pikachu/unknown_*) are not extracted, so the -- front pic stands in for every frame of the script; PikachuFollower -- .picLift lifts it on the runs that draw the alternate pose, and the - -- script's own duration times the beat (#407, #424). + -- script's own duration times the beat (#407, #424). Palette zone + -- PAL_PIKACHU_PORTRAIT covers (7,6)-(11,10) via sgbPalettes above. if self.emote and self.emote.pikaPic then require("src.render.Font").drawBox(6, 5, 7, 7) -- one image per path, cached: this draws every frame of the hold, and diff --git a/tools/extract/palettes.py b/tools/extract/palettes.py index 6be5a70e..bf67e387 100644 --- a/tools/extract/palettes.py +++ b/tools/extract/palettes.py @@ -21,6 +21,16 @@ Output: data/palettes_gbc.lua (committed; not wiped by ROM import) same shape, plus per-species palette entries (BULBASAUR, …), plus a `world` table (below) for true overworld tile/roof/sprite coloring. +Sources (pokeyellow / COLORS OG YELLOW): + data/sgb/sgb_palettes.asm SuperPalettes + CGBBasePalettes (Yellow is + CGB-enhanced; CGBBase is the authentic GBC look) + data/pokemon/palettes.asm MonsterPalettes (same shape as Red) + +Output: data/palettes_yellow.lua (committed; not wiped by ROM import) + palettes[NAME] = SuperPalettes RGB + cgbBase[NAME] = CGBBasePalettes RGB + pokemon[SPECIES] = NAME + The HP bar fill is GB color 2 of PAL_GREENBAR / PAL_YELLOWBAR / PAL_REDBAR; the thresholds live in home/palettes.asm GetHealthBarColor (>= 27 pixels green, >= 10 yellow, else red). @@ -536,6 +546,89 @@ def extract_gbc(pokered_gbc, out_path): return palettes, mon_pals +def _parse_pokeyellow_palette_tables(path): + """Parse SuperPalettes and CGBBasePalettes from pokeyellow sgb_palettes.asm.""" + tables = {"SuperPalettes": {}, "CGBBasePalettes": {}} + orders = {"SuperPalettes": [], "CGBBasePalettes": []} + current = None + for lineno, line in _read_raw(path): + s = line.strip() + if s.startswith("SuperPalettes:"): + current = "SuperPalettes" + continue + if s.startswith("CGBBasePalettes:"): + current = "CGBBasePalettes" + continue + if s.startswith("assert_table_length") or s.endswith(":") and " " not in s: + if s.endswith(":") and not s.startswith(("Super", "CGB")): + current = None + continue + if current is None: + continue + m = re.match(r"RGB\s+([\d,\s]+);\s*PAL_(\w+)", s) + if not m: + continue + nums = [n for n in re.split(r"[,\s]+", m.group(1).strip()) if n] + if len(nums) != 12: + util.die(f"{path}:{lineno}: expected 12 components, got {len(nums)}") + name = m.group(2) + tables[current][name] = _rgb8(nums) + orders[current].append(name) + return tables, orders + + +def extract_yellow(pokeyellow, out_path): + """Extract Yellow SuperPalettes + CGBBasePalettes for COLORS=OG YELLOW.""" + pal_path = os.path.join(pokeyellow, "data/sgb/sgb_palettes.asm") + mon_path = os.path.join(pokeyellow, "data/pokemon/palettes.asm") + tables, orders = _parse_pokeyellow_palette_tables(pal_path) + palettes = tables["SuperPalettes"] + cgb_base = tables["CGBBasePalettes"] + order = orders["SuperPalettes"] + if not palettes: + util.die(f"{pal_path}: SuperPalettes empty") + if not cgb_base: + util.die(f"{pal_path}: CGBBasePalettes empty") + if orders["CGBBasePalettes"] != order: + util.die(f"{pal_path}: CGBBasePalettes order differs from SuperPalettes") + + mon_pals = {} + in_table = False + for lineno, line in _read_raw(mon_path): + s = line.strip() + if s.startswith("MonsterPalettes:"): + in_table = True + continue + if not in_table: + continue + m = re.match(r"db\s+PAL_(\w+)\s*;\s*(\w+)", s) + if not m: + continue + pal, species = m.group(1), m.group(2) + if pal not in palettes: + util.die(f"{mon_path}:{lineno}: unknown palette PAL_{pal}") + if species != "MISSINGNO": + mon_pals[species] = pal + + if len(mon_pals) != 151: + util.die(f"MonsterPalettes parsed {len(mon_pals)} species (want 151)") + for name in ("MEWMON", "GREENBAR", "YELLOWBAR", "REDBAR", "ROUTE"): + if name not in cgb_base: + util.die(f"{pal_path}: CGBBase PAL_{name} missing") + + util.write_lua( + out_path, + {"source": "pokeyellow data/sgb/sgb_palettes.asm + data/pokemon/palettes.asm", + "palettes": palettes, + "cgbBase": cgb_base, + "order": order, + "pokemon": mon_pals}, + header="Pokemon Yellow palettes (COLORS=OG YELLOW): SuperPalettes +\n" + "CGBBasePalettes (authentic GBC look) as 4 8-bit RGB colors\n" + "per palette (color 0 first), plus MonsterPalettes.") + return palettes, cgb_base, mon_pals + + def main(argv=None): parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) sub = parser.add_subparsers(dest="cmd", required=True) @@ -548,11 +641,17 @@ def main(argv=None): p_gbc.add_argument("--pokered-gbc", required=True) p_gbc.add_argument("--out", default="data/palettes_gbc.lua") + p_yel = sub.add_parser("yellow", help="extract pokeyellow CGBBasePalettes") + p_yel.add_argument("--pokeyellow", required=True) + p_yel.add_argument("--out", default="data/palettes_yellow.lua") + args = parser.parse_args(argv) if args.cmd == "vanilla": extract(args.pokered, args.out_dir) - else: + elif args.cmd == "gbc": extract_gbc(args.pokered_gbc, args.out) + else: + extract_yellow(args.pokeyellow, args.out) return 0