Add Pokemon Crystal as a sixth supported version

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.
This commit is contained in:
bryanthaboi
2026-08-23 12:10:28 -04:00
parent e88f2ef060
commit ca4d3d283c
141 changed files with 43306 additions and 583 deletions
+5 -4
View File
@@ -103,6 +103,9 @@ local function applyLoaded(path, statusVerb)
S.loadError = false
S.allowSave = true
end
if Gen.of(S.save, S.version) == 2 then
S.events = Catalog.gen2EventList(Gen.engineOf(S.save, S.version), S.modRoots)
end
local mapId = Gen.playerMap(S.save)
S.mapId = mapId
S.dirty = false
@@ -169,16 +172,14 @@ function App.load(pathOverride, opts)
for _, mod in ipairs(S.mods:status().loaded) do
modRoots[#modRoots + 1] = mod.path
end
S.modRoots = modRoots
if Gen.of(nil, opts.version) == 2 or require("src.core.GameVersion").generation() == 2 then
S.events = Catalog.goldEventList(modRoots)
S.events = Catalog.gen2EventList(Gen.engineOf(nil, opts.version), modRoots)
else
S.events = Catalog.scrapeEvents("data/scripts", "data/generated/trainer_headers.lua",
nil, modRoots)
end
applyLoaded(pathOverride or SaveIO.defaultPath(), "Loaded")
if Gen.of(S.save, S.version) == 2 then
S.events = Catalog.goldEventList(modRoots)
end
end
-- Switch to another save file (Open button, drag-drop, or --save arg).
+6 -9
View File
@@ -125,15 +125,8 @@ function Catalog.scrapeEvents(scriptDir, headerPath, listFiles, extraDirs)
return sortedKeys(found)
end
function Catalog.goldEventList(extraDirs)
local names = {}
local ok, flags = pcall(require, "src.core.gen2.FlagNames")
if ok and flags and flags.events then
for name in pairs(flags.events) do
names[#names + 1] = name
end
end
table.sort(names)
function Catalog.gen2EventList(engine, extraDirs)
local names = require("Gen2Flags").names(engine)
local modFlags = Catalog.scrapeEvents(nil, nil, nil, extraDirs)
local seen = {}
for _, name in ipairs(names) do seen[name] = true end
@@ -146,4 +139,8 @@ function Catalog.goldEventList(extraDirs)
return names
end
function Catalog.goldEventList(extraDirs)
return Catalog.gen2EventList("gs", extraDirs)
end
return Catalog
+73 -8
View File
@@ -24,6 +24,37 @@ function Gen.of(save, version)
return GameVersion.generation()
end
local function versionOf(save, version)
if type(save) == "table" and GameVersion.VERSIONS[save.version] then
return save.version
end
if type(version) == "string" and GameVersion.VERSIONS[version] then
return version
end
return GameVersion.get()
end
Gen.versionOf = versionOf
function Gen.engineOf(save, version)
return GameVersion.engine(versionOf(save, version))
end
function Gen.editionLabel(save, version)
local info = GameVersion.info(versionOf(save, version))
return tostring((info and info.label) or "GEN 2"):upper()
end
function Gen.hasCaughtData(save, version)
if Gen.of(save, version) ~= 2 then return false end
return require("src.battle.gen2.Mon").hasCaughtData(versionOf(save, version))
end
-- engine/menus/init_gender.asm:23-38
function Gen.hasPlayerGender(save, version)
if Gen.of(save, version) ~= 2 then return false end
return Gen.engineOf(save, version) == "crystal"
end
function Gen.ofState(S)
if not S then return GameVersion.generation() end
return Gen.of(S.save, S.version)
@@ -110,10 +141,17 @@ function Gen.bindGoldData(data)
end
function Gen.newGame(version)
if (versionGeneration(version) or GameVersion.generation(version)) == 2 then
return require("src.core.gen2.Save").newGame()
local id = type(version) == "string" and GameVersion.VERSIONS[version] and version
or nil
if (versionGeneration(id) or GameVersion.generation()) == 2 then
local Save2 = require("src.core.gen2.Save")
local save = Save2.newGame({
playerName = id and Save2.defaultPlayerName(id) or nil,
})
if id then save.version = id end
return save
end
return require("src.core.SaveData").newGame()
return require("src.core.SaveData").newGame({ version = id })
end
function Gen.validate(save, data)
@@ -223,6 +261,34 @@ function Gen.setCoins(save, amount)
end
end
-- constants/ram_constants.asm:176-177
function Gen.playerGender(save)
local stored = save and save.player and save.player.gender
return stored == "female" and "female" or "male"
end
function Gen.setPlayerGender(save, gender)
save.player = save.player or {}
save.player.gender = (gender == "female") and "female" or "male"
return save.player.gender
end
-- constants/landmark_constants.asm:3, :111-113
function Gen.landmarkName(data, index)
index = math.floor(tonumber(index) or 0)
local Mon = require("src.battle.gen2.Mon")
if index == Mon.LANDMARK_EVENT then return "EVENT" end
if index == Mon.LANDMARK_GIFT then return "GIFT" end
if index == 0 then return "UNKNOWN" end
local rows = data and data.gen2Landmarks and data.gen2Landmarks.landmarks
for _, rec in pairs(rows or {}) do
if type(rec) == "table" and rec.index == index then
return (tostring(rec.name or rec.id):gsub("\n", " "))
end
end
return ("#%d"):format(index)
end
function Gen.dexOwnedKey(save)
if Gen.of(save) == 2 then return "caught" end
return "owned"
@@ -317,14 +383,13 @@ function Gen.toggleBadge(save, id)
return not on
end
local function goldFlagId(name)
local flags = require("src.core.gen2.FlagNames")
return flags.events and flags.events[name]
local function gen2FlagId(save, name)
return require("Gen2Flags").byName(Gen.engineOf(save))[name]
end
function Gen.getFlag(save, name)
if Gen.of(save) == 2 then
local id = goldFlagId(name)
local id = gen2FlagId(save, name)
if id then
local Events2 = require("src.world.gen2.Events")
local ev = Events2.new()
@@ -338,7 +403,7 @@ end
function Gen.setFlag(save, name, on)
if Gen.of(save) == 2 then
local id = goldFlagId(name)
local id = gen2FlagId(save, name)
if id then
local Events2 = require("src.world.gen2.Events")
local ev = Events2.new()
+275
View File
@@ -0,0 +1,275 @@
-- ../pokecrystal/constants/event_flags.asm:1 against
-- ../pokegold/constants/event_flags.asm:1
local Gen2Flags = {}
Gen2Flags.CRYSTAL_ADDED = {
EVENT_ALAN_GAVE_FIRE_STONE = 257,
EVENT_ANSWERED_DRAGON_MASTER_QUIZ_WRONG = 193,
EVENT_AZALEA_TOWN_KURT = 1956,
EVENT_BATTLE_TOWER_BATTLE_ROOM_YOUNGSTER = 1937,
EVENT_BATTLE_TOWER_OPEN_CIVILIANS = 1999,
EVENT_BEAT_BEAUTY_OLIVIA = 1473,
EVENT_BEAT_BUG_CATCHER_WAYNE = 1472,
EVENT_BEAT_CAMPER_QUENTIN = 1475,
EVENT_BEAT_COOLTRAINERF_CARA = 1470,
EVENT_BEAT_COOLTRAINERM_DARIN = 1469,
EVENT_BEAT_FISHER_TULLY = 1108,
EVENT_BEAT_FISHER_TULLY2 = 1119,
EVENT_BEAT_FISHER_TULLY3 = 1120,
EVENT_BEAT_POKEFANF_JAIME = 1474,
EVENT_BEAT_POKEFANM_ALLAN = 1480,
EVENT_BEAT_POKEFANM_REX = 1479,
EVENT_BEAT_POKEMANIAC_MILLER = 1476,
EVENT_BEAT_SAGE_GAKU = 1481,
EVENT_BEAT_SAGE_KOJI = 1483,
EVENT_BEAT_SAGE_MASA = 1482,
EVENT_BEAT_SUPER_NERD_HUGH = 1477,
EVENT_BEAT_SUPER_NERD_MARKUS = 1478,
EVENT_BEAT_SUPER_NERD_STAN = 1409,
EVENT_BEAT_TWINS_LEA_AND_PIA = 1471,
EVENT_BUENA_OFFERED_HER_PHONE_NUMBER = 828,
EVENT_BUENA_OFFERED_HER_PHONE_NUMBER_NO_BLUE_CARD = 670,
EVENT_BUGGING_KURT_TOO_MUCH = 187,
EVENT_BURNED_TOWER_1F_EUSINE = 1893,
EVENT_BURNED_TOWER_1F_HIDDEN_ULTRA_BALL = 255,
EVENT_BURNED_TOWER_1F_HP_UP = 1622,
EVENT_BURNED_TOWER_MORTY = 1892,
EVENT_CAN_GIVE_GS_BALL_TO_KURT = 190,
EVENT_CIANWOOD_CITY_EUSINE = 1965,
EVENT_COPYCAT_1 = 1774,
EVENT_COPYCAT_2 = 1775,
EVENT_DANA_GAVE_THUNDERSTONE = 258,
EVENT_DARK_CAVE_VIOLET_ENTRANCE_DIRE_HIT = 1998,
EVENT_DRAGONS_DEN_B1F_CALCIUM = 1983,
EVENT_DRAGONS_DEN_B1F_MAX_ELIXER = 1984,
EVENT_DRAGON_SHRINE_CLAIR = 1936,
EVENT_ECRUTEAK_CITY_GRAMPS = 1961,
EVENT_ECRUTEAK_GYM_GRAMPS = 1960,
EVENT_ECRUTEAK_TIN_TOWER_ENTRANCE_WANDERING_SAGE = 1969,
EVENT_ERIN_CALCIUM = 827,
EVENT_EUSINE_IN_BURNED_TOWER = 1962,
EVENT_FLORIA_AT_FLOWER_SHOP = 1896,
EVENT_FLORIA_AT_SUDOWOODO = 1897,
EVENT_FOREST_IS_RESTLESS = 192,
EVENT_FOUGHT_EUSINE = 819,
EVENT_FOUGHT_SUICUNE = 821,
EVENT_GAVE_GS_BALL_TO_KURT = 191,
EVENT_GINA_GAVE_LEAF_STONE = 256,
EVENT_GOLDENROD_CITY_MOVE_TUTOR = 1898,
EVENT_GOLDENROD_GAME_CORNER_MOVE_TUTOR = 1899,
EVENT_GOLDENROD_SALE_OFF = 1776,
EVENT_GOLDENROD_SALE_ON = 1777,
EVENT_GOLDENROD_UNDERGROUND_WAREHOUSE_ULTRA_BALL = 1621,
EVENT_GOT_CLEAR_BELL = 120,
EVENT_GOT_DRATINI = 189,
EVENT_GOT_GS_BALL_FROM_GOLDENROD_POKEMON_CENTER = 832,
EVENT_GOT_ODD_EGG = 830,
EVENT_GOT_RAINBOW_WING = 822,
EVENT_HOLE_IN_BURNED_TOWER = 818,
EVENT_HUEY_PROTEIN = 823,
EVENT_ICE_PATH_1F_PROTEIN = 1982,
EVENT_ILEX_FOREST_ANTIDOTE = 1978,
EVENT_ILEX_FOREST_ETHER = 1979,
EVENT_ILEX_FOREST_FARFETCHD = 1769,
EVENT_ILEX_FOREST_KURT = 1957,
EVENT_ILEX_FOREST_LASS = 1773,
EVENT_ILEX_FOREST_X_ATTACK = 1977,
EVENT_JOEY_HP_UP = 824,
EVENT_KOJI_ALLOWS_YOU_PASSAGE_TO_TIN_TOWER = 820,
EVENT_KURTS_HOUSE_GRANDDAUGHTER_1 = 1932,
EVENT_KURTS_HOUSE_GRANDDAUGHTER_2 = 1933,
EVENT_LAKE_OF_RAGE_ELIXER = 1605,
EVENT_LAKE_OF_RAGE_ELIXIR_ON_STANDBY = 58,
EVENT_MET_BUENA = 829,
EVENT_MET_FLORIA = 185,
EVENT_MOUNT_MORTAR_1F_INSIDE_IRON = 1992,
EVENT_MOUNT_MORTAR_1F_INSIDE_MAX_POTION = 1958,
EVENT_MOUNT_MORTAR_1F_INSIDE_NUGGET = 1959,
EVENT_MOUNT_MORTAR_1F_INSIDE_ULTRA_BALL = 1993,
EVENT_MOUNT_MORTAR_B1F_CARBOS = 1671,
EVENT_MOUNT_MORTAR_B1F_FULL_RESTORE = 1994,
EVENT_MOUNT_MORTAR_B1F_MAX_ETHER = 1995,
EVENT_MOUNT_MORTAR_B1F_PP_UP = 1996,
EVENT_OLIVINE_LIGHTHOUSE_5F_SUPER_REPEL = 1638,
EVENT_PARRY_IRON = 826,
EVENT_PICKED_UP_BERRY_FROM_KABUTO_ITEM_ROOM = 1944,
EVENT_PICKED_UP_CHARCOAL_FROM_HO_OH_ITEM_ROOM = 1943,
EVENT_PICKED_UP_ENERGYPOWDER_FROM_KABUTO_ITEM_ROOM = 1947,
EVENT_PICKED_UP_ENERGY_ROOT_FROM_AERODACTYL_ITEM_ROOM = 1955,
EVENT_PICKED_UP_GOLD_BERRY_FROM_AERODACTYL_ITEM_ROOM = 1952,
EVENT_PICKED_UP_GOLD_BERRY_FROM_HO_OH_ITEM_ROOM = 1940,
EVENT_PICKED_UP_HEAL_POWDER_FROM_AERODACTYL_ITEM_ROOM = 1954,
EVENT_PICKED_UP_HEAL_POWDER_FROM_KABUTO_ITEM_ROOM = 1946,
EVENT_PICKED_UP_MOON_STONE_FROM_AERODACTYL_ITEM_ROOM = 1953,
EVENT_PICKED_UP_MYSTERYBERRY_FROM_HO_OH_ITEM_ROOM = 1941,
EVENT_PICKED_UP_MYSTERYBERRY_FROM_OMANYTE_ITEM_ROOM = 1948,
EVENT_PICKED_UP_MYSTIC_WATER_FROM_OMANYTE_ITEM_ROOM = 1949,
EVENT_PICKED_UP_PSNCUREBERRY_FROM_KABUTO_ITEM_ROOM = 1945,
EVENT_PICKED_UP_REVIVAL_HERB_FROM_HO_OH_ITEM_ROOM = 1942,
EVENT_PICKED_UP_STARDUST_FROM_OMANYTE_ITEM_ROOM = 1950,
EVENT_PICKED_UP_STAR_PIECE_FROM_OMANYTE_ITEM_ROOM = 1951,
EVENT_PLAYERS_HOUSE_1F_NEIGHBOR = 1938,
EVENT_PLAYERS_NEIGHBORS_HOUSE_NEIGHBOR = 1939,
EVENT_RADIO_TOWER_5F_ULTRA_BALL = 1997,
EVENT_RANG_CLEAR_BELL_1 = 1894,
EVENT_RANG_CLEAR_BELL_2 = 1895,
EVENT_ROUTE_30_ANTIDOTE = 1976,
EVENT_ROUTE_31_POTION = 1710,
EVENT_ROUTE_32_REPEL = 1713,
EVENT_ROUTE_34_ILEX_FOREST_GATE_LASS = 1771,
EVENT_ROUTE_34_ILEX_FOREST_GATE_TEACHER_BEHIND_COUNTER = 1770,
EVENT_ROUTE_34_ILEX_FOREST_GATE_TEACHER_IN_WALKWAY = 1772,
EVENT_ROUTE_34_NUGGET = 1980,
EVENT_ROUTE_44_MAX_REPEL = 1981,
EVENT_ROUTE_45_NUGGET = 1720,
EVENT_ROUTE_46_X_SPEED = 1724,
EVENT_RUINS_OF_ALPH_OUTSIDE_TOURIST_FISHER = 1934,
EVENT_RUINS_OF_ALPH_OUTSIDE_TOURIST_YOUNGSTERS = 1935,
EVENT_SAW_SUICUNE_AT_CIANWOOD_CITY = 1966,
EVENT_SAW_SUICUNE_ON_ROUTE_36 = 1968,
EVENT_SAW_SUICUNE_ON_ROUTE_42 = 1967,
EVENT_SET_WHEN_FOUGHT_HO_OH = 1975,
EVENT_SILVER_CAVE_ROOM_1_PROTEIN = 1690,
EVENT_SILVER_CAVE_ROOM_1_ULTRA_BALL = 1985,
EVENT_SILVER_CAVE_ROOM_2_CALCIUM = 1986,
EVENT_SILVER_CAVE_ROOM_2_PP_UP = 1988,
EVENT_SILVER_CAVE_ROOM_2_ULTRA_BALL = 1987,
EVENT_SPROUT_TOWER_2F_X_ACCURACY = 1608,
EVENT_TALKED_TO_FLORIA_AT_FLOWER_SHOP = 186,
EVENT_TALKED_TO_RUINS_COWARD = 188,
EVENT_TEAM_ROCKET_BASE_B1F_GUARD_SPEC = 1643,
EVENT_TEAM_ROCKET_BASE_B3F_FULL_HEAL = 1647,
EVENT_TEAM_ROCKET_BASE_B3F_PROTEIN = 1645,
EVENT_TEAM_ROCKET_BASE_B3F_ULTRA_BALL = 1620,
EVENT_TEAM_ROCKET_BASE_B3F_X_SPECIAL = 1646,
EVENT_TIFFANY_GAVE_PINK_BOW = 260,
EVENT_TIN_TOWER_1F_ENTEI = 1971,
EVENT_TIN_TOWER_1F_EUSINE = 1973,
EVENT_TIN_TOWER_1F_RAIKOU = 1972,
EVENT_TIN_TOWER_1F_SUICUNE = 1970,
EVENT_TIN_TOWER_1F_WISE_TRIO_1 = 1974,
EVENT_TIN_TOWER_1F_WISE_TRIO_2 = 1989,
EVENT_TIN_TOWER_4F_PP_UP = 1613,
EVENT_TIN_TOWER_6F_MAX_POTION = 1990,
EVENT_TIN_TOWER_9F_HP_UP = 1991,
EVENT_TULLY_ASKED_FOR_PHONE_NUMBER = 655,
EVENT_TULLY_GAVE_WATER_STONE = 259,
EVENT_UNION_CAVE_1F_POTION = 1628,
EVENT_UNION_CAVE_1F_X_ATTACK = 1627,
EVENT_VANCE_CARBOS = 825,
EVENT_VICTORY_ROAD_HP_UP = 1703,
EVENT_WADE_HAS_BERRY = 811,
EVENT_WADE_HAS_BITTER_BERRY = 814,
EVENT_WADE_HAS_PRZCUREBERRY = 813,
EVENT_WADE_HAS_PSNCUREBERRY = 812,
EVENT_WALL_OPENED_IN_AERODACTYL_CHAMBER = 809,
EVENT_WALL_OPENED_IN_HO_OH_CHAMBER = 806,
EVENT_WALL_OPENED_IN_KABUTO_CHAMBER = 807,
EVENT_WALL_OPENED_IN_OMANYTE_CHAMBER = 808,
EVENT_WELCOMED_TO_POKECOM_CENTER = 810,
EVENT_WHIRL_ISLAND_SW_ULTRA_BALL = 1680,
EVENT_WILTON_HAS_GREAT_BALL = 816,
EVENT_WILTON_HAS_POKE_BALL = 817,
EVENT_WILTON_HAS_ULTRA_BALL = 815,
EVENT_WISE_TRIOS_ROOM_WISE_TRIO_1 = 1963,
EVENT_WISE_TRIOS_ROOM_WISE_TRIO_2 = 1964,
}
Gen2Flags.CRYSTAL_REMOVED = {
EVENT_ALAN_READY_FOR_REMATCH = true,
EVENT_ANTHONY_READY_FOR_REMATCH = true,
EVENT_ARNIE_READY_FOR_REMATCH = true,
EVENT_BEAT_FISHER_CHRIS = true,
EVENT_BEAT_FISHER_CHRIS2 = true,
EVENT_BEAT_FISHER_CHRIS3 = true,
EVENT_BEAT_SUPER_NERD_ERIC_UNUSED = true,
EVENT_BETH_READY_FOR_REMATCH = true,
EVENT_BEVERLY_READY_FOR_REMATCH = true,
EVENT_BRENT_READY_FOR_REMATCH = true,
EVENT_BURNED_TOWER_1F_BURN_HEAL = true,
EVENT_BURNED_TOWER_1F_X_SPEED = true,
EVENT_BURNED_TOWER_B1F_HIDDEN_BURN_HEAL = true,
EVENT_BURNED_TOWER_B1F_HIDDEN_NUGGET = true,
EVENT_BURNED_TOWER_B1F_HIDDEN_ULTRA_BALL = true,
EVENT_BURNED_TOWER_B1F_HP_UP = true,
EVENT_BURNED_TOWER_FIREBREATHER_DICK_ASHES = true,
EVENT_BURNED_TOWER_FIREBREATHER_DICK_NORMAL = true,
EVENT_CHAD_READY_FOR_REMATCH = true,
EVENT_CHRIS_ASKED_FOR_PHONE_NUMBER = true,
EVENT_CHRIS_READY_FOR_REMATCH = true,
EVENT_DANA_READY_FOR_REMATCH = true,
EVENT_DEREK_READY_FOR_REMATCH = true,
EVENT_ECRUTEAK_TIN_TOWER_ENTRANCE_SAGE_LEFT = true,
EVENT_ECRUTEAK_TIN_TOWER_ENTRANCE_SAGE_RIGHT = true,
EVENT_ERIN_READY_FOR_REMATCH = true,
EVENT_GAVEN_READY_FOR_REMATCH = true,
EVENT_GINA_READY_FOR_REMATCH = true,
EVENT_HUEY_READY_FOR_REMATCH = true,
EVENT_ILEX_FOREST_FARFETCHD_1 = true,
EVENT_ILEX_FOREST_FARFETCHD_10 = true,
EVENT_ILEX_FOREST_FARFETCHD_2 = true,
EVENT_ILEX_FOREST_FARFETCHD_3 = true,
EVENT_ILEX_FOREST_FARFETCHD_4 = true,
EVENT_ILEX_FOREST_FARFETCHD_5 = true,
EVENT_ILEX_FOREST_FARFETCHD_6 = true,
EVENT_ILEX_FOREST_FARFETCHD_7 = true,
EVENT_ILEX_FOREST_FARFETCHD_8 = true,
EVENT_ILEX_FOREST_FARFETCHD_9 = true,
EVENT_IRWIN_READY_FOR_REMATCH = true,
EVENT_JACK_READY_FOR_REMATCH = true,
EVENT_JOEY_READY_FOR_REMATCH = true,
EVENT_JOSE_READY_FOR_REMATCH = true,
EVENT_KENJI_READY_FOR_REMATCH = true,
EVENT_LAKE_OF_RAGE_ETHER_ON_STANDBY = true,
EVENT_LAKE_OF_RAGE_MAX_ETHER = true,
EVENT_LIZ_READY_FOR_REMATCH = true,
EVENT_MOUNT_MORTAR_B1F_FULL_HEAL = true,
EVENT_OLIVINE_LIGHTHOUSE_5F_GREAT_BALL = true,
EVENT_PARRY_READY_FOR_REMATCH = true,
EVENT_RALPH_READY_FOR_REMATCH = true,
EVENT_REENA_READY_FOR_REMATCH = true,
EVENT_ROUTE_31_ANTIDOTE = true,
EVENT_ROUTE_32_POTION = true,
EVENT_ROUTE_45_X_SPECIAL = true,
EVENT_ROUTE_46_DIRE_HIT = true,
EVENT_SILVER_CAVE_ROOM_1_X_ACCURACY = true,
EVENT_SPROUT_TOWER_2F_X_DEFEND = true,
EVENT_TEAM_ROCKET_BASE_B1F_X_ACCURACY = true,
EVENT_TEAM_ROCKET_BASE_B3F_DIRE_HIT = true,
EVENT_TIFFANY_READY_FOR_REMATCH = true,
EVENT_TIN_TOWER_4F_SUPER_POTION = true,
EVENT_TODD_READY_FOR_REMATCH = true,
EVENT_VANCE_READY_FOR_REMATCH = true,
EVENT_VICTORY_ROAD_X_SPECIAL = true,
EVENT_WADE_READY_FOR_REMATCH = true,
EVENT_WHIRL_ISLAND_SW_GUARD_SPEC = true,
EVENT_WILTON_READY_FOR_REMATCH = true,
}
local cache = {}
function Gen2Flags.byName(engine)
engine = (engine == "crystal") and "crystal" or "gs"
if cache[engine] then return cache[engine] end
local ids = {}
local ok, flags = pcall(require, "src.core.gen2.FlagNames")
if ok and type(flags) == "table" and type(flags.events) == "table" then
for name, id in pairs(flags.events) do ids[name] = id end
end
if engine == "crystal" then
for name in pairs(Gen2Flags.CRYSTAL_REMOVED) do ids[name] = nil end
for name, id in pairs(Gen2Flags.CRYSTAL_ADDED) do ids[name] = id end
end
cache[engine] = ids
return ids
end
function Gen2Flags.names(engine)
local names = {}
for name in pairs(Gen2Flags.byName(engine)) do names[#names + 1] = name end
table.sort(names)
return names
end
return Gen2Flags
+70
View File
@@ -1141,4 +1141,74 @@ function Ops.setPokerus(S, mon, value)
return Ops.mark(S, ("%s pokerus byte %d"):format(mon.species, want))
end
-- engine/pokemon/caught_data.asm:169-172
Ops.CAUGHT_TIMES = { "UNKNOWN", "MORN", "DAY", "NITE" }
local function caughtGuard(S, mon)
if not mon then return false end
if not Gen.hasCaughtData(S.save, S.version) then
return Ops.say(S, "This game has no caught data")
end
return true
end
function Ops.setCaughtTime(S, mon, value)
if not caughtGuard(S, mon) then return false end
local want = clamp(math.floor(tonumber(value) or 0), 0, 3)
if want == (mon.caughtTime or 0) then
return Ops.say(S, ("Caught time is already %s"):format(Ops.CAUGHT_TIMES[want + 1]))
end
mon.caughtTime = want
return Ops.mark(S, ("%s caught time %s")
:format(mon.species, Ops.CAUGHT_TIMES[want + 1]))
end
-- constants/pokemon_data_constants.asm:120-121
function Ops.setCaughtLevel(S, mon, value)
if not caughtGuard(S, mon) then return false end
local Mon = require("src.battle.gen2.Mon")
local want = clamp(math.floor(tonumber(value) or 0), 0, Mon.CAUGHT_LEVEL_MASK)
if want == (mon.caughtLevel or 0) then
return Ops.say(S, ("Caught level is already %d"):format(want))
end
mon.caughtLevel = want
return Ops.mark(S, ("%s caught level %d"):format(mon.species, want))
end
-- constants/landmark_constants.asm:111-113
function Ops.setCaughtLocation(S, mon, value)
if not caughtGuard(S, mon) then return false end
local Mon = require("src.battle.gen2.Mon")
local span = Mon.CAUGHT_LOCATION_MASK + 1
local want = math.floor(tonumber(value) or 0) % span
if want == (mon.caughtLocation or 0) then
return Ops.say(S, ("Caught location is already %s")
:format(Gen.landmarkName(S.data, want)))
end
mon.caughtLocation = want
return Ops.mark(S, ("%s caught at %s")
:format(mon.species, Gen.landmarkName(S.data, want)))
end
function Ops.setCaughtByGender(S, mon, gender)
if not caughtGuard(S, mon) then return false end
local Mon = require("src.battle.gen2.Mon")
local want = Mon.caughtGenderOf(gender)
if want == mon.caughtByGender then
return Ops.say(S, ("Caught by is already %s"):format(tostring(want or "none")))
end
mon.caughtByGender = want
return Ops.mark(S, ("%s caught by %s"):format(mon.species, tostring(want or "none")))
end
function Ops.setPlayerGender(S, gender)
if not Gen.hasPlayerGender(S.save, S.version) then
return Ops.say(S, "This game has no player gender")
end
if Gen.playerGender(S.save) == gender then
return Ops.say(S, ("Player is already %s"):format(tostring(gender)))
end
return Ops.mark(S, ("Player gender %s"):format(Gen.setPlayerGender(S.save, gender)))
end
return Ops
+1
View File
@@ -26,6 +26,7 @@ function State.new()
-- App.load's opts; nil in a bare `love . --editor` run.
version = nil,
slotId = nil,
modRoots = nil,
-- Hosted inside the launcher process (Edit on a save row) rather than a
-- standalone `--editor` window: Close returns to the launcher instead of
-- quitting, and App calls onClose() to do it.
+37 -2
View File
@@ -101,6 +101,31 @@ local function drawMoney(S, Kit, x, y, w, h)
end
end
-- engine/menus/init_gender.asm:55-56
local GENDERS = { { "BOY", "male" }, { "GIRL", "female" } }
local function trainerHeight(Kit, s, pad)
return pad * 2 + Kit.textHeight("caption") + 10 * s + 28 * s
end
local function drawTrainer(S, Kit, x, y, w, h)
local s = Kit.scale
local pad = 16 * s
Kit.card(x, y, w, h)
Kit.caption(x + pad, y + pad, "TRAINER")
Kit.textRight("mono", tostring((S.save.player and S.save.player.name) or "?"),
x + w - pad, y + pad, PAL.caption)
local cy = y + pad + Kit.textHeight("caption") + 10 * s
local chipW = (w - 2 * pad - 8 * s) / 2
local gender = Gen.playerGender(S.save)
for i, pair in ipairs(GENDERS) do
if Kit.chip(x + pad + (i - 1) * (chipW + 8 * s), cy, chipW, 28 * s,
pair[1], gender == pair[2], PAL.green, PAL.steel) then
Ops.setPlayerGender(S, pair[2])
end
end
end
local BADGE_COLS = 4
local function badgeHeight(S, Kit, s, pad)
@@ -273,6 +298,10 @@ function M.draw(S, Kit, x, y, w, h)
local listH = 300 * s
Kit.pushClip(x, y, w, h)
local cy = y - off
if Gen.hasPlayerGender(S.save, S.version) then
local trainerH = trainerHeight(Kit, s, pad)
drawTrainer(S, Kit, x, cy, w, trainerH); cy = cy + trainerH + gap
end
drawMoney(S, Kit, x, cy, w, moneyH); cy = cy + moneyH + gap
drawPicker(S, Kit, x, cy, w, pickH); cy = cy + pickH + gap
drawBadges(S, Kit, x, cy, w, badgeH); cy = cy + badgeH + gap
@@ -294,8 +323,14 @@ function M.draw(S, Kit, x, y, w, h)
-- made the old panel unusable.
local moneyH = moneyHeight(Kit, s, pad, S)
local badgeH = badgeHeight(S, Kit, s, pad)
drawMoney(S, Kit, x, y, leftW, moneyH)
drawPicker(S, Kit, x, y + moneyH + gap, leftW, h - moneyH - badgeH - 2 * gap)
local topH = 0
if Gen.hasPlayerGender(S.save, S.version) then
topH = trainerHeight(Kit, s, pad) + gap
drawTrainer(S, Kit, x, y, leftW, topH - gap)
end
drawMoney(S, Kit, x, y + topH, leftW, moneyH)
drawPicker(S, Kit, x, y + topH + moneyH + gap, leftW,
h - topH - moneyH - badgeH - 2 * gap)
drawBadges(S, Kit, x, y + h - badgeH, leftW, badgeH)
drawBag(S, Kit, bagX, y, listW, h)
drawPc(S, Kit, pcX, y, listW, h)
+67 -2
View File
@@ -147,6 +147,66 @@ local function drawDvRows(S, Kit, mon, cx, rowY, colW, rowH, rowGap)
end
end
-- engine/pokemon/caught_data.asm:168-199
local CAUGHT_BY = { { "-", "none" }, { "BOY", "boy" }, { "GIRL", "girl" } }
local function drawCaughtRows(S, Kit, mon, cx, y, inner, row)
local s = Kit.scale
local chipH = 22 * s
local gap = 6 * s
local tinyH = Kit.textHeight("tiny")
Kit.text("tiny", "CAUGHT", cx, y + (row - tinyH) / 2, PAL.caption)
local timeX = cx + 62 * s
local timeW = math.max(34 * s, (cx + inner - timeX - 3 * gap) / 4)
for i, label in ipairs(Ops.CAUGHT_TIMES) do
if Kit.chip(timeX + (i - 1) * (timeW + gap), y + (row - chipH) / 2,
timeW, chipH, label, (mon.caughtTime or 0) == i - 1, PAL.blue, PAL.steel) then
Ops.setCaughtTime(S, mon, i - 1)
end
end
y = y + row + gap
local btn = 24 * s
local lvX = cx + inner - 2 * btn - gap
if Kit.stepper(lvX, y + (row - btn) / 2, btn, btn, "-", { font = "small" }) then
Ops.setCaughtLevel(S, mon, (mon.caughtLevel or 0) - 1)
end
if Kit.stepper(lvX + btn + gap, y + (row - btn) / 2, btn, btn, "+",
{ font = "small" }) then
Ops.setCaughtLevel(S, mon, (mon.caughtLevel or 0) + 1)
end
Kit.textRight("tiny", ("MET LV %d"):format(mon.caughtLevel or 0), lvX - 10 * s,
y + (row - tinyH) / 2, PAL.text)
Kit.text("tiny", "OT", cx, y + (row - tinyH) / 2, PAL.caption)
local otX = cx + 26 * s
local otW = math.max(30 * s, (inner * 0.42 - 26 * s - 2 * gap) / 3)
for i, pair in ipairs(CAUGHT_BY) do
if Kit.chip(otX + (i - 1) * (otW + gap), y + (row - chipH) / 2, otW, chipH,
pair[1], (mon.caughtByGender or "none") == pair[2], PAL.blue, PAL.steel) then
Ops.setCaughtByGender(S, mon, pair[2])
end
end
y = y + row + gap
local whereX = cx + inner - 3 * btn - 2 * gap
if Kit.stepper(whereX, y + (row - btn) / 2, btn, btn, "-", { font = "small" }) then
Ops.setCaughtLocation(S, mon, (mon.caughtLocation or 0) - 1)
end
if Kit.stepper(whereX + btn + gap, y + (row - btn) / 2, btn, btn, "+",
{ font = "small" }) then
Ops.setCaughtLocation(S, mon, (mon.caughtLocation or 0) + 1)
end
if Kit.button(whereX + 2 * (btn + gap), y + (row - btn) / 2, btn, btn, "0",
{ kind = "danger", font = "micro", radius = 6 * s }) then
Ops.setCaughtLocation(S, mon, 0)
end
local where = ("WHERE %s"):format(Gen.landmarkName(S.data, mon.caughtLocation or 0))
Kit.text("tiny", Kit.ellipsize("tiny", where, whereX - 10 * s - cx), cx,
y + (row - tinyH) / 2, PAL.text)
return y + row + gap
end
local function drawMoveRows(S, Kit, mon, rightX, rowY, colW, rowH, rowGap)
local s = Kit.scale
for slot = 1, 4 do
@@ -231,6 +291,7 @@ function MonEditor.draw(S, Kit, x, y, w, h)
-- the field + Set row
local extraH = 0
if Gen.ofState(S) == 2 then extraH = 88 * s end
if Gen.hasCaughtData(S.save, S.version) then extraH = extraH + 102 * s end
local nickFieldH = 30 * s
local contentH = pad + headerH + 18 * s
+ capH + 10 * s + nickFieldH + 18 * s
@@ -341,7 +402,7 @@ function MonEditor.draw(S, Kit, x, y, w, h)
local colY = statsY + cellH + 18 * s
if Gen.ofState(S) == 2 then
local extraY = colY
Kit.caption(cx, extraY, "GOLD")
Kit.caption(cx, extraY, Gen.editionLabel(S.save, S.version))
extraY = extraY + capH + 8 * s
local row = 28 * s
Kit.text("tiny", "HELD " .. tostring(mon.item or "none"), cx, extraY, PAL.text)
@@ -371,7 +432,11 @@ function MonEditor.draw(S, Kit, x, y, w, h)
if mon.unownLetter then bits[#bits + 1] = "Unown " .. tostring(mon.unownLetter) end
Kit.text("tiny", table.concat(bits, " ") ~= "" and table.concat(bits, " ")
or "gender/shiny follow DVs", cx, extraY, PAL.caption)
colY = extraY + 22 * s
extraY = extraY + 22 * s
if Gen.hasCaughtData(S.save, S.version) then
extraY = drawCaughtRows(S, Kit, mon, cx, extraY, inner, row)
end
colY = extraY
end
if narrow then
-- stacked: DVs first, then moves, then the two actions side by side at