mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 23:11:15 +02:00
ca4d3d283c
Crystal boots from a user-supplied ROM, imports a full cache and is playable: copyright, the Crystal intro movie, the animated title, gender select, Oak, and out into Johto. 122 of the cart's 169 script specials are implemented. Import and data - tools/make_crystal_manifest.py derives the manifest by importing make_gold_manifest as a library, with three additive keyword seams. Gold and Silver still regenerate byte-identical, which is the standing requirement for touching that generator. - crystal_symbol_deltas.py and crystal_movie_symbols.py carry the symbol delta: Crystal renames the credits mons, splits the trainer card, Pokegear and pack-pal blocks by gender, and replaces the intro and title outright. - Crystal-only manifest keys: engineFlagOrder (162 flags to Gold's 93, so the badge block sits one higher) and unownCharmap (the main charmap parser stops at the first newcharmap so the two cannot contaminate each other). Extractor - RomExtractorGen2 becomes three-edition. Crystal corrections: PAL_MAP_BANK 0x13, a flat PICS_FIX pic bank, audio bank 0x5e, the mapSongs id-100 hole, seven NPC trades, a TradeTexts stride of 8, the five Crystal tileset anim steps with per-row degrade, and the column-major trainer card portraits. - New: animated front sprites (frames, bitmasks, play and idle scripts), the Battle Tower roster, Kris assets, Mobile System GB art, and the Crystal intro and title via src/import/CrystalMovie.lua. Engine - GameVersion gains engine(id) and fixes(id). Gold and Silver keep their original bugs where the bug is not hardware dependent; Crystal gets the fixes Crystal shipped: Lucky Number boxes 10-14, surfing onto an NPC, and the Reflect and Light Screen defence overflow. - Crystal story: Suicune and Eusine, Celebi behind the GS Ball flag, the Ruins of Alph chambers, Buena, the Move Tutor, the Poke Seer, and the Battle Tower including the wInBattleTowerBattle badge-boost guard. - Kris and the gender flag, animated fronts in battle and the summary screen, and mon caught data. Verification - Every extracted asset is pixel-compared against pret's own source PNGs. - Gold caches are byte-identical before and after, file for file. - New Crystal suites plus a T2 Gen 2 tier; the full suite passes.
159 lines
6.5 KiB
Lua
159 lines
6.5 KiB
Lua
-- Crystal world data: the roamer roster and swarm pairs that differ from
|
|
-- Gold, then the facts only a real Crystal cache can answer.
|
|
-- Self-contained: `luajit tests/crystal_world_test.lua`; also dofile'd by
|
|
-- tests/run_tests.lua. The cache half SKIPs when no crystal cache is present.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local S = require("tests.harness").suite("crystal world")
|
|
local check, eq = S.check, S.eq
|
|
|
|
local Permissions = require("src.world.gen2.Permissions")
|
|
local Roamers = require("src.core.gen2.Roamers")
|
|
local Swarm = Roamers.Swarm
|
|
|
|
-- ---------------------------------------------------------------- ROM-free
|
|
|
|
check(Permissions.isWalkable(0x00), "COLL_FLOOR walkable")
|
|
check(not Permissions.isWalkable(0x07), "COLL_WALL blocked")
|
|
-- pokecrystal/constants/collision_constants.asm:19
|
|
check(Permissions.isGrass(0x18), "COLL_TALL_GRASS is grass")
|
|
check(Permissions.isWater(0x29), "COLL_WATER is water")
|
|
check(Permissions.isWarpCollision(0x71), "COLL_DOOR is a warp")
|
|
check(Permissions.isLedge(0xa0), "COLL_HOP_DOWN is a ledge")
|
|
eq(Permissions.of(0xff), Permissions.WALL, "a missing coll reads as wall")
|
|
|
|
local bare = {}
|
|
Roamers.init(bare)
|
|
eq(#bare.roamers, 3, "no cache -> the three-beast Gold roster")
|
|
check(Roamers.SPECIES ~= nil and #Roamers.SPECIES == 3,
|
|
"Roamers.SPECIES survives as the ROM-free fallback")
|
|
|
|
-- pokecrystal/constants/script_constants.asm:256-257
|
|
local pair = {}
|
|
Swarm.set(pair, "DARK_CAVE_VIOLET_ENTRANCE", 0)
|
|
Swarm.set(pair, "ROUTE_35", 1)
|
|
eq(pair.swarmMaps.DUNSPARCE, "DARK_CAVE_VIOLET_ENTRANCE", "SWARM_DUNSPARCE key")
|
|
eq(pair.swarmMaps.YANMA, "ROUTE_35", "SWARM_YANMA key, independently")
|
|
eq(Swarm.onMap(pair, "ROUTE_35"), "YANMA", "the Yanma map answers YANMA")
|
|
eq(Swarm.mapId(pair, "YANMA"), "ROUTE_35", "mapId takes a kind")
|
|
eq(Swarm.mapId(pair), "DARK_CAVE_VIOLET_ENTRANCE", "and defaults to Gold's key")
|
|
Swarm.timeEvents(pair, 100)
|
|
check(Swarm.timeEvents(pair, 101), "the next day ends the swarm")
|
|
eq(Swarm.onMap(pair, "ROUTE_35"), nil, "and both pairs are stranded")
|
|
|
|
local legacy = { swarmMap = "ROUTE_35", dailyFlags = { swarm = true } }
|
|
eq(Swarm.mapId(legacy), "ROUTE_35", "an old scalar save answers mapId")
|
|
eq(Swarm.onMap(legacy, "ROUTE_35"), "DUNSPARCE", "normalized onto the Gold key")
|
|
eq(Swarm.onMap(legacy, "ROUTE_36"), nil, "another map is not the swarm map")
|
|
check(Swarm.entry(legacy, { swarmGrass = { ROUTE_35 = { "row" } }, grass = {} },
|
|
"ROUTE_35", "grass") ~= nil, "and its swarm table still resolves")
|
|
|
|
-- ------------------------------------------------------------ cache-gated
|
|
|
|
local cache = os.getenv("CRYSTAL_CACHE")
|
|
if not cache then
|
|
local home = os.getenv("HOME") or ""
|
|
cache = home .. "/Library/Application Support/LOVE/crystal-dev/crystal"
|
|
end
|
|
|
|
local mapsPath = cache .. "/data/generated/maps.lua"
|
|
local mapsFile = io.open(mapsPath, "r")
|
|
if not mapsFile then
|
|
check(true, "crystal cache absent : SKIP")
|
|
S.finish()
|
|
return
|
|
end
|
|
mapsFile:close()
|
|
|
|
local function loadLua(rel)
|
|
local chunk = loadfile(cache .. "/" .. rel)
|
|
if not chunk then return nil end
|
|
local ok, value = pcall(chunk)
|
|
return ok and value or nil
|
|
end
|
|
|
|
local maps = loadLua("data/generated/maps.lua")
|
|
local encounters = loadLua("data/generated/encounters.lua")
|
|
local tilesets = loadLua("data/generated/tilesets.lua")
|
|
local constants = loadLua("data/generated/constants.lua")
|
|
|
|
check(maps ~= nil, "maps.lua loads")
|
|
check(encounters ~= nil, "encounters.lua loads")
|
|
check(tilesets ~= nil, "tilesets.lua loads")
|
|
check(constants ~= nil, "constants.lua loads")
|
|
|
|
-- ---- 1. the maps Phase 1 has to reach
|
|
|
|
local count = 0
|
|
for _ in pairs(maps or {}) do count = count + 1 end
|
|
eq(count, 388, "the cache carries 388 maps")
|
|
for _, id in ipairs({
|
|
"NEW_BARK_TOWN", "PLAYERS_HOUSE_1F", "PLAYERS_HOUSE_2F", "ELMS_LAB",
|
|
"ROUTE_29", "CHERRYGROVE_CITY", "ROUTE_30", "ROUTE_31", "VIOLET_CITY",
|
|
"SPROUT_TOWER_1F", "VIOLET_GYM",
|
|
}) do
|
|
check(maps[id] ~= nil, "the New Bark to Violet route has " .. id)
|
|
end
|
|
check(maps.BATTLE_TOWER_1F ~= nil, "and the Crystal-only Battle Tower")
|
|
-- pokegold/constants/map_constants.asm:152
|
|
eq(maps.ECRUTEAK_TIN_TOWER_BACK_ENTRANCE, nil,
|
|
"the one map Crystal drops is absent")
|
|
|
|
-- ---- 2. roamers (CT-4)
|
|
|
|
check(type(encounters.roamMons) == "table",
|
|
"the cache emits encounters.roamMons")
|
|
eq(#encounters.roamMons, 2, "Crystal seeds two beasts, not three")
|
|
eq(encounters.roamMons[1].species, "RAIKOU", "slot 1 is Raikou")
|
|
eq(encounters.roamMons[1].map, "ROUTE_42", "on Route 42")
|
|
eq(encounters.roamMons[2].species, "ENTEI", "slot 2 is Entei")
|
|
eq(encounters.roamMons[2].map, "ROUTE_37", "on Route 37")
|
|
eq(encounters.roamMons[1].level, 40, "both start at level 40")
|
|
eq(encounters.roamMons[2].level, 40, "both start at level 40")
|
|
|
|
local save = {}
|
|
Roamers.init(save, { encounters = encounters })
|
|
eq(#save.roamers, 2, "Roamers.init over the cache builds two slots")
|
|
eq(save.roamers[1].species, "RAIKOU", "slot 1 Raikou")
|
|
eq(save.roamers[2].species, "ENTEI", "slot 2 Entei")
|
|
eq(save.roamers[3], nil, "and no Suicune slot")
|
|
eq(save.roamers[1].hp, 0, "hp is zeroed so stats regenerate on contact")
|
|
|
|
local data = {}
|
|
Roamers.init(data, { data = { gen2Encounters = encounters } })
|
|
eq(#data.roamers, 2, "the opts.data path resolves the same roster")
|
|
|
|
eq(Roamers.checkEncounter(save, "ROUTE_38", false, function() return 3 end), nil,
|
|
"the Suicune slot roll finds nothing on a Crystal save")
|
|
local hit = Roamers.checkEncounter(save, "ROUTE_42", false, function() return 1 end)
|
|
check(hit and hit.species == "RAIKOU", "the Raikou roll still hits")
|
|
|
|
-- ---- 3. the swarm tables the kind byte indexes
|
|
|
|
check(encounters.swarmGrass ~= nil, "the cache carries swarmGrass")
|
|
check(encounters.swarmGrass.DARK_CAVE_VIOLET_ENTRANCE ~= nil,
|
|
"with the Dunsparce table")
|
|
check(encounters.swarmGrass.ROUTE_35 ~= nil, "and the Yanma table")
|
|
|
|
-- ---- 4. the tileset palette-map bank
|
|
-- pokecrystal.sym 13:40e5 TilesetJohtoPalMap (pokegold.sym 02:40c7)
|
|
local johto = (tilesets or {}).TILESET_JOHTO
|
|
check(johto ~= nil, "TILESET_JOHTO is in the cache")
|
|
eq(johto and johto.palMap and johto.palMap.bank, 0x13,
|
|
"TilesetJohtoPalMap is read out of bank $13")
|
|
eq(johto and johto.palMap and johto.palMap.address, 0x40e5,
|
|
"at $40e5")
|
|
local kanto = (tilesets or {}).TILESET_KANTO
|
|
eq(kanto and kanto.palMap and kanto.palMap.bank, 0x13,
|
|
"TilesetKantoPalMap is bank $13 too")
|
|
eq(kanto and kanto.palMap and kanto.palMap.address, 0x4075, "at $4075")
|
|
check(johto and johto.tilePalettes and #johto.tilePalettes > 0,
|
|
"and the map decoded to a per-tile palette list")
|
|
|
|
-- ---- 5. the special table the cache pins
|
|
|
|
eq(#(constants.specialOrder or {}), 169,
|
|
"the cache's specialOrder has Crystal's 169 rows")
|
|
|
|
S.finish()
|