mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-27 08:51:27 +02:00
fix(yellow): address version:yellow issues and title pupil bake
Seed Mt Moon Super Nerd dialogue (#1743), play item-use heal jingles (#1635), use white fades for fly/teleport warps (#1644), add a Yellow-only Advanced palette overlay (#1639), parse Yellow Super Rod data including Safari Dragonair (#1074), and apply title rOBP0 when baking Pikachu eye OAM so pupils stay black with white glints.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
-- #1635: PrintItemUseTextAndRemoveItem items must request Heal_Ailment
|
||||
-- via extra.useJingle after the "X used Y!" line.
|
||||
--
|
||||
-- luajit tests/engine/item_use_jingle_bug1635.lua
|
||||
|
||||
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("item use Heal_Ailment jingle #1635")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local Data = require("src.core.Data")
|
||||
local ItemEffects = require("src.inventory.ItemEffects")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
|
||||
if not Data.maps then Data:load() end
|
||||
|
||||
local save = SaveData.newGame()
|
||||
save.player.name = "RED"
|
||||
|
||||
local function battleStub(playerName)
|
||||
return {
|
||||
kind = "wild",
|
||||
player = {
|
||||
name = playerName or "PIKACHU",
|
||||
stages = { attack = 0, defense = 0, speed = 0, special = 0, accuracy = 0 },
|
||||
mon = {},
|
||||
},
|
||||
enemy = { name = "RATTATA" },
|
||||
}
|
||||
end
|
||||
|
||||
local function assertJingle(itemId, battle)
|
||||
local result, msgs, extra = ItemEffects.use(Data, save, itemId, nil, battle)
|
||||
check(result == "consumed" or result == "consumed_escape",
|
||||
itemId .. " succeeds (" .. tostring(result) .. ")")
|
||||
check(extra and extra.useJingle, itemId .. " sets useJingle")
|
||||
check(msgs and msgs[1] and msgs[1]:find("used"),
|
||||
itemId .. " prints used line")
|
||||
return result, msgs, extra
|
||||
end
|
||||
|
||||
for _, id in ipairs({ "REPEL", "SUPER_REPEL", "MAX_REPEL" }) do
|
||||
assertJingle(id, nil)
|
||||
end
|
||||
|
||||
local b = battleStub()
|
||||
for _, id in ipairs({
|
||||
"X_ATTACK", "X_DEFEND", "X_SPEED", "X_SPECIAL",
|
||||
"X_ACCURACY", "DIRE_HIT", "GUARD_SPEC",
|
||||
}) do
|
||||
local _, _, extra = assertJingle(id, b)
|
||||
if id == "X_ATTACK" then
|
||||
check(extra.afterMessages and #extra.afterMessages > 0,
|
||||
"X_ATTACK follows used line with effect text")
|
||||
end
|
||||
end
|
||||
|
||||
local _, _, dollExtra = assertJingle("POKE_DOLL", battleStub())
|
||||
eq(dollExtra.afterMessages, nil, "Poké Doll is used-line only")
|
||||
|
||||
S.finish()
|
||||
@@ -0,0 +1,39 @@
|
||||
-- #1743: Mt. Moon B2F Super Nerd is text_asm with no def_trainers row, so
|
||||
-- Yellow never gets trainerHeaders.MtMoonB2F[1]. Without a seed,
|
||||
-- engageTrainer falls through to the "I like shorts!" fallback.
|
||||
--
|
||||
-- luajit tests/engine/mt_moon_super_nerd_header_bug1743.lua
|
||||
|
||||
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("mt moon super nerd header #1743")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local Data = require("src.core.Data")
|
||||
|
||||
local empty = { trainer_headers = {} }
|
||||
Data.seedMtMoonB2FSuperNerd(empty)
|
||||
local seeded = empty.trainer_headers.MtMoonB2F[1]
|
||||
check(seeded ~= nil, "seed fills MtMoonB2F[1] when missing")
|
||||
eq(seeded.battle, "_MtMoonB2FSuperNerdTheyreBothMineText",
|
||||
"pre-battle text is They're both mine")
|
||||
eq(seeded.won, "_MtMoonB2FSuperNerdOkIllShareText", "won text seeded")
|
||||
eq(seeded.after, "_MtMoonB2FSuperNerdTheresAPokemonLabText", "after text seeded")
|
||||
eq(seeded.event, "EVENT_BEAT_MT_MOON_3_SUPER_NERD",
|
||||
"event name matches shipped Red/Blue pin")
|
||||
|
||||
-- idempotent: existing header wins
|
||||
empty.trainer_headers.MtMoonB2F[1] = { battle = "KEEP" }
|
||||
Data.seedMtMoonB2FSuperNerd(empty)
|
||||
eq(empty.trainer_headers.MtMoonB2F[1].battle, "KEEP",
|
||||
"seed does not overwrite an existing header")
|
||||
|
||||
-- live Data load (Red/Blue pin or Yellow seed) must expose the battle label
|
||||
if not Data.maps then Data:load() end
|
||||
local header = Data:trainerHeader("MtMoonB2F", 1)
|
||||
check(header ~= nil, "loaded Data has MtMoonB2F[1]")
|
||||
eq(header.battle, "_MtMoonB2FSuperNerdTheyreBothMineText",
|
||||
"engageTrainer can resolve real pre-battle text")
|
||||
|
||||
S.finish()
|
||||
@@ -0,0 +1,32 @@
|
||||
-- #1644: Fly/Teleport special warps fade white (GBFadeOutToWhite /
|
||||
-- GBFadeInFromWhite), not black door-warp shape.
|
||||
--
|
||||
-- luajit tests/engine/special_warp_white_fade_bug1644.lua
|
||||
|
||||
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("special warp white fade #1644")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local Timing = require("src.core.Timing")
|
||||
local Transition = require("src.render.Transition")
|
||||
|
||||
local white = { 1, 1, 1 }
|
||||
local t = Transition.new(nil, function() end, nil, false, {
|
||||
color = white,
|
||||
frames = Timing.FADE_OUT_TO_WHITE,
|
||||
framesIn = Timing.FADE_IN_FROM_WHITE,
|
||||
})
|
||||
eq(t.color[1], 1, "special warp veil is white R")
|
||||
eq(t.color[2], 1, "special warp veil is white G")
|
||||
eq(t.color[3], 1, "special warp veil is white B")
|
||||
eq(t.frames, Timing.FADE_OUT_TO_WHITE, "fade out uses FADE_OUT_TO_WHITE")
|
||||
eq(t.framesIn, Timing.FADE_IN_FROM_WHITE, "fade in uses FADE_IN_FROM_WHITE")
|
||||
|
||||
local door = Transition.new(nil, function() end, nil, true)
|
||||
eq(door.color[1], 0, "door warp stays black")
|
||||
eq(door.framesIn, 0, "door warp has no fade in")
|
||||
check(door.frames == Timing.WARP_FADE_OUT, "door warp uses WARP_FADE_OUT")
|
||||
|
||||
S.finish()
|
||||
@@ -31,16 +31,19 @@ local save = SaveData.newGame()
|
||||
local player = { name = "FIXMON", stages = {} }
|
||||
local xBattle = { player = player, kind = "wild" }
|
||||
|
||||
local _, baseline = ItemEffects.use(Data, save, "X_ATTACK", nil, xBattle)
|
||||
T.check(baseline[1]:find("ATTACK", 1, true) ~= nil,
|
||||
local _, baseline, baselineExtra = ItemEffects.use(Data, save, "X_ATTACK", nil, xBattle)
|
||||
local baselineRose = baselineExtra and baselineExtra.afterMessages
|
||||
and baselineExtra.afterMessages[1] or baseline[1]
|
||||
T.check(baselineRose:find("ATTACK", 1, true) ~= nil,
|
||||
"X ATTACK's rose! message names the stat in English with no catalog")
|
||||
|
||||
withCatalog({ ATTACK = "ATTAQUE" }, function()
|
||||
player.stages.attack = nil
|
||||
local _, msgs = ItemEffects.use(Data, save, "X_ATTACK", nil, xBattle)
|
||||
T.check(msgs[1]:find("ATTAQUE", 1, true) ~= nil,
|
||||
local _, msgs, extra = ItemEffects.use(Data, save, "X_ATTACK", nil, xBattle)
|
||||
local rose = extra and extra.afterMessages and extra.afterMessages[1] or msgs[1]
|
||||
T.check(rose:find("ATTAQUE", 1, true) ~= nil,
|
||||
"a catalog translating ATTACK reaches the X ATTACK rose! message")
|
||||
T.check(msgs[1]:find("ATTACK", 1, true) == nil,
|
||||
T.check(rose:find("ATTACK", 1, true) == nil,
|
||||
"...and the untranslated English stat name is gone")
|
||||
end)
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
-- Title eye OBP remap lives in ImageWriter.applyTitleObp0; pixel-level
|
||||
-- coverage is tests/title_pikachu_obp_test.py (love_stub ImageData is a
|
||||
-- no-op). This file only pins the export so the Lua bake path stays wired.
|
||||
--
|
||||
-- luajit tests/engine/title_pikachu_obp_bug.lua
|
||||
|
||||
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("title pikachu OBP export")
|
||||
local check = S.check
|
||||
|
||||
local ImageWriter = require("src.import.ImageWriter")
|
||||
check(type(ImageWriter.applyTitleObp0) == "function",
|
||||
"ImageWriter.applyTitleObp0 is exported for RomExtractor")
|
||||
check(type(ImageWriter.SHADES) == "table" and #ImageWriter.SHADES == 4,
|
||||
"ImageWriter.SHADES exposes the 4 DMG ramps")
|
||||
|
||||
S.finish()
|
||||
@@ -0,0 +1,79 @@
|
||||
-- #1639: Yellow-only Advanced overlay — BEACH_HOUSE world bake, sprite
|
||||
-- coverage past Red's 72 entries, saturated YELLOWMON — without changing
|
||||
-- Red/Blue Advanced.
|
||||
--
|
||||
-- luajit tests/engine/yellow_advanced_palette_bug1639.lua
|
||||
|
||||
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("yellow advanced palette #1639")
|
||||
local check, eq = S.check, S.eq
|
||||
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
|
||||
local prevVer = GameVersion.get()
|
||||
local prevMode = PaletteFX.mode
|
||||
|
||||
PaletteFX.setMode("redpp")
|
||||
|
||||
-- Red: no BEACH_HOUSE, shared HOUSE still present
|
||||
GameVersion.set("red")
|
||||
check(not PaletteFX.hasWorldTileset("BEACH_HOUSE"),
|
||||
"Red Advanced has no BEACH_HOUSE tileset")
|
||||
check(PaletteFX.hasWorldTileset("HOUSE"), "Red Advanced still has HOUSE")
|
||||
local redHouse = PaletteFX.worldGroupColors(nil, "HOUSE", "PALLET_TOWN", 0)
|
||||
|
||||
-- Blue unchanged for BEACH_HOUSE
|
||||
GameVersion.set("blue")
|
||||
check(not PaletteFX.hasWorldTileset("BEACH_HOUSE"),
|
||||
"Blue Advanced has no BEACH_HOUSE tileset")
|
||||
|
||||
-- Yellow: BEACH_HOUSE present, HOUSE still inherits from Red pack
|
||||
GameVersion.set("yellow")
|
||||
check(PaletteFX.hasWorldTileset("BEACH_HOUSE"),
|
||||
"Yellow Advanced profiles BEACH_HOUSE")
|
||||
check(PaletteFX.hasWorldTileset("HOUSE"),
|
||||
"Yellow Advanced still sees shared HOUSE")
|
||||
local beach = PaletteFX.worldGroupColors(nil, "BEACH_HOUSE", "SUMMER_BEACH_HOUSE", 0)
|
||||
check(beach ~= nil and #beach == 8, "BEACH_HOUSE has 8 group colors")
|
||||
eq(beach[1][1][1], 255, "BEACH_HOUSE sand group is warm (R=255)")
|
||||
|
||||
-- Sprite: Pikachu @60 and high indices resolve; Red path for bike stays 0
|
||||
local pika = PaletteFX.spriteObp({ source = "ROM:SpriteSheetPointerTable[60]" }, "pika")
|
||||
check(pika ~= nil, "Yellow SPRITE_PIKACHU gets an OBP")
|
||||
eq(pika[2][2], 255, "Pikachu OBP mid shade is saturated yellow G=255")
|
||||
local boulder = PaletteFX.spriteObp({ source = "ROM:SpriteSheetPointerTable[72]" }, "b")
|
||||
check(boulder ~= nil, "Yellow boulder (index 72) gets an OBP")
|
||||
|
||||
GameVersion.set("red")
|
||||
local redBall = PaletteFX.spriteObp({ source = "ROM:SpriteSheetPointerTable[60]" }, "ball")
|
||||
check(redBall ~= nil, "Red poke-ball @60 still resolves")
|
||||
-- Red group 0 mid is orange skin, not pure yellow
|
||||
check(redBall[2][2] < 200, "Red @60 is not Yellow's Pikachu ramp")
|
||||
|
||||
-- YELLOWMON / MEWMON / LOGO: Advanced uses CGBBase (saturated), not washed
|
||||
-- SuperPalettes
|
||||
GameVersion.set("yellow")
|
||||
local yellowMon = PaletteFX.pal(nil, "YELLOWMON")
|
||||
eq(yellowMon[2][2], 255, "Advanced Yellow YELLOWMON mid is saturated yellow G")
|
||||
eq(yellowMon[2][3], 0, "Advanced Yellow YELLOWMON mid is pure yellow B=0")
|
||||
local mewmon = PaletteFX.pal(nil, "MEWMON")
|
||||
eq(mewmon[2][1], 255, "MEWMON stays Yellow title yellow (not Red++ purple)")
|
||||
eq(mewmon[2][2], 255, "MEWMON body is saturated yellow G=255 (not washed 156)")
|
||||
eq(mewmon[2][3], 0, "MEWMON body is pure yellow B=0")
|
||||
check(mewmon[3][3] < 150, "MEWMON cheek stays red, not purple")
|
||||
local logo2 = PaletteFX.pal(nil, "LOGO2")
|
||||
eq(logo2[2][2], 255, "LOGO2 fill is saturated yellow under Advanced")
|
||||
eq(logo2[2][3], 0, "LOGO2 fill is pure yellow B=0")
|
||||
|
||||
-- Red HOUSE colors identical before/after Yellow merge path
|
||||
GameVersion.set("red")
|
||||
local redHouse2 = PaletteFX.worldGroupColors(nil, "HOUSE", "PALLET_TOWN", 0)
|
||||
eq(redHouse2[1][1][1], redHouse[1][1][1], "Red HOUSE colors unchanged")
|
||||
|
||||
PaletteFX.setMode(prevMode)
|
||||
GameVersion.set(prevVer)
|
||||
|
||||
S.finish()
|
||||
@@ -102,5 +102,44 @@ class ApplyKnownNonreproducibleOverridesTest(TestCase):
|
||||
texts, field_data)
|
||||
|
||||
|
||||
class YellowSuperRodParseTest(TestCase):
|
||||
"""#1074: Yellow's inline Super Rod table (species, level), not Red's."""
|
||||
|
||||
def test_parse_super_rod_yellow_safari_dragonair(self):
|
||||
import json
|
||||
import tempfile
|
||||
from extract import field
|
||||
|
||||
asm = (
|
||||
"SuperRodFishingSlots::\n"
|
||||
"\tdb SAFARI_ZONE_CENTER, MAGIKARP, 5, MAGIKARP, 10, "
|
||||
"DRATINI, 10, DRAGONAIR, 15\n"
|
||||
"\tdb SAFARI_ZONE_EAST, MAGIKARP, 5, MAGIKARP, 10, "
|
||||
"MAGIKARP, 15, DRATINI, 15\n"
|
||||
"\tdb -1 ; end\n"
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
wild = Path(td) / "data" / "wild"
|
||||
wild.mkdir(parents=True)
|
||||
(wild / "super_rod.asm").write_text(asm)
|
||||
parsed = field.parse_super_rod_yellow(td)
|
||||
|
||||
self.assertEqual(parsed["SAFARI_ZONE_CENTER"], [
|
||||
{"level": 5, "species": "MAGIKARP"},
|
||||
{"level": 10, "species": "MAGIKARP"},
|
||||
{"level": 10, "species": "DRATINI"},
|
||||
{"level": 15, "species": "DRAGONAIR"},
|
||||
])
|
||||
self.assertNotIn("DRAGONAIR",
|
||||
[s["species"] for s in parsed["SAFARI_ZONE_EAST"]])
|
||||
|
||||
def test_shipped_yellow_manifest_has_safari_dragonair(self):
|
||||
import json
|
||||
path = Path(__file__).resolve().parents[1] / "tools" / "rom_manifest_yellow.json"
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
center = data["field"]["superRod"]["SAFARI_ZONE_CENTER"]
|
||||
self.assertIn({"level": 15, "species": "DRAGONAIR"}, center)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Title rOBP0=$E0 remaps eye OAM shades 1/2 to white (#1639 pupils)."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest import TestCase, main
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
|
||||
import build_rom_data as b # noqa: E402
|
||||
from PIL import Image # noqa: E402
|
||||
|
||||
|
||||
class TitleObp0Test(TestCase):
|
||||
def test_mid_and_dark_become_white(self):
|
||||
img = Image.new("RGBA", (4, 1), (0, 0, 0, 0))
|
||||
img.putpixel((0, 0), b.GB_SHADES[1])
|
||||
img.putpixel((1, 0), b.GB_SHADES[2])
|
||||
img.putpixel((2, 0), b.GB_SHADES[3])
|
||||
img.putpixel((3, 0), b.GB_SHADES[0])
|
||||
b._apply_title_obp0(img)
|
||||
self.assertEqual(img.getpixel((0, 0)), b.GB_SHADES[0])
|
||||
self.assertEqual(img.getpixel((1, 0)), b.GB_SHADES[0])
|
||||
self.assertEqual(img.getpixel((2, 0)), b.GB_SHADES[3])
|
||||
self.assertEqual(img.getpixel((3, 0)), b.GB_SHADES[0])
|
||||
|
||||
def test_transparent_untouched(self):
|
||||
img = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
|
||||
b._apply_title_obp0(img)
|
||||
self.assertEqual(img.getpixel((0, 0))[3], 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -22,9 +22,12 @@ eq(mewmonSgb[3][1], 222, "SGB: Color 2 is red R=222")
|
||||
PaletteFX.setMode("redpp")
|
||||
local mewmonAdv = PaletteFX.pal(nil, "MEWMON")
|
||||
check(mewmonAdv ~= nil, "MEWMON palette exists in ADVANCED mode on Yellow")
|
||||
-- Must NOT be Red++'s Mew purple {115, 33, 165}! It must be Yellow's Pikachu palette!
|
||||
-- Must NOT be Red++'s Mew purple {115, 33, 165}! It must be Yellow's Pikachu
|
||||
-- CGBBase yellow {255,255,0}, not washed SuperPalette cream {255,255,156}.
|
||||
eq(mewmonAdv[1][1], 255, "ADVANCED: Color 0 is white R=255 (Pikachu eye sclera)")
|
||||
eq(mewmonAdv[2][1], 255, "ADVANCED: Color 1 is yellow R=255 (Pikachu body)")
|
||||
eq(mewmonAdv[2][2], 255, "ADVANCED: Color 1 is saturated yellow G=255")
|
||||
eq(mewmonAdv[2][3], 0, "ADVANCED: Color 1 is pure yellow B=0 (not washed cream)")
|
||||
check(mewmonAdv[3][3] < 150, "ADVANCED: Color 2 is not purple (B < 150, red cheeks)")
|
||||
|
||||
-- Test 3: OG YELLOW mode (ogred on Yellow)
|
||||
|
||||
Reference in New Issue
Block a user