diff --git a/data/palettes_gbc.lua b/data/palettes_gbc.lua index 452c4c48..5c98c88b 100644 --- a/data/palettes_gbc.lua +++ b/data/palettes_gbc.lua @@ -1,7 +1,10 @@ -- Generated by tools/build_data.py. DO NOT EDIT. +-- extracted from https://github.com/Stewmath/pokered-gbc -- Red++ / pokered-gbc SuperPalettes (COLORS=RED++): 4 8-bit RGB -- colors per palette (color 0 first), including per-species --- pals and the GEN_2 MonsterPalettes assignment. +-- pals and the GEN_2 MonsterPalettes assignment, plus `world` +-- (true overworld tile/roof/sprite GBC coloring -- see this +-- file's module docstring for its shape). return { order = { "TOWNMAP2", @@ -1833,5 +1836,3841 @@ return { ZAPDOS = "ZAPDOS", ZUBAT = "ZUBAT", }, - source = "pokered-gbc data/super_palettes.asm + data/mon_palettes.asm", + source = "pokered-gbc data/super_palettes.asm + data/mon_palettes.asm + color/**", + world = { + groupColors = { + CAVERN = { + { + { 123, 115, 197 }, + { 90, 90, 156 }, + { 58, 58, 99 }, + { 0, 0, 0 }, + }, + { + { 123, 115, 197 }, + { 115, 58, 140 }, + { 107, 0, 66 }, + { 0, 0, 0 }, + }, + { + { 123, 115, 197 }, + { 66, 107, 156 }, + { 0, 90, 107 }, + { 0, 0, 0 }, + }, + { + { 123, 115, 197 }, + { 41, 41, 140 }, + { 25, 25, 82 }, + { 0, 0, 0 }, + }, + { + { 247, 247, 90 }, + { 132, 115, 148 }, + { 132, 115, 82 }, + { 0, 0, 0 }, + }, + { + { 123, 115, 197 }, + { 99, 74, 123 }, + { 66, 33, 41 }, + { 0, 0, 0 }, + }, + { + { 123, 115, 197 }, + { 107, 99, 189 }, + { 90, 74, 165 }, + { 0, 0, 0 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + CEMETERY = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + CLUB = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + DOJO = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + FACILITY = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + FOREST = { + { + { 222, 255, 222 }, + { 173, 173, 173 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 181, 255, 82 }, + { 197, 148, 58 }, + { 165, 123, 25 }, + { 58, 58, 58 }, + }, + { + { 181, 255, 82 }, + { 99, 206, 8 }, + { 41, 115, 0 }, + { 58, 58, 58 }, + }, + { + { 189, 189, 255 }, + { 148, 156, 255 }, + { 107, 99, 255 }, + { 58, 58, 58 }, + }, + { + { 222, 255, 222 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 222, 255, 222 }, + { 197, 148, 58 }, + { 165, 123, 25 }, + { 58, 58, 58 }, + }, + { + { 181, 255, 82 }, + { 165, 255, 115 }, + { 90, 189, 41 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + FOREST_GATE = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + GATE = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + GYM = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + HOUSE = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + INTERIOR = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 156, 82 }, + { 247, 82, 49 }, + { 0, 0, 0 }, + }, + }, + LAB = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 156, 82 }, + { 247, 82, 49 }, + { 0, 0, 0 }, + }, + }, + LOBBY = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 156, 82 }, + { 247, 82, 49 }, + { 0, 0, 0 }, + }, + }, + MANSION = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 156, 82 }, + { 247, 82, 49 }, + { 0, 0, 0 }, + }, + }, + MART = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 156, 82 }, + { 247, 82, 49 }, + { 0, 0, 0 }, + }, + }, + MUSEUM = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 173, 173, 173 }, + { 107, 107, 107 }, + { 0, 0, 0 }, + }, + }, + OVERWORLD = { + { + { 222, 255, 222 }, + { 173, 173, 173 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 222, 255, 222 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 181, 255, 82 }, + { 99, 206, 8 }, + { 41, 115, 0 }, + { 58, 58, 58 }, + }, + { + { 189, 189, 255 }, + { 148, 156, 255 }, + { 107, 99, 255 }, + { 58, 58, 58 }, + }, + { + { 222, 255, 222 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 222, 255, 222 }, + { 197, 148, 58 }, + { 165, 123, 25 }, + { 58, 58, 58 }, + }, + { + { 222, 255, 222 }, + { 165, 255, 115 }, + { 90, 189, 41 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + PLATEAU = { + { + { 222, 255, 222 }, + { 173, 173, 173 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 222, 255, 222 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 181, 255, 82 }, + { 99, 206, 8 }, + { 41, 115, 0 }, + { 58, 58, 58 }, + }, + { + { 189, 189, 255 }, + { 148, 156, 255 }, + { 107, 99, 255 }, + { 58, 58, 58 }, + }, + { + { 222, 255, 222 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 222, 255, 222 }, + { 197, 148, 58 }, + { 165, 123, 25 }, + { 58, 58, 58 }, + }, + { + { 222, 255, 222 }, + { 165, 255, 115 }, + { 90, 189, 41 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + POKECENTER = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 156, 82 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 156, 82 }, + { 247, 82, 49 }, + { 0, 0, 0 }, + }, + }, + REDS_HOUSE_1 = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + REDS_HOUSE_2 = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + SHIP = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + SHIP_PORT = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + UNDERGROUND = { + { + { 247, 230, 214 }, + { 156, 156, 156 }, + { 107, 107, 107 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 156, 197 }, + { 247, 82, 49 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 165, 8 }, + { 74, 107, 0 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 123, 132, 255 }, + { 74, 74, 255 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 255, 255, 58 }, + { 255, 132, 8 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 173, 140, 58 }, + { 132, 107, 25 }, + { 58, 58, 58 }, + }, + { + { 247, 230, 214 }, + { 140, 156, 255 }, + { 115, 132, 255 }, + { 58, 58, 58 }, + }, + { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 255, 255, 255 }, + { 0, 0, 0 }, + }, + }, + }, + roofByMapIndex = { + [0] = { + { 255, 255, 255 }, + { 197, 197, 197 }, + }, + [1] = { + { 0, 239, 58 }, + { 0, 197, 58 }, + }, + [2] = { + { 197, 206, 214 }, + { 165, 140, 156 }, + }, + [3] = { + { 115, 197, 255 }, + { 115, 165, 214 }, + }, + [4] = { + { 189, 99, 255 }, + { 156, 74, 197 }, + }, + [5] = { + { 239, 66, 0 }, + { 181, 66, 0 }, + }, + [6] = { + { 123, 214, 156 }, + { 25, 165, 90 }, + }, + [7] = { + { 255, 25, 148 }, + { 206, 25, 99 }, + }, + [8] = { + { 239, 0, 0 }, + { 181, 0, 0 }, + }, + [9] = { + { 132, 0, 255 }, + { 82, 0, 206 }, + }, + [10] = { + { 255, 222, 0 }, + { 230, 181, 0 }, + }, + [11] = { + { 255, 255, 255 }, + { 197, 197, 197 }, + }, + [12] = { + { 255, 255, 255 }, + { 197, 197, 197 }, + }, + [13] = { + { 0, 239, 58 }, + { 0, 197, 58 }, + }, + [14] = { + { 197, 206, 214 }, + { 165, 140, 156 }, + }, + [15] = { + { 197, 206, 214 }, + { 165, 140, 156 }, + }, + [16] = { + { 255, 222, 0 }, + { 230, 181, 0 }, + }, + [17] = { + { 239, 66, 0 }, + { 181, 66, 0 }, + }, + [18] = { + { 255, 222, 0 }, + { 230, 181, 0 }, + }, + [19] = { + { 255, 222, 0 }, + { 230, 181, 0 }, + }, + [20] = { + { 189, 99, 255 }, + { 156, 74, 197 }, + }, + [21] = { + { 189, 99, 255 }, + { 156, 74, 197 }, + }, + [22] = { + { 239, 66, 0 }, + { 181, 66, 0 }, + }, + [23] = { + { 239, 66, 0 }, + { 181, 66, 0 }, + }, + [24] = { + { 255, 25, 148 }, + { 206, 25, 99 }, + }, + [25] = { + { 255, 25, 148 }, + { 206, 25, 99 }, + }, + [26] = { + { 255, 25, 148 }, + { 206, 25, 99 }, + }, + [27] = { + { 123, 214, 156 }, + { 25, 165, 90 }, + }, + [28] = { + { 123, 214, 156 }, + { 25, 165, 90 }, + }, + [29] = { + { 123, 214, 156 }, + { 25, 165, 90 }, + }, + [30] = { + { 239, 0, 0 }, + { 181, 0, 0 }, + }, + [31] = { + { 239, 0, 0 }, + { 181, 0, 0 }, + }, + [32] = { + { 239, 0, 0 }, + { 181, 0, 0 }, + }, + [33] = { + { 132, 0, 255 }, + { 82, 0, 206 }, + }, + [34] = { + { 132, 0, 255 }, + { 82, 0, 206 }, + }, + [35] = { + { 115, 197, 255 }, + { 115, 165, 214 }, + }, + [36] = { + { 115, 197, 255 }, + { 115, 165, 214 }, + }, + }, + roofGroup = { + OVERWORLD = 6, + PLATEAU = 6, + }, + spriteAssignment = { + [0] = 0, + [1] = 1, + [2] = 3, + [3] = "random", + [4] = 0, + [5] = "random", + [6] = "random", + [7] = "random", + [8] = 0, + [9] = "random", + [10] = "random", + [11] = "random", + [12] = "random", + [13] = "random", + [14] = "random", + [15] = 1, + [16] = 1, + [17] = "random", + [18] = "random", + [19] = "random", + [20] = "random", + [21] = 2, + [22] = 1, + [23] = 3, + [24] = "random", + [25] = "random", + [26] = "random", + [27] = "random", + [28] = "random", + [29] = 0, + [30] = 3, + [31] = 3, + [32] = "random", + [33] = "random", + [34] = "random", + [35] = "random", + [36] = "random", + [37] = "random", + [38] = "random", + [39] = "random", + [40] = 0, + [41] = 2, + [42] = "random", + [43] = "random", + [44] = "random", + [45] = "random", + [46] = "random", + [47] = "random", + [48] = "random", + [49] = "random", + [50] = "random", + [51] = "random", + [52] = "random", + [53] = "random", + [54] = "random", + [55] = 0, + [56] = 1, + [57] = 3, + [58] = 0, + [59] = 0, + [60] = 0, + [61] = 0, + [62] = 3, + [63] = 3, + [64] = 0, + [65] = 3, + [66] = 0, + [67] = 3, + [68] = 3, + [69] = "random", + [70] = "random", + [71] = 3, + }, + spritePalettes = { + [0] = { + { 222, 255, 222 }, + { 255, 156, 82 }, + { 255, 58, 8 }, + { 0, 0, 0 }, + }, + [1] = { + { 222, 255, 222 }, + { 255, 156, 82 }, + { 82, 74, 255 }, + { 0, 0, 0 }, + }, + [2] = { + { 222, 255, 222 }, + { 255, 156, 82 }, + { 58, 189, 25 }, + { 0, 0, 0 }, + }, + [3] = { + { 222, 255, 222 }, + { 255, 156, 82 }, + { 123, 82, 25 }, + { 0, 0, 0 }, + }, + [4] = { + { 222, 255, 222 }, + { 255, 156, 82 }, + { 247, 82, 49 }, + { 0, 0, 0 }, + }, + [5] = { + { 255, 255, 255 }, + { 255, 255, 255 }, + { 107, 107, 107 }, + { 0, 0, 0 }, + }, + [6] = { + { 181, 255, 82 }, + { 99, 206, 8 }, + { 41, 115, 0 }, + { 58, 58, 58 }, + }, + [7] = { + { 222, 255, 222 }, + { 197, 148, 58 }, + { 165, 123, 25 }, + { 58, 58, 58 }, + }, + }, + tileGroups = { + CAVERN = { + [0] = 0, + [1] = 5, + [2] = 5, + [3] = 5, + [4] = 5, + [5] = 5, + [6] = 5, + [7] = 5, + [8] = 5, + [9] = 5, + [10] = 5, + [11] = 5, + [12] = 5, + [13] = 5, + [14] = 5, + [15] = 5, + [16] = 5, + [17] = 5, + [18] = 5, + [19] = 5, + [20] = 3, + [21] = 5, + [22] = 5, + [23] = 5, + [24] = 5, + [25] = 5, + [26] = 5, + [27] = 5, + [28] = 5, + [29] = 5, + [30] = 5, + [31] = 5, + [32] = 5, + [33] = 5, + [34] = 0, + [35] = 5, + [36] = 5, + [37] = 5, + [38] = 5, + [39] = 5, + [40] = 5, + [41] = 5, + [42] = 5, + [43] = 5, + [44] = 5, + [45] = 5, + [46] = 5, + [47] = 5, + [48] = 5, + [49] = 5, + [50] = 5, + [51] = 5, + [52] = 5, + [53] = 5, + [54] = 5, + [55] = 5, + [56] = 6, + [57] = 5, + [58] = 5, + [59] = 5, + [60] = 0, + [61] = 5, + [62] = 5, + [63] = 5, + [64] = 5, + [65] = 5, + [66] = 0, + [67] = 0, + [68] = 0, + [69] = 5, + [70] = 0, + [71] = 0, + [72] = 5, + [73] = 0, + [74] = 5, + [75] = 0, + [76] = 5, + [77] = 5, + [78] = 0, + [79] = 0, + [80] = 0, + [81] = 0, + [82] = 0, + [83] = 0, + [84] = 0, + [85] = 0, + [86] = 0, + [87] = 0, + [88] = 0, + [89] = 0, + [90] = 0, + [91] = 0, + [92] = 5, + [93] = 5, + [94] = 0, + [95] = 0, + }, + CEMETERY = { + [0] = 0, + [1] = 0, + [2] = 5, + [3] = 5, + [4] = 5, + [5] = 0, + [6] = 0, + [7] = 5, + [8] = 5, + [9] = 1, + [10] = 1, + [11] = 5, + [12] = 5, + [13] = 5, + [14] = 5, + [15] = 5, + [16] = 0, + [17] = 0, + [18] = 5, + [19] = 5, + [20] = 5, + [21] = 0, + [22] = 0, + [23] = 5, + [24] = 5, + [25] = 1, + [26] = 1, + [27] = 5, + [28] = 5, + [29] = 5, + [30] = 5, + [31] = 5, + [32] = 5, + [33] = 5, + [34] = 6, + [35] = 5, + [36] = 5, + [37] = 5, + [38] = 5, + [39] = 5, + [40] = 0, + [41] = 0, + [42] = 0, + [43] = 0, + [44] = 0, + [45] = 0, + [46] = 0, + [47] = 5, + [48] = 5, + [49] = 5, + [50] = 1, + [51] = 5, + [52] = 5, + [53] = 5, + [54] = 5, + [55] = 5, + [56] = 0, + [57] = 0, + [58] = 5, + [59] = 5, + [60] = 5, + [61] = 5, + [62] = 5, + [63] = 5, + [64] = 0, + [65] = 0, + [66] = 1, + [67] = 0, + [68] = 5, + [69] = 5, + [70] = 5, + [71] = 0, + [72] = 0, + [73] = 0, + [74] = 2, + [75] = 2, + [76] = 2, + [77] = 2, + [78] = 3, + [79] = 5, + [80] = 0, + [81] = 0, + [82] = 1, + [83] = 0, + [84] = 0, + [85] = 0, + [86] = 0, + [87] = 5, + [88] = 0, + [89] = 5, + [90] = 5, + [91] = 5, + [92] = 5, + [93] = 5, + [94] = 0, + [95] = 0, + }, + CLUB = { + [0] = 0, + [1] = 2, + [2] = 2, + [3] = 2, + [4] = 0, + [5] = 5, + [6] = 2, + [7] = 5, + [8] = 5, + [9] = 1, + [10] = 1, + [11] = 1, + [12] = 1, + [13] = 1, + [14] = 1, + [15] = 0, + [16] = 5, + [17] = 2, + [18] = 2, + [19] = 2, + [20] = 0, + [21] = 1, + [22] = 1, + [23] = 5, + [24] = 5, + [25] = 0, + [26] = 1, + [27] = 1, + [28] = 1, + [29] = 1, + [30] = 6, + [31] = 6, + [32] = 0, + [33] = 0, + [34] = 0, + [35] = 0, + [36] = 2, + [37] = 2, + [38] = 2, + [39] = 2, + [40] = 1, + [41] = 1, + [42] = 2, + [43] = 2, + [44] = 1, + [45] = 1, + [46] = 1, + [47] = 1, + [48] = 1, + [49] = 1, + [50] = 1, + [51] = 1, + [52] = 1, + [53] = 1, + [54] = 5, + [55] = 0, + [56] = 0, + [57] = 0, + [58] = 0, + [59] = 0, + [60] = 0, + [61] = 0, + [62] = 0, + [63] = 6, + [64] = 0, + [65] = 0, + [66] = 6, + [67] = 6, + [68] = 6, + [69] = 6, + [70] = 6, + [71] = 5, + [72] = 5, + [73] = 5, + [74] = 5, + [75] = 0, + [76] = 0, + [77] = 0, + [78] = 0, + [79] = 0, + [80] = 0, + [81] = 0, + [82] = 0, + [83] = 0, + [84] = 0, + [85] = 0, + [86] = 0, + [87] = 0, + [88] = 0, + [89] = 0, + [90] = 0, + [91] = 0, + [92] = 0, + [93] = 0, + [94] = 0, + [95] = 0, + }, + DOJO = { + [0] = 6, + [1] = 0, + [2] = 5, + [3] = 1, + [4] = 0, + [5] = 0, + [6] = 1, + [7] = 5, + [8] = 5, + [9] = 0, + [10] = 0, + [11] = 0, + [12] = 0, + [13] = 5, + [14] = 5, + [15] = 0, + [16] = 6, + [17] = 0, + [18] = 5, + [19] = 5, + [20] = 3, + [21] = 0, + [22] = 1, + [23] = 5, + [24] = 5, + [25] = 0, + [26] = 0, + [27] = 0, + [28] = 0, + [29] = 5, + [30] = 5, + [31] = 0, + [32] = 0, + [33] = 0, + [34] = 5, + [35] = 5, + [36] = 6, + [37] = 6, + [38] = 6, + [39] = 6, + [40] = 0, + [41] = 5, + [42] = 5, + [43] = 2, + [44] = 2, + [45] = 2, + [46] = 2, + [47] = 2, + [48] = 0, + [49] = 0, + [50] = 5, + [51] = 5, + [52] = 6, + [53] = 6, + [54] = 0, + [55] = 0, + [56] = 5, + [57] = 5, + [58] = 5, + [59] = 5, + [60] = 1, + [61] = 1, + [62] = 6, + [63] = 1, + [64] = 2, + [65] = 2, + [66] = 6, + [67] = 6, + [68] = 0, + [69] = 0, + [70] = 0, + [71] = 0, + [72] = 0, + [73] = 0, + [74] = 0, + [75] = 0, + [76] = 1, + [77] = 1, + [78] = 5, + [79] = 5, + [80] = 2, + [81] = 2, + [82] = 6, + [83] = 6, + [84] = 0, + [85] = 0, + [86] = 0, + [87] = 0, + [88] = 5, + [89] = 5, + [90] = 5, + [91] = 0, + [92] = 0, + [93] = 0, + [94] = 5, + [95] = 5, + }, + FACILITY = { + [0] = 0, + [1] = 1, + [2] = 5, + [3] = 5, + [4] = 5, + [5] = 2, + [6] = 2, + [7] = 5, + [8] = 4, + [9] = 2, + [10] = 2, + [11] = 5, + [12] = 5, + [13] = 5, + [14] = 5, + [15] = 5, + [16] = 0, + [17] = 0, + [18] = 5, + [19] = 5, + [20] = 3, + [21] = 2, + [22] = 2, + [23] = 5, + [24] = 4, + [25] = 2, + [26] = 2, + [27] = 5, + [28] = 5, + [29] = 5, + [30] = 5, + [31] = 5, + [32] = 1, + [33] = 1, + [34] = 1, + [35] = 5, + [36] = 4, + [37] = 4, + [38] = 5, + [39] = 5, + [40] = 2, + [41] = 2, + [42] = 0, + [43] = 0, + [44] = 0, + [45] = 0, + [46] = 0, + [47] = 5, + [48] = 1, + [49] = 1, + [50] = 1, + [51] = 0, + [52] = 5, + [53] = 5, + [54] = 5, + [55] = 5, + [56] = 2, + [57] = 2, + [58] = 5, + [59] = 5, + [60] = 5, + [61] = 5, + [62] = 5, + [63] = 5, + [64] = 5, + [65] = 5, + [66] = 1, + [67] = 5, + [68] = 5, + [69] = 5, + [70] = 5, + [71] = 5, + [72] = 5, + [73] = 5, + [74] = 5, + [75] = 5, + [76] = 5, + [77] = 5, + [78] = 5, + [79] = 5, + [80] = 5, + [81] = 5, + [82] = 1, + [83] = 5, + [84] = 5, + [85] = 1, + [86] = 0, + [87] = 5, + [88] = 0, + [89] = 5, + [90] = 0, + [91] = 0, + [92] = 5, + [93] = 5, + [94] = 0, + [95] = 0, + }, + FOREST = { + [0] = 2, + [1] = 4, + [2] = 5, + [3] = 5, + [4] = 6, + [5] = 6, + [6] = 6, + [7] = 6, + [8] = 5, + [9] = 5, + [10] = 5, + [11] = 5, + [12] = 5, + [13] = 2, + [14] = 5, + [15] = 5, + [16] = 0, + [17] = 0, + [18] = 5, + [19] = 5, + [20] = 3, + [21] = 6, + [22] = 6, + [23] = 6, + [24] = 5, + [25] = 5, + [26] = 5, + [27] = 5, + [28] = 5, + [29] = 5, + [30] = 5, + [31] = 5, + [32] = 2, + [33] = 5, + [34] = 5, + [35] = 6, + [36] = 6, + [37] = 6, + [38] = 6, + [39] = 6, + [40] = 5, + [41] = 5, + [42] = 5, + [43] = 5, + [44] = 5, + [45] = 5, + [46] = 5, + [47] = 5, + [48] = 2, + [49] = 5, + [50] = 5, + [51] = 0, + [52] = 2, + [53] = 5, + [54] = 5, + [55] = 1, + [56] = 5, + [57] = 2, + [58] = 5, + [59] = 5, + [60] = 5, + [61] = 5, + [62] = 5, + [63] = 5, + [64] = 5, + [65] = 5, + [66] = 5, + [67] = 5, + [68] = 0, + [69] = 0, + [70] = 0, + [71] = 0, + [72] = 5, + [73] = 5, + [74] = 5, + [75] = 5, + [76] = 5, + [77] = 5, + [78] = 5, + [79] = 2, + [80] = 2, + [81] = 2, + [82] = 2, + [83] = 2, + [84] = 2, + [85] = 2, + [86] = 2, + [87] = 2, + [88] = 2, + [89] = 2, + [90] = 2, + [91] = 2, + [92] = 2, + [93] = 2, + [94] = 2, + [95] = 2, + }, + FOREST_GATE = { + [0] = 5, + [1] = 6, + [2] = 0, + [3] = 0, + [4] = 1, + [5] = 2, + [6] = 2, + [7] = 5, + [8] = 5, + [9] = 5, + [10] = 5, + [11] = 5, + [12] = 5, + [13] = 5, + [14] = 5, + [15] = 5, + [16] = 0, + [17] = 0, + [18] = 0, + [19] = 0, + [20] = 1, + [21] = 2, + [22] = 2, + [23] = 5, + [24] = 5, + [25] = 6, + [26] = 5, + [27] = 5, + [28] = 5, + [29] = 5, + [30] = 5, + [31] = 5, + [32] = 5, + [33] = 5, + [34] = 5, + [35] = 5, + [36] = 0, + [37] = 5, + [38] = 5, + [39] = 6, + [40] = 6, + [41] = 5, + [42] = 5, + [43] = 5, + [44] = 6, + [45] = 6, + [46] = 0, + [47] = 0, + [48] = 5, + [49] = 6, + [50] = 5, + [51] = 5, + [52] = 0, + [53] = 5, + [54] = 5, + [55] = 1, + [56] = 1, + [57] = 0, + [58] = 6, + [59] = 5, + [60] = 6, + [61] = 6, + [62] = 0, + [63] = 0, + [64] = 0, + [65] = 0, + [66] = 0, + [67] = 0, + [68] = 0, + [69] = 0, + [70] = 0, + [71] = 0, + [72] = 5, + [73] = 0, + [74] = 5, + [75] = 0, + [76] = 5, + [77] = 5, + [78] = 0, + [79] = 0, + [80] = 0, + [81] = 0, + [82] = 0, + [83] = 0, + [84] = 0, + [85] = 0, + [86] = 0, + [87] = 0, + [88] = 0, + [89] = 0, + [90] = 0, + [91] = 0, + [92] = 5, + [93] = 5, + [94] = 1, + [95] = 0, + }, + GATE = { + [0] = 5, + [1] = 2, + [2] = 0, + [3] = 0, + [4] = 1, + [5] = 2, + [6] = 2, + [7] = 5, + [8] = 5, + [9] = 5, + [10] = 5, + [11] = 5, + [12] = 5, + [13] = 5, + [14] = 5, + [15] = 5, + [16] = 0, + [17] = 0, + [18] = 0, + [19] = 0, + [20] = 1, + [21] = 2, + [22] = 2, + [23] = 5, + [24] = 5, + [25] = 6, + [26] = 5, + [27] = 5, + [28] = 5, + [29] = 5, + [30] = 5, + [31] = 5, + [32] = 5, + [33] = 5, + [34] = 5, + [35] = 5, + [36] = 0, + [37] = 5, + [38] = 5, + [39] = 6, + [40] = 6, + [41] = 5, + [42] = 5, + [43] = 5, + [44] = 6, + [45] = 6, + [46] = 0, + [47] = 0, + [48] = 5, + [49] = 6, + [50] = 5, + [51] = 5, + [52] = 0, + [53] = 5, + [54] = 5, + [55] = 1, + [56] = 1, + [57] = 0, + [58] = 6, + [59] = 5, + [60] = 6, + [61] = 6, + [62] = 0, + [63] = 0, + [64] = 0, + [65] = 0, + [66] = 0, + [67] = 0, + [68] = 0, + [69] = 0, + [70] = 0, + [71] = 0, + [72] = 5, + [73] = 0, + [74] = 5, + [75] = 0, + [76] = 5, + [77] = 5, + [78] = 0, + [79] = 0, + [80] = 0, + [81] = 0, + [82] = 0, + [83] = 0, + [84] = 0, + [85] = 0, + [86] = 0, + [87] = 0, + [88] = 0, + [89] = 0, + [90] = 0, + [91] = 0, + [92] = 5, + [93] = 5, + [94] = 1, + [95] = 0, + }, + GYM = { + [0] = 6, + [1] = 0, + [2] = 5, + [3] = 1, + [4] = 0, + [5] = 0, + [6] = 1, + [7] = 5, + [8] = 5, + [9] = 0, + [10] = 0, + [11] = 0, + [12] = 0, + [13] = 5, + [14] = 5, + [15] = 0, + [16] = 6, + [17] = 0, + [18] = 5, + [19] = 5, + [20] = 3, + [21] = 0, + [22] = 1, + [23] = 5, + [24] = 5, + [25] = 0, + [26] = 0, + [27] = 0, + [28] = 0, + [29] = 5, + [30] = 5, + [31] = 0, + [32] = 0, + [33] = 0, + [34] = 5, + [35] = 5, + [36] = 6, + [37] = 6, + [38] = 6, + [39] = 6, + [40] = 0, + [41] = 5, + [42] = 5, + [43] = 2, + [44] = 2, + [45] = 2, + [46] = 2, + [47] = 2, + [48] = 0, + [49] = 0, + [50] = 5, + [51] = 5, + [52] = 6, + [53] = 6, + [54] = 0, + [55] = 0, + [56] = 5, + [57] = 5, + [58] = 5, + [59] = 5, + [60] = 1, + [61] = 1, + [62] = 6, + [63] = 1, + [64] = 2, + [65] = 2, + [66] = 6, + [67] = 6, + [68] = 0, + [69] = 0, + [70] = 0, + [71] = 0, + [72] = 0, + [73] = 0, + [74] = 0, + [75] = 0, + [76] = 1, + [77] = 1, + [78] = 5, + [79] = 5, + [80] = 2, + [81] = 2, + [82] = 6, + [83] = 6, + [84] = 0, + [85] = 0, + [86] = 0, + [87] = 0, + [88] = 5, + [89] = 5, + [90] = 5, + [91] = 0, + [92] = 0, + [93] = 0, + [94] = 5, + [95] = 5, + }, + HOUSE = { + [0] = 5, + [1] = 0, + [2] = 0, + [3] = 0, + [4] = 1, + [5] = 0, + [6] = 3, + [7] = 3, + [8] = 2, + [9] = 2, + [10] = 2, + [11] = 2, + [12] = 2, + [13] = 2, + [14] = 5, + [15] = 5, + [16] = 0, + [17] = 0, + [18] = 0, + [19] = 0, + [20] = 1, + [21] = 0, + [22] = 3, + [23] = 3, + [24] = 5, + [25] = 5, + [26] = 5, + [27] = 5, + [28] = 0, + [29] = 0, + [30] = 5, + [31] = 5, + [32] = 0, + [33] = 0, + [34] = 3, + [35] = 5, + [36] = 5, + [37] = 5, + [38] = 5, + [39] = 5, + [40] = 5, + [41] = 5, + [42] = 2, + [43] = 2, + [44] = 5, + [45] = 5, + [46] = 5, + [47] = 5, + [48] = 5, + [49] = 5, + [50] = 5, + [51] = 5, + [52] = 5, + [53] = 5, + [54] = 5, + [55] = 5, + [56] = 5, + [57] = 5, + [58] = 5, + [59] = 5, + [60] = 5, + [61] = 5, + [62] = 5, + [63] = 5, + [64] = 0, + [65] = 0, + [66] = 0, + [67] = 0, + [68] = 0, + [69] = 0, + [70] = 5, + [71] = 5, + [72] = 5, + [73] = 5, + [74] = 5, + [75] = 5, + [76] = 5, + [77] = 5, + [78] = 5, + [79] = 5, + [80] = 5, + [81] = 5, + [82] = 5, + [83] = 5, + [84] = 5, + [85] = 5, + [86] = 5, + [87] = 5, + [88] = 5, + [89] = 5, + [90] = 5, + [91] = 5, + [92] = 5, + [93] = 5, + [94] = 5, + [95] = 5, + }, + INTERIOR = { + [0] = 0, + [1] = 2, + [2] = 2, + [3] = 0, + [4] = 0, + [5] = 5, + [6] = 5, + [7] = 2, + [8] = 2, + [9] = 2, + [10] = 2, + [11] = 5, + [12] = 5, + [13] = 5, + [14] = 5, + [15] = 2, + [16] = 2, + [17] = 5, + [18] = 5, + [19] = 1, + [20] = 1, + [21] = 5, + [22] = 5, + [23] = 2, + [24] = 2, + [25] = 2, + [26] = 2, + [27] = 5, + [28] = 5, + [29] = 5, + [30] = 5, + [31] = 0, + [32] = 0, + [33] = 5, + [34] = 5, + [35] = 1, + [36] = 1, + [37] = 0, + [38] = 0, + [39] = 2, + [40] = 2, + [41] = 2, + [42] = 2, + [43] = 0, + [44] = 0, + [45] = 0, + [46] = 0, + [47] = 0, + [48] = 0, + [49] = 1, + [50] = 1, + [51] = 1, + [52] = 0, + [53] = 0, + [54] = 0, + [55] = 2, + [56] = 2, + [57] = 2, + [58] = 2, + [59] = 0, + [60] = 0, + [61] = 0, + [62] = 0, + [63] = 5, + [64] = 5, + [65] = 1, + [66] = 1, + [67] = 1, + [68] = 1, + [69] = 0, + [70] = 1, + [71] = 1, + [72] = 5, + [73] = 5, + [74] = 5, + [75] = 5, + [76] = 5, + [77] = 5, + [78] = 5, + [79] = 5, + [80] = 2, + [81] = 5, + [82] = 5, + [83] = 1, + [84] = 1, + [85] = 1, + [86] = 1, + [87] = 0, + [88] = 5, + [89] = 0, + [90] = 0, + [91] = 5, + [92] = 5, + [93] = 5, + [94] = 5, + [95] = 0, + }, + LAB = { + [0] = 0, + [1] = 6, + [2] = 5, + [3] = 5, + [4] = 5, + [5] = 5, + [6] = 0, + [7] = 0, + [8] = 5, + [9] = 5, + [10] = 5, + [11] = 5, + [12] = 2, + [13] = 2, + [14] = 0, + [15] = 0, + [16] = 5, + [17] = 5, + [18] = 5, + [19] = 5, + [20] = 5, + [21] = 5, + [22] = 0, + [23] = 0, + [24] = 0, + [25] = 0, + [26] = 5, + [27] = 5, + [28] = 6, + [29] = 6, + [30] = 0, + [31] = 0, + [32] = 5, + [33] = 5, + [34] = 5, + [35] = 5, + [36] = 6, + [37] = 6, + [38] = 0, + [39] = 1, + [40] = 5, + [41] = 5, + [42] = 5, + [43] = 5, + [44] = 2, + [45] = 2, + [46] = 5, + [47] = 5, + [48] = 5, + [49] = 5, + [50] = 5, + [51] = 5, + [52] = 1, + [53] = 1, + [54] = 0, + [55] = 1, + [56] = 0, + [57] = 0, + [58] = 5, + [59] = 5, + [60] = 2, + [61] = 2, + [62] = 5, + [63] = 5, + [64] = 5, + [65] = 5, + [66] = 5, + [67] = 5, + [68] = 5, + [69] = 5, + [70] = 5, + [71] = 6, + [72] = 5, + [73] = 5, + [74] = 5, + [75] = 5, + [76] = 1, + [77] = 1, + [78] = 6, + [79] = 0, + [80] = 5, + [81] = 5, + [82] = 5, + [83] = 5, + [84] = 5, + [85] = 5, + [86] = 5, + [87] = 6, + [88] = 6, + [89] = 6, + [90] = 5, + [91] = 5, + [92] = 0, + [93] = 0, + [94] = 0, + [95] = 0, + }, + LOBBY = { + [0] = 0, + [1] = 1, + [2] = 1, + [3] = 1, + [4] = 1, + [5] = 5, + [6] = 1, + [7] = 6, + [8] = 6, + [9] = 5, + [10] = 5, + [11] = 5, + [12] = 5, + [13] = 5, + [14] = 6, + [15] = 6, + [16] = 0, + [17] = 5, + [18] = 1, + [19] = 1, + [20] = 1, + [21] = 5, + [22] = 1, + [23] = 6, + [24] = 6, + [25] = 5, + [26] = 5, + [27] = 5, + [28] = 5, + [29] = 5, + [30] = 6, + [31] = 6, + [32] = 4, + [33] = 1, + [34] = 5, + [35] = 5, + [36] = 5, + [37] = 5, + [38] = 5, + [39] = 5, + [40] = 5, + [41] = 5, + [42] = 0, + [43] = 0, + [44] = 0, + [45] = 0, + [46] = 0, + [47] = 0, + [48] = 5, + [49] = 5, + [50] = 5, + [51] = 5, + [52] = 5, + [53] = 5, + [54] = 5, + [55] = 6, + [56] = 5, + [57] = 5, + [58] = 0, + [59] = 0, + [60] = 0, + [61] = 0, + [62] = 0, + [63] = 0, + [64] = 0, + [65] = 0, + [66] = 0, + [67] = 0, + [68] = 6, + [69] = 0, + [70] = 5, + [71] = 5, + [72] = 2, + [73] = 2, + [74] = 1, + [75] = 5, + [76] = 5, + [77] = 5, + [78] = 5, + [79] = 5, + [80] = 0, + [81] = 0, + [82] = 0, + [83] = 0, + [84] = 6, + [85] = 0, + [86] = 0, + [87] = 0, + [88] = 2, + [89] = 2, + [90] = 5, + [91] = 1, + [92] = 5, + [93] = 5, + [94] = 0, + [95] = 0, + }, + MANSION = { + [0] = 0, + [1] = 5, + [2] = 0, + [3] = 0, + [4] = 1, + [5] = 6, + [6] = 5, + [7] = 5, + [8] = 2, + [9] = 2, + [10] = 5, + [11] = 5, + [12] = 5, + [13] = 5, + [14] = 5, + [15] = 0, + [16] = 0, + [17] = 6, + [18] = 0, + [19] = 0, + [20] = 1, + [21] = 4, + [22] = 5, + [23] = 5, + [24] = 5, + [25] = 5, + [26] = 5, + [27] = 5, + [28] = 5, + [29] = 5, + [30] = 5, + [31] = 0, + [32] = 6, + [33] = 0, + [34] = 5, + [35] = 5, + [36] = 5, + [37] = 5, + [38] = 5, + [39] = 5, + [40] = 4, + [41] = 5, + [42] = 0, + [43] = 0, + [44] = 0, + [45] = 0, + [46] = 0, + [47] = 0, + [48] = 5, + [49] = 0, + [50] = 5, + [51] = 5, + [52] = 5, + [53] = 5, + [54] = 5, + [55] = 5, + [56] = 5, + [57] = 5, + [58] = 5, + [59] = 5, + [60] = 5, + [61] = 0, + [62] = 0, + [63] = 0, + [64] = 5, + [65] = 5, + [66] = 5, + [67] = 5, + [68] = 2, + [69] = 2, + [70] = 5, + [71] = 5, + [72] = 5, + [73] = 5, + [74] = 5, + [75] = 5, + [76] = 5, + [77] = 5, + [78] = 5, + [79] = 5, + [80] = 5, + [81] = 2, + [82] = 2, + [83] = 2, + [84] = 2, + [85] = 5, + [86] = 5, + [87] = 5, + [88] = 5, + [89] = 5, + [90] = 5, + [91] = 5, + [92] = 5, + [93] = 5, + [94] = 5, + [95] = 5, + }, + MART = { + [0] = 0, + [1] = 0, + [2] = 1, + [3] = 1, + [4] = 5, + [5] = 5, + [6] = 0, + [7] = 0, + [8] = 6, + [9] = 0, + [10] = 6, + [11] = 0, + [12] = 1, + [13] = 0, + [14] = 6, + [15] = 6, + [16] = 6, + [17] = 0, + [18] = 1, + [19] = 1, + [20] = 5, + [21] = 5, + [22] = 0, + [23] = 1, + [24] = 5, + [25] = 5, + [26] = 0, + [27] = 0, + [28] = 1, + [29] = 1, + [30] = 6, + [31] = 6, + [32] = 2, + [33] = 2, + [34] = 5, + [35] = 5, + [36] = 1, + [37] = 7, + [38] = 1, + [39] = 1, + [40] = 5, + [41] = 6, + [42] = 1, + [43] = 1, + [44] = 6, + [45] = 6, + [46] = 6, + [47] = 6, + [48] = 2, + [49] = 2, + [50] = 5, + [51] = 5, + [52] = 1, + [53] = 7, + [54] = 0, + [55] = 1, + [56] = 6, + [57] = 0, + [58] = 0, + [59] = 0, + [60] = 0, + [61] = 0, + [62] = 6, + [63] = 6, + [64] = 0, + [65] = 0, + [66] = 0, + [67] = 0, + [68] = 0, + [69] = 0, + [70] = 0, + [71] = 0, + [72] = 0, + [73] = 0, + [74] = 0, + [75] = 0, + [76] = 6, + [77] = 6, + [78] = 4, + [79] = 4, + [80] = 0, + [81] = 0, + [82] = 0, + [83] = 0, + [84] = 0, + [85] = 0, + [86] = 0, + [87] = 0, + [88] = 0, + [89] = 5, + [90] = 5, + [91] = 5, + [92] = 0, + [93] = 0, + [94] = 0, + [95] = 0, + }, + MUSEUM = { + [0] = 5, + [1] = 1, + [2] = 0, + [3] = 0, + [4] = 1, + [5] = 2, + [6] = 2, + [7] = 5, + [8] = 5, + [9] = 5, + [10] = 5, + [11] = 5, + [12] = 5, + [13] = 5, + [14] = 5, + [15] = 5, + [16] = 0, + [17] = 0, + [18] = 0, + [19] = 0, + [20] = 1, + [21] = 2, + [22] = 2, + [23] = 5, + [24] = 5, + [25] = 6, + [26] = 5, + [27] = 5, + [28] = 5, + [29] = 5, + [30] = 5, + [31] = 5, + [32] = 5, + [33] = 5, + [34] = 5, + [35] = 5, + [36] = 0, + [37] = 5, + [38] = 5, + [39] = 6, + [40] = 6, + [41] = 5, + [42] = 5, + [43] = 5, + [44] = 6, + [45] = 6, + [46] = 6, + [47] = 6, + [48] = 6, + [49] = 6, + [50] = 5, + [51] = 5, + [52] = 0, + [53] = 5, + [54] = 5, + [55] = 1, + [56] = 1, + [57] = 0, + [58] = 6, + [59] = 5, + [60] = 6, + [61] = 6, + [62] = 0, + [63] = 0, + [64] = 0, + [65] = 0, + [66] = 0, + [67] = 0, + [68] = 0, + [69] = 0, + [70] = 6, + [71] = 6, + [72] = 5, + [73] = 0, + [74] = 5, + [75] = 0, + [76] = 5, + [77] = 5, + [78] = 0, + [79] = 0, + [80] = 0, + [81] = 0, + [82] = 0, + [83] = 0, + [84] = 0, + [85] = 0, + [86] = 0, + [87] = 0, + [88] = 0, + [89] = 0, + [90] = 0, + [91] = 0, + [92] = 5, + [93] = 5, + [94] = 1, + [95] = 0, + }, + OVERWORLD = { + [0] = 0, + [1] = 5, + [2] = 5, + [3] = 1, + [4] = 5, + [5] = 6, + [6] = 6, + [7] = 6, + [8] = 6, + [9] = 6, + [10] = 4, + [11] = 4, + [12] = 4, + [13] = 0, + [14] = 5, + [15] = 0, + [16] = 0, + [17] = 5, + [18] = 6, + [19] = 5, + [20] = 3, + [21] = 6, + [22] = 6, + [23] = 6, + [24] = 6, + [25] = 6, + [26] = 0, + [27] = 5, + [28] = 5, + [29] = 2, + [30] = 5, + [31] = 0, + [32] = 0, + [33] = 0, + [34] = 0, + [35] = 0, + [36] = 5, + [37] = 6, + [38] = 6, + [39] = 5, + [40] = 6, + [41] = 6, + [42] = 0, + [43] = 0, + [44] = 2, + [45] = 2, + [46] = 2, + [47] = 0, + [48] = 2, + [49] = 0, + [50] = 5, + [51] = 5, + [52] = 5, + [53] = 5, + [54] = 5, + [55] = 5, + [56] = 6, + [57] = 0, + [58] = 0, + [59] = 0, + [60] = 5, + [61] = 2, + [62] = 2, + [63] = 0, + [64] = 2, + [65] = 2, + [66] = 0, + [67] = 0, + [68] = 0, + [69] = 0, + [70] = 0, + [71] = 0, + [72] = 5, + [73] = 5, + [74] = 0, + [75] = 5, + [76] = 6, + [77] = 6, + [78] = 0, + [79] = 0, + [80] = 2, + [81] = 2, + [82] = 2, + [83] = 6, + [84] = 5, + [85] = 5, + [86] = 0, + [87] = 0, + [88] = 5, + [89] = 5, + [90] = 6, + [91] = 5, + [92] = 6, + [93] = 6, + [94] = 0, + [95] = 0, + }, + PLATEAU = { + [0] = 0, + [1] = 5, + [2] = 5, + [3] = 0, + [4] = 0, + [5] = 5, + [6] = 5, + [7] = 2, + [8] = 2, + [9] = 0, + [10] = 0, + [11] = 1, + [12] = 1, + [13] = 5, + [14] = 5, + [15] = 5, + [16] = 5, + [17] = 5, + [18] = 5, + [19] = 5, + [20] = 3, + [21] = 5, + [22] = 5, + [23] = 2, + [24] = 2, + [25] = 0, + [26] = 0, + [27] = 1, + [28] = 1, + [29] = 5, + [30] = 5, + [31] = 5, + [32] = 0, + [33] = 0, + [34] = 5, + [35] = 2, + [36] = 5, + [37] = 5, + [38] = 5, + [39] = 5, + [40] = 5, + [41] = 5, + [42] = 5, + [43] = 5, + [44] = 2, + [45] = 2, + [46] = 0, + [47] = 0, + [48] = 5, + [49] = 5, + [50] = 5, + [51] = 5, + [52] = 5, + [53] = 5, + [54] = 5, + [55] = 5, + [56] = 0, + [57] = 5, + [58] = 5, + [59] = 5, + [60] = 5, + [61] = 6, + [62] = 6, + [63] = 6, + [64] = 6, + [65] = 6, + [66] = 6, + [67] = 6, + [68] = 6, + [69] = 2, + [70] = 0, + [71] = 0, + [72] = 0, + [73] = 0, + [74] = 0, + [75] = 0, + [76] = 0, + [77] = 0, + [78] = 0, + [79] = 0, + [80] = 0, + [81] = 0, + [82] = 0, + [83] = 0, + [84] = 0, + [85] = 0, + [86] = 0, + [87] = 0, + [88] = 0, + [89] = 0, + [90] = 0, + [91] = 0, + [92] = 0, + [93] = 0, + [94] = 0, + [95] = 0, + }, + POKECENTER = { + [0] = 0, + [1] = 0, + [2] = 1, + [3] = 1, + [4] = 5, + [5] = 5, + [6] = 0, + [7] = 0, + [8] = 6, + [9] = 0, + [10] = 6, + [11] = 0, + [12] = 1, + [13] = 0, + [14] = 6, + [15] = 6, + [16] = 6, + [17] = 0, + [18] = 1, + [19] = 1, + [20] = 5, + [21] = 5, + [22] = 0, + [23] = 1, + [24] = 5, + [25] = 5, + [26] = 0, + [27] = 0, + [28] = 1, + [29] = 1, + [30] = 6, + [31] = 6, + [32] = 2, + [33] = 2, + [34] = 5, + [35] = 5, + [36] = 1, + [37] = 4, + [38] = 1, + [39] = 1, + [40] = 5, + [41] = 6, + [42] = 1, + [43] = 1, + [44] = 6, + [45] = 6, + [46] = 6, + [47] = 6, + [48] = 2, + [49] = 2, + [50] = 5, + [51] = 5, + [52] = 1, + [53] = 4, + [54] = 0, + [55] = 1, + [56] = 6, + [57] = 0, + [58] = 0, + [59] = 0, + [60] = 0, + [61] = 0, + [62] = 6, + [63] = 6, + [64] = 0, + [65] = 0, + [66] = 0, + [67] = 0, + [68] = 0, + [69] = 0, + [70] = 0, + [71] = 0, + [72] = 0, + [73] = 0, + [74] = 0, + [75] = 0, + [76] = 6, + [77] = 6, + [78] = 4, + [79] = 4, + [80] = 0, + [81] = 0, + [82] = 0, + [83] = 0, + [84] = 0, + [85] = 0, + [86] = 0, + [87] = 0, + [88] = 0, + [89] = 5, + [90] = 5, + [91] = 5, + [92] = 0, + [93] = 0, + [94] = 0, + [95] = 0, + }, + REDS_HOUSE_1 = { + [0] = 5, + [1] = 0, + [2] = 0, + [3] = 0, + [4] = 1, + [5] = 0, + [6] = 3, + [7] = 3, + [8] = 2, + [9] = 2, + [10] = 5, + [11] = 5, + [12] = 5, + [13] = 5, + [14] = 0, + [15] = 0, + [16] = 0, + [17] = 0, + [18] = 0, + [19] = 0, + [20] = 1, + [21] = 0, + [22] = 3, + [23] = 3, + [24] = 5, + [25] = 5, + [26] = 5, + [27] = 5, + [28] = 5, + [29] = 5, + [30] = 0, + [31] = 0, + [32] = 0, + [33] = 0, + [34] = 5, + [35] = 5, + [36] = 5, + [37] = 5, + [38] = 5, + [39] = 5, + [40] = 5, + [41] = 5, + [42] = 5, + [43] = 5, + [44] = 5, + [45] = 0, + [46] = 0, + [47] = 0, + [48] = 5, + [49] = 5, + [50] = 5, + [51] = 5, + [52] = 5, + [53] = 5, + [54] = 5, + [55] = 5, + [56] = 5, + [57] = 5, + [58] = 5, + [59] = 5, + [60] = 5, + [61] = 0, + [62] = 0, + [63] = 0, + [64] = 0, + [65] = 0, + [66] = 0, + [67] = 0, + [68] = 2, + [69] = 2, + [70] = 5, + [71] = 5, + [72] = 0, + [73] = 0, + [74] = 0, + [75] = 0, + [76] = 0, + [77] = 0, + [78] = 0, + [79] = 0, + [80] = 0, + [81] = 0, + [82] = 0, + [83] = 0, + [84] = 0, + [85] = 0, + [86] = 0, + [87] = 0, + [88] = 0, + [89] = 0, + [90] = 0, + [91] = 0, + [92] = 5, + [93] = 0, + [94] = 0, + [95] = 0, + }, + REDS_HOUSE_2 = { + [0] = 5, + [1] = 0, + [2] = 0, + [3] = 0, + [4] = 1, + [5] = 0, + [6] = 3, + [7] = 3, + [8] = 2, + [9] = 2, + [10] = 5, + [11] = 5, + [12] = 5, + [13] = 5, + [14] = 0, + [15] = 0, + [16] = 0, + [17] = 0, + [18] = 0, + [19] = 0, + [20] = 1, + [21] = 0, + [22] = 3, + [23] = 3, + [24] = 5, + [25] = 5, + [26] = 5, + [27] = 5, + [28] = 5, + [29] = 5, + [30] = 0, + [31] = 0, + [32] = 0, + [33] = 0, + [34] = 5, + [35] = 5, + [36] = 5, + [37] = 5, + [38] = 5, + [39] = 5, + [40] = 5, + [41] = 5, + [42] = 5, + [43] = 5, + [44] = 5, + [45] = 0, + [46] = 0, + [47] = 0, + [48] = 5, + [49] = 5, + [50] = 5, + [51] = 5, + [52] = 5, + [53] = 5, + [54] = 5, + [55] = 5, + [56] = 5, + [57] = 5, + [58] = 5, + [59] = 5, + [60] = 5, + [61] = 0, + [62] = 0, + [63] = 0, + [64] = 0, + [65] = 0, + [66] = 0, + [67] = 0, + [68] = 2, + [69] = 2, + [70] = 5, + [71] = 5, + [72] = 0, + [73] = 0, + [74] = 0, + [75] = 0, + [76] = 0, + [77] = 0, + [78] = 0, + [79] = 0, + [80] = 0, + [81] = 0, + [82] = 0, + [83] = 0, + [84] = 0, + [85] = 0, + [86] = 0, + [87] = 0, + [88] = 0, + [89] = 0, + [90] = 0, + [91] = 0, + [92] = 5, + [93] = 0, + [94] = 0, + [95] = 0, + }, + SHIP = { + [0] = 0, + [1] = 0, + [2] = 6, + [3] = 6, + [4] = 4, + [5] = 0, + [6] = 0, + [7] = 1, + [8] = 1, + [9] = 5, + [10] = 5, + [11] = 0, + [12] = 5, + [13] = 0, + [14] = 1, + [15] = 1, + [16] = 0, + [17] = 0, + [18] = 6, + [19] = 6, + [20] = 3, + [21] = 0, + [22] = 0, + [23] = 1, + [24] = 1, + [25] = 5, + [26] = 5, + [27] = 0, + [28] = 5, + [29] = 1, + [30] = 1, + [31] = 1, + [32] = 6, + [33] = 6, + [34] = 0, + [35] = 4, + [36] = 1, + [37] = 0, + [38] = 0, + [39] = 5, + [40] = 5, + [41] = 6, + [42] = 6, + [43] = 5, + [44] = 5, + [45] = 5, + [46] = 2, + [47] = 2, + [48] = 6, + [49] = 6, + [50] = 0, + [51] = 0, + [52] = 1, + [53] = 1, + [54] = 5, + [55] = 5, + [56] = 5, + [57] = 6, + [58] = 6, + [59] = 5, + [60] = 5, + [61] = 5, + [62] = 2, + [63] = 2, + [64] = 5, + [65] = 5, + [66] = 1, + [67] = 1, + [68] = 5, + [69] = 5, + [70] = 0, + [71] = 0, + [72] = 1, + [73] = 1, + [74] = 4, + [75] = 0, + [76] = 0, + [77] = 0, + [78] = 0, + [79] = 0, + [80] = 5, + [81] = 5, + [82] = 0, + [83] = 0, + [84] = 1, + [85] = 1, + [86] = 0, + [87] = 0, + [88] = 1, + [89] = 1, + [90] = 0, + [91] = 0, + [92] = 0, + [93] = 0, + [94] = 0, + [95] = 0, + }, + SHIP_PORT = { + [0] = 0, + [1] = 2, + [2] = 3, + [3] = 3, + [4] = 3, + [5] = 0, + [6] = 0, + [7] = 0, + [8] = 0, + [9] = 5, + [10] = 5, + [11] = 0, + [12] = 0, + [13] = 3, + [14] = 3, + [15] = 3, + [16] = 3, + [17] = 3, + [18] = 5, + [19] = 5, + [20] = 3, + [21] = 0, + [22] = 0, + [23] = 0, + [24] = 0, + [25] = 0, + [26] = 0, + [27] = 0, + [28] = 0, + [29] = 0, + [30] = 5, + [31] = 5, + [32] = 3, + [33] = 0, + [34] = 5, + [35] = 5, + [36] = 0, + [37] = 0, + [38] = 0, + [39] = 0, + [40] = 0, + [41] = 0, + [42] = 3, + [43] = 0, + [44] = 0, + [45] = 0, + [46] = 0, + [47] = 5, + [48] = 3, + [49] = 3, + [50] = 5, + [51] = 5, + [52] = 0, + [53] = 0, + [54] = 0, + [55] = 0, + [56] = 0, + [57] = 0, + [58] = 5, + [59] = 5, + [60] = 5, + [61] = 0, + [62] = 0, + [63] = 0, + [64] = 3, + [65] = 0, + [66] = 5, + [67] = 5, + [68] = 0, + [69] = 0, + [70] = 0, + [71] = 0, + [72] = 5, + [73] = 5, + [74] = 5, + [75] = 5, + [76] = 5, + [77] = 0, + [78] = 5, + [79] = 5, + [80] = 2, + [81] = 3, + [82] = 0, + [83] = 0, + [84] = 0, + [85] = 0, + [86] = 2, + [87] = 2, + [88] = 5, + [89] = 5, + [90] = 0, + [91] = 3, + [92] = 0, + [93] = 0, + [94] = 0, + [95] = 0, + }, + UNDERGROUND = { + [0] = 0, + [1] = 1, + [2] = 0, + [3] = 5, + [4] = 5, + [5] = 5, + [6] = 5, + [7] = 5, + [8] = 5, + [9] = 5, + [10] = 5, + [11] = 1, + [12] = 1, + [13] = 0, + [14] = 0, + [15] = 0, + [16] = 0, + [17] = 0, + [18] = 0, + [19] = 5, + [20] = 5, + [21] = 1, + [22] = 0, + [23] = 0, + [24] = 1, + [25] = 0, + [26] = 0, + [27] = 0, + [28] = 0, + [29] = 0, + [30] = 0, + [31] = 0, + [32] = 0, + [33] = 0, + [34] = 0, + [35] = 0, + [36] = 0, + [37] = 0, + [38] = 0, + [39] = 0, + [40] = 0, + [41] = 0, + [42] = 0, + [43] = 0, + [44] = 0, + [45] = 0, + [46] = 0, + [47] = 0, + [48] = 0, + [49] = 0, + [50] = 0, + [51] = 0, + [52] = 0, + [53] = 0, + [54] = 0, + [55] = 0, + [56] = 0, + [57] = 0, + [58] = 0, + [59] = 0, + [60] = 0, + [61] = 0, + [62] = 0, + [63] = 0, + [64] = 0, + [65] = 0, + [66] = 0, + [67] = 0, + [68] = 0, + [69] = 0, + [70] = 0, + [71] = 0, + [72] = 0, + [73] = 0, + [74] = 0, + [75] = 0, + [76] = 0, + [77] = 0, + [78] = 0, + [79] = 0, + [80] = 0, + [81] = 0, + [82] = 0, + [83] = 0, + [84] = 0, + [85] = 0, + [86] = 0, + [87] = 0, + [88] = 0, + [89] = 0, + [90] = 0, + [91] = 0, + [92] = 0, + [93] = 0, + [94] = 0, + [95] = 0, + }, + }, + }, } diff --git a/data/scripts/victories.lua b/data/scripts/victories.lua index 98be5682..66dc1b39 100644 --- a/data/scripts/victories.lua +++ b/data/scripts/victories.lua @@ -5,24 +5,46 @@ -- Rocket Hideout Giovanni: his Silph Scope is an item ball next to him -- (data/maps/objects/RocketHideoutB4F.asm), so no reward entry needed. -- The TM each gym leader hands out afterwards is also ported. +-- +-- `deactivate` lists the EVENT_BEAT_* flags each gym's victory script +-- sets to retire unfought non-leader trainers (PewterGym.asm +-- "; deactivate gym trainers" / SetEventRange in the other gyms, and +-- FightingDojo.asm SetEventRange EVENT_BEAT_KARATE_MASTER .. +-- EVENT_BEAT_FIGHTING_DOJO_TRAINER_3). + +local function range(prefix, first, last) + local t = {} + for i = first, last do + t[#t + 1] = prefix .. i + end + return t +end return { ["OPP_BROCK#1"] = { badge = "BOULDERBADGE", flag = "EVENT_BEAT_BROCK", - item = "TM_BIDE" }, + item = "TM_BIDE", + deactivate = { "EVENT_BEAT_PEWTER_GYM_TRAINER_0" } }, ["OPP_MISTY#1"] = { badge = "CASCADEBADGE", flag = "EVENT_BEAT_MISTY", - item = "TM_BUBBLEBEAM" }, + item = "TM_BUBBLEBEAM", + deactivate = range("EVENT_BEAT_CERULEAN_GYM_TRAINER_", 0, 1) }, ["OPP_LT_SURGE#1"] = { badge = "THUNDERBADGE", flag = "EVENT_BEAT_LT_SURGE", - item = "TM_THUNDERBOLT" }, + item = "TM_THUNDERBOLT", + deactivate = range("EVENT_BEAT_VERMILION_GYM_TRAINER_", 0, 2) }, ["OPP_ERIKA#1"] = { badge = "RAINBOWBADGE", flag = "EVENT_BEAT_ERIKA", - item = "TM_MEGA_DRAIN" }, + item = "TM_MEGA_DRAIN", + deactivate = range("EVENT_BEAT_CELADON_GYM_TRAINER_", 0, 6) }, ["OPP_KOGA#1"] = { badge = "SOULBADGE", flag = "EVENT_BEAT_KOGA", - item = "TM_TOXIC" }, + item = "TM_TOXIC", + deactivate = range("EVENT_BEAT_FUCHSIA_GYM_TRAINER_", 0, 5) }, ["OPP_SABRINA#1"] = { badge = "MARSHBADGE", flag = "EVENT_BEAT_SABRINA", - item = "TM_PSYWAVE" }, + item = "TM_PSYWAVE", + deactivate = range("EVENT_BEAT_SAFFRON_GYM_TRAINER_", 0, 6) }, ["OPP_BLAINE#1"] = { badge = "VOLCANOBADGE", flag = "EVENT_BEAT_BLAINE", - item = "TM_FIRE_BLAST" }, + item = "TM_FIRE_BLAST", + deactivate = range("EVENT_BEAT_CINNABAR_GYM_TRAINER_", 0, 6) }, ["OPP_GIOVANNI#3"] = { badge = "EARTHBADGE", flag = "EVENT_BEAT_GIOVANNI", - item = "TM_FISSURE" }, + item = "TM_FISSURE", + deactivate = range("EVENT_BEAT_VIRIDIAN_GYM_TRAINER_", 0, 7) }, -- Silph Co. Giovanni: unlocks the president's Master Ball gift ["OPP_GIOVANNI#2"] = { flag = "EVENT_BEAT_SILPH_CO_GIOVANNI" }, @@ -31,7 +53,8 @@ return { -- FightingDojoKarateMasterPostBattleScript sets EVENT_BEAT_KARATE_MASTER, -- which gates the HITMONLEE/HITMONCHAN gift). OPP_BLACKBELT party 1 is -- only him (data/maps/objects/FightingDojo.asm). - ["OPP_BLACKBELT#1"] = { flag = "EVENT_BEAT_KARATE_MASTER" }, + ["OPP_BLACKBELT#1"] = { flag = "EVENT_BEAT_KARATE_MASTER", + deactivate = range("EVENT_BEAT_FIGHTING_DOJO_TRAINER_", 0, 3) }, -- Elite Four progress flags (their rooms' door logic isn't ported, but -- the flags make the Hall of Fame checkable) diff --git a/main.lua b/main.lua index 3c56fd3a..739003ce 100644 --- a/main.lua +++ b/main.lua @@ -223,6 +223,9 @@ function love.quit() if editorMode and EditorApp.quit then return EditorApp.quit() -- return true to abort quit end + pcall(function() + require("src.core.DiscordPresence").shutdown() + end) end function love.filedropped(file) diff --git a/src/battle/BattleState.lua b/src/battle/BattleState.lua index b25167e6..44363626 100644 --- a/src/battle/BattleState.lua +++ b/src/battle/BattleState.lua @@ -425,6 +425,14 @@ function BattleState.newTrainer(game, oppClass, partyIndex) self.oppClass = oppClass self.trainer = game.data.trainers[oppClass] assert(self.trainer, "unknown trainer class " .. tostring(oppClass)) + -- pret GetTrainerName_: RIVAL1/2/3 copy wRivalName into wTrainerName + -- instead of TrainerNames ("RIVAL1" etc.). Overlay so we don't mutate + -- the shared data table. + if oppClass == "OPP_RIVAL1" or oppClass == "OPP_RIVAL2" + or oppClass == "OPP_RIVAL3" then + local rivalName = (game.save.player and game.save.player.rival) or "BLUE" + self.trainer = setmetatable({ name = rivalName }, { __index = self.trainer }) + end self.enemyAIMods = self.trainer.aiMods local partyDef = self.trainer.parties[partyIndex or 1] assert(partyDef, ("trainer %s has no party %s"):format(oppClass, tostring(partyIndex))) diff --git a/src/core/Data.lua b/src/core/Data.lua index 3a0f4621..47cf2429 100644 --- a/src/core/Data.lua +++ b/src/core/Data.lua @@ -97,6 +97,30 @@ function Data:seedDefaults() -- the overworld's Kanto literals, same fill-if-absent contract; required -- here rather than at the top so core keeps out of src/world at load time require("src.world.FieldDefaults").seed(self) + -- Cinnabar's quiz trainers are text_asm (no def_trainers), so the + -- extractor never writes headers for them. Seed the EVENT_BEAT_* / + -- after-battle rows so Blaine's SetEventRange deactivation and talk + -- after-text work like the other gyms (scripts/CinnabarGym.asm). + self:seedCinnabarGymTrainerHeaders() +end + +function Data:seedCinnabarGymTrainerHeaders() + local headers = self.trainer_headers + if not headers or headers.CinnabarGym then return end + local gym = {} + for i = 0, 6 do + local n = i + 1 + -- object indices 2..8 are SUPER_NERD1..7; range 0 -- they only + -- engage via talk / wrong quiz answer, never sight lines + gym[i + 2] = { + event = "EVENT_BEAT_CINNABAR_GYM_TRAINER_" .. i, + range = 0, + battle = "_CinnabarGymSuperNerd" .. n .. "BattleText", + won = "_CinnabarGymSuperNerd" .. n .. "EndBattleText", + after = "_CinnabarGymSuperNerd" .. n .. "AfterBattleText", + } + end + headers.CinnabarGym = gym end -- POKEPORT_DATA_DIR points a test runner at another dataset root (the diff --git a/src/core/DiscordPresence.lua b/src/core/DiscordPresence.lua new file mode 100644 index 00000000..2be37aad --- /dev/null +++ b/src/core/DiscordPresence.lua @@ -0,0 +1,658 @@ +-- Discord Rich Presence via the local Discord IPC pipe. Shows the player's +-- current map (and battle status) on their Discord profile. Desktop only; +-- no-ops on mobile, headless runs, or when Discord is not running. +-- +-- Application ID is the only credential needed for presence. The Discord +-- Interactions public key is unused here (that is for verifying bot HTTP +-- payloads, not Rich Presence). + +local Json = require("src.link.Json") +local Logger = require("src.core.Logger") +local Runtime = require("src.mods.Runtime") + +local DiscordPresence = { + APP_ID = "1529183141267374262", +} + +local OP_HANDSHAKE, OP_FRAME, OP_CLOSE = 0, 1, 2 + +local MIN_UPDATE_INTERVAL = 2.0 +local RECONNECT_INTERVAL = 30.0 + +local state = { + enabled = false, + connected = false, + socket = nil, -- Windows: file handle; Unix: ffi sockfd + isWindows = false, + ffi = nil, + ffiReady = false, + startedAt = 0, + dirty = false, + lastSentKey = nil, + lastSendAt = -1e9, + nextReconnectAt = 0, + location = nil, -- display name + mapId = nil, + activity = "menu", -- menu | exploring | battle + battleLabel = nil, + unsubs = {}, + pid = nil, + loggedAbsent = false, -- one quiet note when Discord isn't running +} + +local function disabledByEnv() + return os.getenv("POKEPORT_NO_DISCORD") == "1" + or os.getenv("POKEPORT_AUTOPILOT") ~= nil + or os.getenv("POKEPORT_DRIVER") ~= nil +end + +local function isDesktop() + if not love or not love.system or not love.system.getOS then return false end + local osName = love.system.getOS() + return osName == "OS X" or osName == "Windows" or osName == "Linux" +end + +local function packU32(n) + n = math.floor(n) % 0x100000000 + local b1 = n % 256; n = math.floor(n / 256) + local b2 = n % 256; n = math.floor(n / 256) + local b3 = n % 256; n = math.floor(n / 256) + local b4 = n % 256 + return string.char(b1, b2, b3, b4) +end + +local function unpackU32(s) + local b1, b2, b3, b4 = s:byte(1, 4) + return b1 + b2 * 256 + b3 * 65536 + b4 * 16777216 +end + +local function nonce() + return string.format("%08x-%04x-4%03x-%04x-%04x%08x", + math.random(0, 0xffffffff), + math.random(0, 0xffff), + math.random(0, 0xfff), + math.random(0, 0x3fff) + 0x8000, + math.random(0, 0xffff), + math.random(0, 0xffffffff)) +end + +local function resolvePid() + if state.pid then return state.pid end + local ffi = state.ffi + if ffi then + if state.isWindows then + pcall(ffi.cdef, "unsigned long GetCurrentProcessId(void);") + local ok, p = pcall(function() return ffi.C.GetCurrentProcessId() end) + if ok and p and tonumber(p) and tonumber(p) > 0 then + state.pid = tonumber(p) + return state.pid + end + else + pcall(ffi.cdef, "int getpid(void);") + local ok, p = pcall(function() return ffi.C.getpid() end) + if ok and p and tonumber(p) and tonumber(p) > 0 then + state.pid = tonumber(p) + return state.pid + end + end + end + state.pid = 1000 + math.random(1, 8999) + return state.pid +end + +local function ensureFfi() + if state.ffiReady then return state.ffi end + if state.ffi == false then return nil end + local ok, ffi = pcall(require, "ffi") + if not ok or not ffi then + state.ffi = false + return nil + end + state.ffi = ffi + if state.isWindows then + state.ffiReady = true + return ffi + end + -- Darwin sockaddr_un has sun_len + uint8_t family; Linux uses u16 family. + local osName = love.system.getOS() + local cdef + if osName == "OS X" then + cdef = [[ + typedef unsigned int socklen_t; + typedef int ssize_t; + struct sockaddr_un { + uint8_t sun_len; + uint8_t sun_family; + char sun_path[104]; + }; + struct timeval { long tv_sec; int tv_usec; }; + int socket(int domain, int type, int protocol); + int connect(int sockfd, const void *addr, socklen_t addrlen); + int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); + ssize_t send(int sockfd, const void *buf, size_t len, int flags); + ssize_t recv(int sockfd, void *buf, size_t len, int flags); + int close(int fd); + ]] + else + cdef = [[ + typedef unsigned int socklen_t; + typedef int ssize_t; + struct sockaddr_un { + unsigned short sun_family; + char sun_path[108]; + }; + struct timeval { long tv_sec; long tv_usec; }; + int socket(int domain, int type, int protocol); + int connect(int sockfd, const void *addr, socklen_t addrlen); + int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); + ssize_t send(int sockfd, const void *buf, size_t len, int flags); + ssize_t recv(int sockfd, void *buf, size_t len, int flags); + int close(int fd); + ]] + end + -- cdef may already be registered from a prior boot; either way we need + -- sockaddr_un usable before claiming readiness + pcall(ffi.cdef, cdef) + local ready = pcall(function() return ffi.new("struct sockaddr_un") end) + if not ready then + state.ffi = false + state.ffiReady = false + return nil + end + state.ffiReady = true + return ffi +end + +local function closeSocket() + local sock = state.socket + state.socket = nil + state.connected = false + if not sock then return end + if state.isWindows then + pcall(function() sock:close() end) + elseif state.ffi then + pcall(state.ffi.C.close, sock) + end +end + +local function writeRaw(bytes) + if not state.socket then return false end + if state.isWindows then + local ok = pcall(function() + state.socket:seek("end") + local _, writeErr = state.socket:write(bytes) + state.socket:flush() + if writeErr then error(writeErr) end + end) + return ok + end + local ffi = state.ffi + if not ffi then return false end + local ok, sent = pcall(ffi.C.send, state.socket, bytes, #bytes, 0) + return ok and sent == #bytes +end + +local function sendFrame(opcode, payload) + local body = payload or "" + local ok, frame = pcall(function() + return packU32(opcode) .. packU32(#body) .. body + end) + if not ok or not frame then return false end + return writeRaw(frame) +end + +local function readExact(n) + if not state.socket or n <= 0 then return nil end + if state.isWindows then + local ok, data = pcall(function() return state.socket:read(n) end) + if not ok or not data or #data < n then return nil end + return data + end + local ffi = state.ffi + if not ffi then return nil end + local ok, data = pcall(function() + local buf = ffi.new("char[?]", n) + local got = 0 + while got < n do + local nread = ffi.C.recv(state.socket, buf + got, n - got, 0) + if not nread or nread <= 0 then return nil end + got = got + tonumber(nread) + end + return ffi.string(buf, n) + end) + if not ok then return nil end + return data +end + +local function receiveFrame() + local header = readExact(8) + if not header then return nil end + local ok, opcode, length = pcall(function() + return unpackU32(header:sub(1, 4)), unpackU32(header:sub(5, 8)) + end) + if not ok or not opcode or not length or length < 0 or length > 65536 then + return nil + end + local data = length > 0 and readExact(length) or "" + if data == nil then return nil end + return opcode, data +end + +local function unixSocketPaths() + local paths = {} + local envs = { "XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP" } + local bases = {} + for _, key in ipairs(envs) do + local v = os.getenv(key) + if v and v ~= "" then + if v:sub(-1) == "/" then v = v:sub(1, -2) end + bases[#bases + 1] = v + end + end + bases[#bases + 1] = "/tmp" + local suffixes = { + "", + "/app/com.discordapp.Discord", + "/app/com.discordapp.DiscordCanary", + "/app/com.discordapp.DiscordPTB", + "/snap.discord", + "/snap.discord-canary", + } + local seen = {} + for i = 0, 9 do + local name = "discord-ipc-" .. i + for _, base in ipairs(bases) do + for _, suffix in ipairs(suffixes) do + local path = base .. suffix .. "/" .. name + if not seen[path] then + seen[path] = true + paths[#paths + 1] = path + end + end + end + end + return paths +end + +local function setSocketTimeout(fd, seconds) + local ffi = state.ffi + if not ffi or not fd then return end + pcall(function() + local tv = ffi.new("struct timeval") + tv.tv_sec = seconds + tv.tv_usec = 0 + -- SOL_SOCKET / SO_RCVTIMEO / SO_SNDTIMEO differ by OS + local sol, rcv, snd + if love.system.getOS() == "OS X" then + sol, rcv, snd = 0xffff, 0x1006, 0x1005 + else + sol, rcv, snd = 1, 20, 21 + end + ffi.C.setsockopt(fd, sol, rcv, tv, ffi.sizeof(tv)) + ffi.C.setsockopt(fd, sol, snd, tv, ffi.sizeof(tv)) + end) +end + +local function connectUnix() + local ffi = ensureFfi() + if not ffi then return false end + local darwin = love.system.getOS() == "OS X" + local maxPath = darwin and 103 or 107 + -- AF_UNIX=1, SOCK_STREAM=1 on Linux/macOS + local pathsOk, paths = pcall(unixSocketPaths) + if not pathsOk or not paths then return false end + for _, path in ipairs(paths) do + if #path <= maxPath then + local ok, connected = pcall(function() + local fd = ffi.C.socket(1, 1, 0) + if not fd or fd < 0 then return false end + local addr = ffi.new("struct sockaddr_un") + addr.sun_family = 1 + if darwin then + -- SUN_LEN: sizeof(sun_len+sun_family) + path + NUL + addr.sun_len = 2 + #path + 1 + end + ffi.copy(addr.sun_path, path) + local rc = ffi.C.connect(fd, ffi.cast("void*", addr), ffi.sizeof(addr)) + if rc == 0 then + setSocketTimeout(fd, 1) -- handshake must not freeze the game + state.socket = fd + return true + end + ffi.C.close(fd) + return false + end) + if ok and connected then return true end + end + end + return false +end + +local function connectWindows() + for i = 0, 9 do + local path = "\\\\.\\pipe\\discord-ipc-" .. i + local ok, file = pcall(io.open, path, "r+") + if ok and file then + state.socket = file + return true + end + end + return false +end + +local function handshake() + local ok, payload = pcall(Json.encode, { v = 1, client_id = DiscordPresence.APP_ID }) + if not ok or not payload then return false end + if not sendFrame(OP_HANDSHAKE, payload) then return false end + -- Discord echoes READY as a FRAME. A timeout/missing client is a soft miss. + local opcode = receiveFrame() + if opcode == OP_FRAME then return true end + -- Windows named pipes have no easy read timeout: if the write succeeded + -- and Discord is present, proceed optimistically so boot never blocks. + if state.isWindows then return true end + return false +end + +local function noteAbsent() + if state.loggedAbsent then return end + state.loggedAbsent = true + -- info, not warn: missing Discord is normal for many players + Logger.info("discord: rich presence unavailable (Discord not running)") +end + +local function connect() + local ok, result = pcall(function() + closeSocket() + local linked + if state.isWindows then + linked = connectWindows() + else + linked = connectUnix() + end + if not linked then return false end + if not handshake() then + closeSocket() + return false + end + state.connected = true + state.dirty = true + state.lastSentKey = nil + state.loggedAbsent = false + Logger.info("discord: rich presence connected") + return true + end) + if not ok then + closeSocket() + return false + end + return result and true or false +end + +local function locationName(game, mapId) + if not mapId then return nil end + local field = game and game.data and game.data.field + local townMap = field and field.townMap + local locations = townMap and (townMap.locations or townMap) + local entry = type(locations) == "table" and locations[mapId] + if type(entry) == "table" then + local name = entry.name or entry.label + if type(name) == "string" and name ~= "" then return name end + end + local def = game and game.data and game.data.maps and game.data.maps[mapId] + if def and type(def.label) == "string" and def.label ~= "" then + -- PalletTown -> Pallet Town + return def.label:gsub("(%l)(%u)", "%1 %2") + end + return tostring(mapId):gsub("_", " ") +end + +local function speciesName(game, species) + if not species or not game or not game.data or not game.data.pokemon then + return nil + end + local def = game.data.pokemon[species] + return def and def.name or tostring(species):gsub("_", " ") +end + +local function buildActivity() + local details, activityState + if state.activity == "battle" then + details = state.battleLabel or "In battle" + activityState = state.location and (state.location) or "Kanto" + elseif state.activity == "exploring" and state.location then + details = state.location + activityState = "Exploring the overworld" + else + details = state.location or "Kanto" + activityState = "At the title screen" + end + + return { + details = details, + state = activityState, + timestamps = { start = state.startedAt }, + assets = { + large_image = "logo", + large_text = "Pokemon Gen1Recomp", + }, + } +end + +local function activityKey(activity) + return (activity.details or "") .. "|" .. (activity.state or "") +end + +local function flush(force) + if not state.enabled or not state.connected then return end + local ok = pcall(function() + local now = love.timer.getTime() + if not force and (now - state.lastSendAt) < MIN_UPDATE_INTERVAL then + state.dirty = true + return + end + local activity = buildActivity() + local key = activityKey(activity) + if not force and key == state.lastSentKey then + state.dirty = false + return + end + local payload = Json.encode({ + cmd = "SET_ACTIVITY", + args = { + pid = resolvePid(), + activity = activity, + }, + nonce = nonce(), + }) + if not sendFrame(OP_FRAME, payload) then + closeSocket() + state.nextReconnectAt = now + RECONNECT_INTERVAL + return + end + state.lastSentKey = key + state.lastSendAt = now + state.dirty = false + end) + if not ok then + closeSocket() + local now = love and love.timer and love.timer.getTime and love.timer.getTime() or 0 + state.nextReconnectAt = now + RECONNECT_INTERVAL + end +end + +local function setPresence(fields) + -- event listeners must never throw into the overworld/battle path + pcall(function() + if fields.location ~= nil then state.location = fields.location end + if fields.mapId ~= nil then state.mapId = fields.mapId end + if fields.activity ~= nil then state.activity = fields.activity end + if fields.clearBattle then + state.battleLabel = nil + elseif fields.battleLabel ~= nil then + state.battleLabel = fields.battleLabel + end + state.dirty = true + flush(false) + end) +end + +local function subscribe(game) + local events = Runtime.events + if not events or not events.on then return end + + local function track(name, fn) + state.unsubs[#state.unsubs + 1] = events:on(name, fn, 0, "discord") + end + + track("map.entered", function(ev) + local mapId = ev and ev.mapId + setPresence({ + mapId = mapId, + location = locationName(game, mapId), + activity = "exploring", + clearBattle = true, + }) + end) + + track("battle.started", function(ev) + local battle = ev and ev.battle + local label = "In battle" + if ev and ev.kind == "wild" then + local name = speciesName(game, ev.species) + if name then + label = "Battling wild " .. name + if ev.level then label = label .. " Lv" .. tostring(ev.level) end + else + label = "Wild battle" + end + elseif ev and ev.kind == "trainer" then + local tname = battle and battle.trainer and battle.trainer.name + label = tname and ("Battling " .. tname) or "Trainer battle" + elseif ev and ev.kind == "link" then + label = "Link battle" + end + local mapId = state.mapId + or (game.save and game.save.player and game.save.player.map) + setPresence({ + activity = "battle", + battleLabel = label, + location = locationName(game, mapId) or state.location, + mapId = mapId, + }) + end) + + track("battle.ended", function() + local mapId = state.mapId + or (game.save and game.save.player and game.save.player.map) + setPresence({ + activity = mapId and "exploring" or "menu", + clearBattle = true, + location = locationName(game, mapId) or state.location, + mapId = mapId, + }) + end) + + track("screen.pushed", function(ev) + local s = ev and ev.state + local id = s and s.screenId + if not id and s and s.onNewGame then id = "TitleState" end + if id == "TitleState" or id == "IntroMovie" then + state.mapId = nil + setPresence({ + activity = "menu", + location = id == "IntroMovie" and "Watching the intro" or "Title screen", + clearBattle = true, + }) + end + end) +end + +function DiscordPresence.init(game) + local ok, err = pcall(function() + DiscordPresence.shutdown() + if disabledByEnv() or not isDesktop() then return end + + state.enabled = true + state.isWindows = love.system.getOS() == "Windows" + state.startedAt = os.time() + state.activity = "menu" + state.location = "Title screen" + state.mapId = nil + state.battleLabel = nil + state.dirty = true + state.nextReconnectAt = 0 + state.loggedAbsent = false + + if not state.isWindows and not ensureFfi() then + -- no FFI / broken cdef: stay disabled, never throw + state.enabled = false + return + end + + subscribe(game) + if connect() then + flush(true) + else + noteAbsent() + local now = (love.timer and love.timer.getTime and love.timer.getTime()) or 0 + state.nextReconnectAt = now + RECONNECT_INTERVAL + end + end) + if not ok then + closeSocket() + state.enabled = false + -- still soft: a broken presence stack must not abort boot + Logger.info("discord: rich presence disabled (%s)", tostring(err)) + end +end + +function DiscordPresence.update(_dt) + if not state.enabled then return end + pcall(function() + local now = love.timer.getTime() + if not state.connected then + if now >= state.nextReconnectAt then + state.nextReconnectAt = now + RECONNECT_INTERVAL + if connect() then + flush(true) + else + noteAbsent() + end + end + return + end + if state.dirty then flush(false) end + end) +end + +function DiscordPresence.shutdown() + pcall(function() + for _, unsub in ipairs(state.unsubs) do + pcall(unsub) + end + state.unsubs = {} + if state.connected then + pcall(function() + -- omit activity to clear the presence + sendFrame(OP_FRAME, Json.encode({ + cmd = "SET_ACTIVITY", + args = { pid = resolvePid() }, + nonce = nonce(), + })) + sendFrame(OP_CLOSE, "{}") + end) + end + closeSocket() + end) + state.enabled = false + state.dirty = false + state.lastSentKey = nil + state.connected = false + state.socket = nil +end + +-- test / debug helpers +DiscordPresence._state = state +DiscordPresence.locationName = locationName + +return DiscordPresence diff --git a/src/core/Game.lua b/src/core/Game.lua index e4b3e022..83508a4f 100644 --- a/src/core/Game.lua +++ b/src/core/Game.lua @@ -68,6 +68,10 @@ function Game:load() local OverworldState = require("src.world.OverworldController") self.overworld = OverworldState + -- Discord Rich Presence: map name / battle status on the player's profile. + -- Soft-fail: missing Discord / IPC errors must never block boot. + pcall(function() require("src.core.DiscordPresence").init(self) end) + -- every service is up but nothing is on the stack yet; this payload is -- the sanctioned way for a mod to obtain the Game object ModRuntime.emit("game.ready", { game = self }) @@ -103,7 +107,7 @@ end function Game:makeTitleState() local OverworldState = require("src.world.OverworldController") local factory = Screens.get(self, bootScreens(self).title or "TitleState") - return factory.new(self, { + local title = factory.new(self, { onNewGame = function() while self.stack:top() do self.stack:pop() end -- New Game keeps the standalone options.lua preferences @@ -126,6 +130,8 @@ function Game:makeTitleState() end end, }) + title.screenId = title.screenId or "TitleState" + return title end -- QUIT from the START menu: back to the title like a power-cycle, @@ -187,6 +193,7 @@ function Game:update(dt) -- Overworld tilt toggle tween: presentational, so it runs on the real -- frame dt (not the fixed logic step) for a smooth ~0.25s glide. require("src.render.Tilt").update(dt) + pcall(function() require("src.core.DiscordPresence").update(dt) end) end -- render.zones' identity default: unhooked, the zone list reaches the blit @@ -391,6 +398,7 @@ function Game:applyOptions(opts) require("src.render.PaletteFX").applyOptions(opts) require("src.render.Tilt").applyOptions(opts) require("src.render.GBCFX").applyOptions(opts) + require("src.core.VideoMode").applyOptions(opts) end function Game:restoreSave(loaded, recovered) diff --git a/src/core/GameSpeed.lua b/src/core/GameSpeed.lua index a21309d0..85b81b3c 100644 --- a/src/core/GameSpeed.lua +++ b/src/core/GameSpeed.lua @@ -18,7 +18,7 @@ local GameSpeed = {} -- attempt is long enough that the iteration loop, not the engine, is the -- bottleneck. Vsync caps how much a real frame can do, so past 10X the -- multiplier is increasingly a ceiling rather than a rate. -GameSpeed.LEVELS = { 1, 2, 4, 10, 20, 30, 50, 75, 100,500 } +GameSpeed.LEVELS = { 1, 2, 4, 10, 20, 30, 50, 75, 100,200 } GameSpeed.DEFAULT = 1 function GameSpeed.levelLabel(v) diff --git a/src/core/SaveData.lua b/src/core/SaveData.lua index 9bf30d59..56b540be 100644 --- a/src/core/SaveData.lua +++ b/src/core/SaveData.lua @@ -47,6 +47,8 @@ function SaveData.defaultOptions() colors = "gbc", tilt = 0, gbcfx = 0, + -- windowed | borderless (desktop fullscreen); ignored on mobile + videoMode = "windowed", -- Native mod enablement is an installation option, not save-slot data. -- Missing entries mean enabled so newly installed mods work by default. mods = {}, diff --git a/src/core/VideoMode.lua b/src/core/VideoMode.lua new file mode 100644 index 00000000..39dc715a --- /dev/null +++ b/src/core/VideoMode.lua @@ -0,0 +1,54 @@ +-- Windowed vs borderless (desktop) fullscreen. +-- +-- Persisted as save.options.videoMode. Applied from OptionsMenu and on +-- boot via Game:applyOptions. No-ops on mobile and in headless stubs +-- that lack love.window. + +local VideoMode = {} + +VideoMode.MODES = { "windowed", "borderless" } +VideoMode.DEFAULT = "windowed" + +local LABELS = { + windowed = "WINDOWED", + borderless = "BORDERLESS", +} + +function VideoMode.normalize(mode) + if mode == "borderless" then return "borderless" end + return VideoMode.DEFAULT +end + +function VideoMode.modeLabel(mode) + return LABELS[VideoMode.normalize(mode)] or LABELS[VideoMode.DEFAULT] +end + +function VideoMode.isMobile() + if not love or not love.system or not love.system.getOS then return false end + local osName = love.system.getOS() + return osName == "Android" or osName == "iOS" +end + +-- Cycle windowed <-> borderless (dir ignored; two modes). +function VideoMode.cycle(mode, _dir) + return VideoMode.normalize(mode) == "borderless" and "windowed" or "borderless" +end + +-- Push the mode into the live window. Safe when love.window is missing. +function VideoMode.apply(mode) + if VideoMode.isMobile() then return end + if not love or not love.window or not love.window.setFullscreen then return end + mode = VideoMode.normalize(mode) + if mode == "borderless" then + -- "desktop" = borderless fullscreen matching the display + love.window.setFullscreen(true, "desktop") + else + love.window.setFullscreen(false) + end +end + +function VideoMode.applyOptions(opts) + VideoMode.apply(opts and opts.videoMode) +end + +return VideoMode diff --git a/src/render/PaletteFX.lua b/src/render/PaletteFX.lua index 445eec20..0d65b52c 100644 --- a/src/render/PaletteFX.lua +++ b/src/render/PaletteFX.lua @@ -196,6 +196,114 @@ function PaletteFX.monPalName(data, species, transformed) return "MEWMON" end +-- ------- true GBC overworld coloring (color/loadpalettes.asm, +-- color/data/*, color/sprites.asm ColorOverworldSprite) ------------------- +-- +-- RED++ pairs its named-palette battle/mon colors above with pokered-gbc's +-- real per-tile system: LoadTilesetPalette assigns one of 8 four-color BG +-- palettes to every tile GRAPHIC in a tileset (by tile id, not by map +-- position), and LoadTownPalette swaps just the ROOF slot (index 6) per +-- town/route. `data/palettes_gbc.lua`'s `world` table holds the extracted +-- data (tools/extract/palettes.py extract_gbc_world); these queries are +-- mode-independent (only check the pack exists) so TileRenderer can +-- precompute geometry once regardless of the active COLORS mode -- callers +-- that resolve to actual on-screen COLOR should gate on usesGbcPack() +-- themselves, the same way they already gate other RED++-only behavior. +-- +-- LoadTilesetPalette's 3 hardcoded single-tile fixes (Celadon Mart) and +-- LoadTownPalette's Route 6/Saffron y<2 roof split are control flow, not +-- data, so they are not in the extracted pack -- they live here instead. +local TILE_GROUP_EXCEPTIONS = { + -- tile ids $4b-$4f -> BLUE (outside sky, seen through the mart's roof) + CELADON_MART_ROOF = { tiles = { [0x4b] = true, [0x4c] = true, [0x4d] = true, + [0x4e] = true, [0x4f] = true }, group = 3 }, + -- tile $37 -> BROWN (counter miscoloration fix) + CELADON_MART_3F = { tiles = { [0x37] = true }, group = 5 }, + -- tiles $07/$08/$17/$18 -> YELLOW (bench, blue by default) + CELADON_MART_1F = { tiles = { [0x07] = true, [0x08] = true, + [0x17] = true, [0x18] = true }, group = 4 }, +} +local ROOF_GROUP = 6 +local ROUTE_6_SAFFRON = { mapId = "ROUTE_6", useMapId = "SAFFRON_CITY", cellYBelow = 2 } + +-- whether the extracted pack has real per-tile GBC data for this tileset +-- (false for a mod tileset with no pokered-gbc counterpart, or when the +-- pack failed to load at all) +function PaletteFX.hasWorldTileset(tileset) + local pack = PaletteFX.gbcPack() + local w = pack and pack.world + return (w and w.tileGroups[tileset]) ~= nil +end + +-- the palette-group (0-7) a tile GRAPHIC id resolves to in this tileset, +-- with the current map's tile-id exceptions (if any) applied first +function PaletteFX.worldGroupAt(tileset, mapId, tileId) + local pack = PaletteFX.gbcPack() + local w = pack and pack.world + local groups = w and w.tileGroups[tileset] + if not groups then return nil end + local exc = TILE_GROUP_EXCEPTIONS[mapId] + if exc and exc.tiles[tileId] then return exc.group end + return groups[tileId] or 7 -- TEXT: tile ids past the tileset's 96 (menus) +end + +-- this tileset's resolved 8-entry {r,g,b}x4 palette array, with the ROOF +-- slot swapped to the current town/route (Route 6's north end uses +-- Saffron's roof colors while the player stands in its top 2 cell rows, +-- like pokered's wYCoord check -- data is Game.data, for the map lookup) +function PaletteFX.worldGroupColors(data, tileset, mapId, playerCellY) + local pack = PaletteFX.gbcPack() + local w = pack and pack.world + local base = w and w.groupColors[tileset] + if not base then return nil end + if not w.roofGroup[tileset] then return base end + local roofMapId = mapId + if mapId == ROUTE_6_SAFFRON.mapId and playerCellY + and playerCellY < ROUTE_6_SAFFRON.cellYBelow then + roofMapId = ROUTE_6_SAFFRON.useMapId + end + local roofMap = data and data.maps and data.maps[roofMapId] + local roof = roofMap and w.roofByMapIndex[roofMap.index] + if not roof then return base end + local out = {} + for i = 1, 8 do out[i] = base[i] end + -- LoadTownPalette only overwrites W2_BgPaletteData + $32, i.e. colors 1 + -- and 2 (0-indexed) of the 4-color ROOF slot -- color 0 (background, + -- typically the sky-through-gaps white) and color 3 (outline black) keep + -- the tileset's own OUTDOOR_ROOF/INDOOR_ROOF base, only the roof + -- material's 2 middle shades are town-specific + local base4 = base[ROOF_GROUP + 1] + out[ROOF_GROUP + 1] = { base4[1], roof[1], roof[2], base4[4] } + return out +end + +-- an overworld sprite's resolved 4-color OBJ palette (ColorOverworldSprite), +-- or nil when unassigned/unavailable, plus the resolved group index (for +-- callers that want a stable cache key without hashing the colors table). +-- spriteDef carries the ROM picture-id crosswalk in its `source` field +-- ("ROM:SpriteSheetPointerTable[N]"); seed (any stable per-instance value, +-- e.g. an NPC's `id`) resolves the "random" sentinel -- a deliberate +-- approximation of ColorOverworldSprite's per-OAM-slot pseudo-random pick +-- (`swap a; and 3` on the sprite's OAM offset, which has no equivalent +-- here): a stable hash instead, so the same NPC instance always shows the +-- same one of the 4 SPR_PAL_* colors. +function PaletteFX.spriteObp(spriteDef, seed) + local pack = PaletteFX.gbcPack() + local w = pack and pack.world + local src = spriteDef and spriteDef.source + if not (w and src) then return nil end + local idx = tonumber(src:match("%[(%d+)%]")) + local group = idx and w.spriteAssignment[idx] + if group == nil then return nil end + if group == "random" then + local h = 0 + seed = tostring(seed or "") + for i = 1, #seed do h = (h * 31 + seed:byte(i)) % 4294967296 end + group = h % 4 + end + return w.spritePalettes[group], group +end + -- GetHealthBarColor (home/palettes.asm) on the standard 48px bar function PaletteFX.barPalName(hp, maxHp) local px = maxHp > 0 and math.floor(hp * 48 / maxHp) or 0 @@ -237,10 +345,24 @@ function PaletteFX.setMode(mode) end end if not ok then PaletteFX.mode = "gbc" end - -- battle pics bake the active pack into ImageData; drop the cache when - -- the pack (or any COLORS mode) changes so the next draw re-tints + -- battle pics and overworld sprites bake the active pack into ImageData; + -- drop those caches when the pack (or any COLORS mode) changes so the + -- next draw re-tints if prev ~= PaletteFX.mode then pcall(function() require("src.battle.BattleState").invalidate() end) + pcall(function() require("src.render.SpriteRenderer").invalidate() end) + -- RED++'s baked tileset atlas (TileRenderer.getGbcAtlas) is built once + -- per loaded map, so a mode toggle needs every cached Map/TileRenderer + -- dropped and the currently-visible one rebuilt in place -- otherwise + -- the on-screen map keeps its stale (wrong-mode) atlas until the next + -- map transition happens to reload it. + pcall(function() + require("src.world.MapLoader").invalidateAll() + local Game = require("src.core.Game") + if Game.overworld and Game.overworld.map and Game.overworld.reloadMap then + Game.overworld:reloadMap(Game.overworld.map.id, "colors") + end + end) end end diff --git a/src/render/SpriteRenderer.lua b/src/render/SpriteRenderer.lua index 7895eea6..f4c73923 100644 --- a/src/render/SpriteRenderer.lua +++ b/src/render/SpriteRenderer.lua @@ -18,10 +18,49 @@ local function getImage(path) return imageCache[path] end +-- RED++ overworld sprite OBJ-palette recolor (color/sprites.asm +-- ColorOverworldSprite), baked into an ImageData like BattleState's mon-pic +-- palette bake (src/battle/BattleState.lua getImage): CPU-remap the 4 DMG +-- shades to the resolved OBP colors, cached per (image path, group). +-- +-- Sprite sheets carry no real alpha (every pixel, including the +-- background, is opaque -- confirmed by sampling the extracted PNGs): the +-- "transparent" look in every other draw path is a coincidence of the +-- whole-canvas shade-remap shader, where shade 0 (white) happens to map to +-- a similarly light color in whatever terrain zone the sprite stands over. +-- That coincidence breaks once terrain is colored per-tile instead of one +-- flat color per map (different tiles can have very different color-0s), +-- so shade 0 is keyed to alpha 0 here explicitly -- matching real GBC OBJ +-- hardware, where sprite palette index 0 is unconditionally transparent +-- (same rule TileRenderer's getColor0KeyShader documents for tall grass). +local obpCache = {} + +local function getObpImage(path, colors, group) + local key = path .. "#obp" .. group + if not obpCache[key] then + local img + if love.image and love.image.newImageData then + local id = Assets.imageData(path) + id:mapPixel(function(_, _, r, g, b, a) + if a == 0 then return r, g, b, a end + if r > 0.83 then return r, g, b, 0 end -- OBJ color 0: always transparent + local col = r > 0.5 and colors[2] or r > 0.17 and colors[3] or colors[4] + return col[1] / 255, col[2] / 255, col[3] / 255, a + end) + img = love.graphics.newImage(id) + else + img = getImage(path) -- headless stub: no pixel access + end + obpCache[key] = img + end + return obpCache[key] +end + -- hot reload drops the sheets; live instances hold their own image, so -- the world rebuilds them (MapLoader.invalidateAll) rather than this function SpriteRenderer.invalidate() imageCache = {} + obpCache = {} end Assets.register(SpriteRenderer.invalidate) @@ -29,9 +68,12 @@ Assets.register(SpriteRenderer.invalidate) local STAND = { down = 0, up = 1, left = 2, right = 2 } local WALK = { down = 3, up = 4, left = 5, right = 5 } -function SpriteRenderer.new(spriteDef) +-- seed: any stable per-instance value (e.g. an NPC's `id`) used to resolve +-- RED++'s per-instance "random" OBP sentinel (PaletteFX.spriteObp) +function SpriteRenderer.new(spriteDef, seed) local self = setmetatable({}, SpriteRenderer) self.def = spriteDef + self.seed = seed self.image = getImage(spriteDef.image) local iw, ih = self.image:getDimensions() self.frames = {} @@ -46,13 +88,26 @@ end function SpriteRenderer:draw(px, py, camX, camY, facing, walkPhase, stepFlip) local x = math.floor(px - camX) local y = math.floor(py - camY) - 4 + local image = self.image -- full-color art claims its 16x16 cell out of the shade-remap pass - if self.def.trueColor then PaletteFX.markTrueColor(x, y, 16, 16) end + if self.def.trueColor then + PaletteFX.markTrueColor(x, y, 16, 16) + elseif PaletteFX.usesGbcPack() then + -- RED++: the world canvas is already true-color (TileRenderer bakes + -- terrain, this bakes the sprite) and the world pass runs unshaded + -- (OverworldState.sgbWorldZones), so this draws like any normal sprite + -- -- opaque character pixels over a real-alpha-transparent background, + -- no trueColor rect needed (there is no shader left to exempt it from). + local colors, group = PaletteFX.spriteObp(self.def, self.seed) + if colors then + image = getObpImage(self.def.image, colors, group) + end + end -- single-frame sprites (item balls, fossils...) have one fixed pose; -- still 3-frame sprites turn to face (the nurse at her machine, -- facePlayer on STAY NPCs) but never show walk frames if self.def.frames <= 1 then - love.graphics.draw(self.image, self.frames[0], x, y) + love.graphics.draw(image, self.frames[0], x, y) return end local frame = (self.def.walker and walkPhase == 1) @@ -65,9 +120,9 @@ function SpriteRenderer:draw(px, py, camX, camY, facing, walkPhase, stepFlip) end local quad = self.frames[frame] or self.frames[0] if flip then - love.graphics.draw(self.image, quad, x + 16, y, 0, -1, 1) + love.graphics.draw(image, quad, x + 16, y, 0, -1, 1) else - love.graphics.draw(self.image, quad, x, y) + love.graphics.draw(image, quad, x, y) end end diff --git a/src/render/TileRenderer.lua b/src/render/TileRenderer.lua index f8773806..b5197cbf 100644 --- a/src/render/TileRenderer.lua +++ b/src/render/TileRenderer.lua @@ -108,10 +108,23 @@ end -- the water/flower branches did before they were data. -- ------------------------------------------------------------------ --- the 8 shifted variants of one tile (built once per sheet + tile id) +-- shade 0-3 -> one of `colors`' 4 entries (same cutoffs PaletteFX's shader +-- uses), alpha passed through unchanged; nil colors leaves r,g,b as-is. +-- Shared by the whole-atlas bake (getGbcAtlas) and the animated-tile +-- variants below, so water/flowers/spinners match the static tiles around +-- them under RED++ instead of showing their un-recolored grayscale. +local function recolorSample(r, g, b, a, colors) + if not (colors and a > 0) then return r, g, b, a end + local col = r > 0.83 and colors[1] or r > 0.5 and colors[2] + or r > 0.17 and colors[3] or colors[4] + return col[1] / 255, col[2] / 255, col[3] / 255, a +end + +-- the 8 shifted variants of one tile (built once per sheet + tile id [+ +-- gbcKey, when `colors` recolors it for RED++ -- see buildAnim]) local shiftVariants = {} -local function getShiftVariants(tilesetImagePath, perRow, tile) - local key = tilesetImagePath .. "#" .. tile +local function getShiftVariants(tilesetImagePath, perRow, tile, colors, gbcKey) + local key = tilesetImagePath .. "#" .. tile .. (gbcKey or "") if shiftVariants[key] ~= nil then return shiftVariants[key] end if not (love.image and love.image.newImageData) then shiftVariants[key] = false @@ -126,6 +139,7 @@ local function getShiftVariants(tilesetImagePath, perRow, tile) for y = 0, 7 do for x = 0, 7 do local r, g, b, a = id:getPixel(sx + x, sy + y) + r, g, b, a = recolorSample(r, g, b, a, colors) v:setPixel((x + o) % 8, y, r, g, b, a) end end @@ -136,12 +150,27 @@ local function getShiftVariants(tilesetImagePath, perRow, tile) end local frameImages = {} -local function getFrameImages(paths) - local key = table.concat(paths, "|") +local function getFrameImages(paths, colors, gbcKey) + local key = table.concat(paths, "|") .. (gbcKey or "") if frameImages[key] ~= nil then return frameImages[key] end local out = {} for i, path in ipairs(paths) do - local ok, img = pcall(getImage, path) + local ok, img = pcall(function() + if not (colors and love.image and love.image.newImageData) then + return getImage(path) + end + local id = Assets.imageData(path) + local w, h = id:getDimensions() + local out2 = love.image.newImageData(w, h) + for y = 0, h - 1 do + for x = 0, w - 1 do + local r, g, b, a = id:getPixel(x, y) + r, g, b, a = recolorSample(r, g, b, a, colors) + out2:setPixel(x, y, r, g, b, a) + end + end + return love.graphics.newImage(out2) + end) if not ok then frameImages[key] = false return false @@ -237,17 +266,33 @@ end -- one entry's runtime form: the tile ids it claims, the textures a step -- picks from, and either a step sequence (hshift/frames) or a gate -- (toggle). nil when the entry's pixels could not be built. -local function buildAnim(spec, tilesetImagePath, perRow, quads) +-- +-- gbc, when present (RED++ with a baked atlas -- see getGbcAtlas), recolors +-- hshift/frames entries (water/flowers) the same way the atlas bakes their +-- static tile, so they match their surroundings instead of showing raw +-- grayscale over an otherwise fully-colored map. The "toggle" kind +-- (spinner puzzle blur, gfx/overworld/spinners.png) is a whole-atlas clone +-- built from the ORIGINAL grayscale atlas, not worth recoloring for a rare, +-- gameplay-gated blur -- it is skipped under gbc, same as the buildAnim +-- caller already does for a texture-build failure (the static, correctly- +-- colored tile shows through unanimated). +local function buildAnim(spec, tilesetImagePath, perRow, quads, gbc) local tiles = spec.tiles if not tiles then if spec.tile == nil then return nil end tiles = { spec.tile } end local period = spec.period or ANIM_PERIOD + local colors + if gbc then + local group = PaletteFX.worldGroupAt(gbc.tilesetId, gbc.mapId, tiles[1]) + colors = group and gbc.groupColors[group + 1] + end if spec.kind == "hshift" then local offsets = spec.offsets if not offsets or #offsets == 0 then return nil end - local textures = getShiftVariants(tilesetImagePath, perRow, tiles[1]) + local textures = getShiftVariants(tilesetImagePath, perRow, tiles[1], + colors, gbc and gbc.key) if not textures then return nil end local sequence = {} for i, offset in ipairs(offsets) do sequence[i] = offset + 1 end @@ -256,11 +301,12 @@ local function buildAnim(spec, tilesetImagePath, perRow, quads) elseif spec.kind == "frames" then local sequence = spec.sequence if not (spec.images and sequence and #sequence > 0) then return nil end - local textures = getFrameImages(spec.images) + local textures = getFrameImages(spec.images, colors, gbc and gbc.key) if not textures then return nil end return { tiles = tiles, textures = textures, sequence = sequence, period = period } elseif spec.kind == "toggle" then + if gbc then return nil end local image = getToggleImage(spec, tilesetImagePath, perRow) if not image then return nil end -- the patch texture is a whole-atlas clone, so each cell needs the @@ -271,10 +317,82 @@ local function buildAnim(spec, tilesetImagePath, perRow, quads) return nil end -function TileRenderer.new(map) +-- True GBC overworld coloring (COLORS=RED++): recolor the WHOLE tileset +-- atlas once, per (tileset image, map), rather than trying to retrofit the +-- SGB zone/shade-remap-shader post-process (built for a handful of coarse +-- screen regions) into per-tile precision -- pokered-gbc's real model is +-- "one of 8 four-color BG palettes baked per tile GRAPHIC" +-- (color/loadpalettes.asm LoadTilesetPalette), which is exactly a +-- recolored atlas, not a shader pass. Every existing draw path (batches, +-- quads, border fill) then just works unmodified, with no shader at +-- draw time; OverworldState.sgbWorldZones skips the shade-remap zone pass +-- entirely when this is active (re-running it over already-true-color +-- pixels would corrupt them), and SpriteRenderer's own OBP bake composites +-- on top with ordinary alpha blending -- no trueColor exemption needed, +-- because there is no shader left for it to be exempted from. +-- +-- Only the ROOF group (index 6, OVERWORLD/PLATEAU only) varies by town +-- (LoadTownPalette); Route 6's mid-map Saffron-roof y<2 split is not +-- reproduced (it would need a rebuild on crossing the boundary for two +-- tile-rows of one route -- not worth the complexity), so it bakes with +-- the route's own default roof (Vermilion's) throughout. +local gbcAtlasCache = {} + +local function getGbcAtlas(imagePath, tilesetId, mapId, perRow, data) + local key = imagePath .. "#gbc:" .. mapId + if gbcAtlasCache[key] ~= nil then return gbcAtlasCache[key] or nil end + local img = false + if love.image and love.image.newImageData then + local groupColors = PaletteFX.worldGroupColors(data, tilesetId, mapId, nil) + if groupColors then + local src = Assets.imageData(imagePath) + local iw, ih = src:getDimensions() + local out = love.image.newImageData(iw, ih) + local tileColors = {} + for t = 0, (iw / 8) * (ih / 8) - 1 do + local colors = tileColors[t] + if colors == nil then + local group = PaletteFX.worldGroupAt(tilesetId, mapId, t) + colors = (group and groupColors[group + 1]) or false + tileColors[t] = colors + end + local ox, oy = (t % perRow) * 8, math.floor(t / perRow) * 8 + for py = 0, 7 do + for px = 0, 7 do + local sx, sy = ox + px, oy + py + local r, g, b, a = src:getPixel(sx, sy) + r, g, b, a = recolorSample(r, g, b, a, colors) + out:setPixel(sx, sy, r, g, b, a) + end + end + end + img = love.graphics.newImage(out) + end + end + gbcAtlasCache[key] = img + return img or nil +end + +-- data: Game.data (threaded through explicitly, not required lazily, so +-- headless tests that build a map from a plain local table still work) +function TileRenderer.new(map, data) local self = setmetatable({}, TileRenderer) self.map = map + self.data = data self.image = getImage(map.tileset.image) + local gbcCtx + if data and PaletteFX.usesGbcPack() and PaletteFX.hasWorldTileset(map.tileset.id) then + local gbc = getGbcAtlas(map.tileset.image, map.tileset.id, map.id, + map.tileset.tilesPerRow, data) + if gbc then + self.image = gbc + self.gbcAtlas = true + -- also recolors the animated water/flower entries below, so they + -- match the atlas's static tiles instead of showing raw grayscale + gbcCtx = { tilesetId = map.tileset.id, mapId = map.id, key = "#gbc:" .. map.id, + groupColors = PaletteFX.worldGroupColors(data, map.tileset.id, map.id, nil) } + end + end -- a full-color atlas colors everything it paints, ring and border fill -- included, so every draw entry point claims its rect out of the pass self.trueColor = map.tileset.trueColor or nil @@ -301,7 +419,7 @@ function TileRenderer.new(map) local declared = map.tileset.animatedTiles or TileRenderer.defaultAnimatedTiles(map.tileset) for _, spec in ipairs(declared) do - local anim = buildAnim(spec, map.tileset.image, perRow, self.quads) + local anim = buildAnim(spec, map.tileset.image, perRow, self.quads, gbcCtx) if anim then anim.cells = {} anims[#anims + 1] = anim @@ -487,7 +605,10 @@ end -- rebuild after a block change (Cut trees) function TileRenderer:rebuild() - local fresh = TileRenderer.new(self.map) + local fresh = TileRenderer.new(self.map, self.data) + self.image = fresh.image + self.gbcAtlas = fresh.gbcAtlas + self.quads = fresh.quads self.ringBatch = fresh.ringBatch self.mapBatch = fresh.mapBatch self.anims = fresh.anims diff --git a/src/ui/OptionsMenu.lua b/src/ui/OptionsMenu.lua index 091a9962..2a68f0a5 100644 --- a/src/ui/OptionsMenu.lua +++ b/src/ui/OptionsMenu.lua @@ -3,7 +3,7 @@ -- (cycles the merged rulesets registry; gen1_faithful keeps the original -- quirks), plus the port's audio rows and display rows: music/SFX -- volume (0-7), music low-pass filter (OFF/1X/2X/3X), COLORS / TILT / --- GBC FX, and the MODS row that opens the mod manager. +-- GBC FX / VIDEO MODE, and the MODS row that opens the mod manager. -- Rows are descriptors fed through the ui.options.rows hook, so mods can -- add their own; CANCEL is appended after the hook and stays fixed on the -- bottom line like pokered's. @@ -12,6 +12,7 @@ local PaletteFX = require("src.render.PaletteFX") local Tilt = require("src.render.Tilt") local GBCFX = require("src.render.GBCFX") local GameSpeed = require("src.core.GameSpeed") +local VideoMode = require("src.core.VideoMode") local Logger = require("src.core.Logger") local Runtime = require("src.mods.Runtime") local OptionRows = require("src.ui.OptionRows") @@ -191,6 +192,16 @@ local function buildRows(game) GBCFX.setLevel(o.gbcfx) return true end }, + { id = "videoMode", label = "VIDEO MODE", + value = function(g) + return VideoMode.modeLabel(g.save.options.videoMode) + end, + step = function(g, dir) + local o = g.save.options + o.videoMode = VideoMode.cycle(o.videoMode, dir) + VideoMode.apply(o.videoMode) + return true + end }, -- fast-forward the logic clock only; music and sfx keep their tempo -- (src/core/GameSpeed.lua), so this is safe to leave on { id = "speed", label = "GAME SPEED", diff --git a/src/ui/TitleState.lua b/src/ui/TitleState.lua index f1d8f9aa..4049c04a 100644 --- a/src/ui/TitleState.lua +++ b/src/ui/TitleState.lua @@ -1,7 +1,7 @@ -- Title screen (engine/movie/title.asm + engine/menus/main_menu.asm): -- the logo (or a text fallback while the asset is missing), a cycling -- Pokémon front sprite, the copyright line, and the CONTINUE / NEW GAME --- / OPTION main menu on START or A. +-- / OPTION / EXIT GAME main menu on START or A. local Font = require("src.render.Font") local Music = require("src.core.Music") @@ -165,6 +165,11 @@ function TitleState:openMenu() table.insert(items, { label = "OPTION", onSelect = function() require("src.ui.Screens").push(game, "OptionsMenu") end }) + table.insert(items, { label = "EXIT GAME", onSelect = function() + if love.event and love.event.quit then + love.event.quit() + end + end }) game.stack:push(Menu.new(game, items, { tx = 0, ty = 0, tw = 13, th = #items * 2 + 2 })) end diff --git a/src/world/Map.lua b/src/world/Map.lua index f4061a08..714653bd 100644 --- a/src/world/Map.lua +++ b/src/world/Map.lua @@ -37,8 +37,11 @@ end -- on the raw blocks, honoring the surf rule (water/shore passable only -- while surfing, same fallbacks as Map.new). function Map.defPassable(def, tilesetDef, cx, cy, surfing) + -- Fail closed: a missing tileset used to return true and re-open the + -- Pallet south-shore stranding (cross onto ROUTE_21 solids). No data + -- means we cannot prove the landing is safe, so the step bumps. if not (def and tilesetDef and tilesetDef.blocks and tilesetDef.walkable) then - return true -- no data to judge with: keep the old permissive behavior + return false end local tx, ty = cx * 2, cy * 2 + 1 local bx, by = math.floor(tx / 4), math.floor(ty / 4) diff --git a/src/world/MapLoader.lua b/src/world/MapLoader.lua index 49760805..043e3ca3 100644 --- a/src/world/MapLoader.lua +++ b/src/world/MapLoader.lua @@ -23,7 +23,7 @@ function MapLoader.load(data, mapId) -- warp tiles are stored per tileset macro name; the generated tilesets -- module carries them in the tileset entry itself local map = Map.new(def, tilesetDef) - map.renderer = TileRenderer.new(map) + map.renderer = TileRenderer.new(map, data) cache[mapId] = map return map end diff --git a/src/world/NPC.lua b/src/world/NPC.lua index 395e4469..b94e83a0 100644 --- a/src/world/NPC.lua +++ b/src/world/NPC.lua @@ -26,7 +26,7 @@ function NPC.new(data, mapId, objDef) self.id = string.format("%s_obj_%d", mapId, objDef.index) local spriteDef = data.sprites[objDef.sprite] assert(spriteDef, "unknown sprite " .. tostring(objDef.sprite)) - self.sprite = SpriteRenderer.new(spriteDef) + self.sprite = SpriteRenderer.new(spriteDef, self.id) -- object_event coordinates are already walk-grid cells self.cellX, self.cellY = objDef.x, objDef.y self.px, self.py = self.cellX * 16, self.cellY * 16 diff --git a/src/world/OverworldController.lua b/src/world/OverworldController.lua index fb7de260..199dbde8 100644 --- a/src/world/OverworldController.lua +++ b/src/world/OverworldController.lua @@ -388,8 +388,25 @@ end -- which recolored the whole screen per map -- see the survey zoom -- entry in docs/known-differences.md). Border fill inherits the -- current map's palette. +-- +-- RED++ true overworld coloring does NOT go through this zone/shader +-- system at all: TileRenderer bakes real per-tile GBC colors straight into +-- a recolored tileset atlas (see TileRenderer's gbcAtlas), and +-- SpriteRenderer bakes sprites' OBP colors the same way, so the world +-- canvas is already final RGB by the time this runs. Returning an EMPTY +-- list here (when the current map has that baked atlas) skips the shader +-- entirely -- Renderer:endFrame's blit sees zoneList[1] == nil and falls +-- back to a plain, unshaded draw. Returning plain `nil` would NOT do this: +-- endFrame treats a nil worldZones as "no world-specific zones, reuse the +-- UI pass's zones" (sgbPalettes' whole-screen named-palette zone), which +-- would re-run the DMG shade-remap over already-true-color pixels using +-- an unrelated 4-color palette -- exactly the "colors are wrong" bug this +-- fixes. function OverworldState:sgbWorldZones() local PaletteFX = require("src.render.PaletteFX") + if PaletteFX.usesGbcPack() and self.map.renderer and self.map.renderer.gbcAtlas then + return {} + end local base = PaletteFX.pal(Game.data, self:paletteNameFor(self.map)) if not base then return nil end local vw, vh = Game.renderer:worldViewSize() @@ -2023,6 +2040,8 @@ function OverworldState:engageTrainer(npc, onDone) end -- Badges/items awarded after specific battles (data/scripts/victories.lua). +-- `deactivate` retires unfought gym/dojo trainers the way the originals' +-- SetEvent / SetEventRange do after the leader victory. function OverworldState:checkVictoryRewards(trainerClass, partyIndex) local victories = require("data.scripts.victories") local reward = victories[trainerClass .. "#" .. tostring(partyIndex or 1)] @@ -2031,6 +2050,11 @@ function OverworldState:checkVictoryRewards(trainerClass, partyIndex) if Game.save.flags[reward.flag] then return self:runVictoryHook() end Game.save.flags[reward.flag] = true end + if reward.deactivate then + for _, flag in ipairs(reward.deactivate) do + Game.save.flags[flag] = true + end + end local lines = {} if reward.badge then Game.save.inventory[reward.badge] = 1 diff --git a/src/world/Player.lua b/src/world/Player.lua index 0fe5093c..483d2ca2 100644 --- a/src/world/Player.lua +++ b/src/world/Player.lua @@ -26,12 +26,12 @@ function Player.new(data, cx, cy, facing) local walkId = FieldDefaults.fieldValue(data, "playerSprites", "walk") local surfId = FieldDefaults.fieldValue(data, "playerSprites", "surf") local bikeId = FieldDefaults.fieldValue(data, "playerSprites", "bike") - self.sprite = SpriteRenderer.new(data.sprites[walkId]) + self.sprite = SpriteRenderer.new(data.sprites[walkId], "player") if surfId and data.sprites[surfId] then - self.surfSprite = SpriteRenderer.new(data.sprites[surfId]) + self.surfSprite = SpriteRenderer.new(data.sprites[surfId], "player") end if bikeId and data.sprites[bikeId] then - self.bikeSprite = SpriteRenderer.new(data.sprites[bikeId]) + self.bikeSprite = SpriteRenderer.new(data.sprites[bikeId], "player") end -- the ledge-hop shadow quarter-tile (gfx/overworld/shadow.png, -- LedgeHoppingShadow, engine/overworld/ledges.asm) diff --git a/tests/mod_ui_tests.lua b/tests/mod_ui_tests.lua index f023ead6..b8705bef 100644 --- a/tests/mod_ui_tests.lua +++ b/tests/mod_ui_tests.lua @@ -205,7 +205,7 @@ end local om = OptionsMenu.new(optGame()) local WANT_IDS = { "textSpeed", "animations", "battleStyle", "ruleset", "musicVol", "sfxVol", "musicFilter", "colors", "tilt", - "gbcfx", "speed", "mods", "controls" } + "gbcfx", "videoMode", "speed", "mods", "controls" } check(#om.rows == #WANT_IDS, "vanilla options row count (plus MODS/CONTROLS)") for i, id in ipairs(WANT_IDS) do check(om.rows[i].id == id, "options row order: " .. id) @@ -238,7 +238,7 @@ check(om.game.save.options.musicVol == 0, "music volume clamps at 0") -- the MODS row is the manager's discoverable home local mgGame = optGame() om = OptionsMenu.new(mgGame) -om.rows[12].activate(mgGame) +om.rows[13].activate(mgGame) check(getmetatable(mgGame.stack:top()) == ManagerState, "the MODS row opens the manager") check(mgGame.stack:top().screenId == "ManagerState", @@ -248,7 +248,7 @@ check(mgGame.stack:top().screenId == "ManagerState", local BindingsMenu = require("src.ui.BindingsMenu") local cbGame = optGame() om = OptionsMenu.new(cbGame) -om.rows[13].activate(cbGame) +om.rows[14].activate(cbGame) local bm = cbGame.stack:top() check(getmetatable(bm) == BindingsMenu, "the CONTROLS row opens the rebind list") diff --git a/tests/parity_A.lua b/tests/parity_A.lua index 3e82b6ea..e082401f 100644 --- a/tests/parity_A.lua +++ b/tests/parity_A.lua @@ -263,4 +263,103 @@ do package.loaded["src.render.TextBox"] = realTB end +-- (7) gym / dojo leader victory deactivates unfought non-leader trainers +-- (PewterGym.asm "; deactivate gym trainers" / SetEventRange in the +-- other gyms; FightingDojo.asm SetEventRange through TRAINER_3). +do + local victories = require("data.scripts.victories") + local expected = { + ["OPP_BROCK#1"] = { "EVENT_BEAT_PEWTER_GYM_TRAINER_0" }, + ["OPP_MISTY#1"] = { "EVENT_BEAT_CERULEAN_GYM_TRAINER_0", + "EVENT_BEAT_CERULEAN_GYM_TRAINER_1" }, + ["OPP_LT_SURGE#1"] = { "EVENT_BEAT_VERMILION_GYM_TRAINER_0", + "EVENT_BEAT_VERMILION_GYM_TRAINER_1", + "EVENT_BEAT_VERMILION_GYM_TRAINER_2" }, + ["OPP_ERIKA#1"] = 7, + ["OPP_KOGA#1"] = 6, + ["OPP_SABRINA#1"] = 7, + ["OPP_BLAINE#1"] = 7, + ["OPP_GIOVANNI#3"] = 8, + ["OPP_BLACKBELT#1"] = 4, + } + for key, want in pairs(expected) do + local d = victories[key] and victories[key].deactivate + check(d ~= nil, key .. " lists trainers to deactivate") + if type(want) == "number" then + eq(#d, want, key .. " deactivates " .. want .. " trainers") + else + for i, flag in ipairs(want) do + eq(d[i], flag, key .. " deactivate[" .. i .. "]") + end + end + end + + -- Cinnabar trainers have no extracted def_trainers; seeded headers let + -- EVENT_BEAT_CINNABAR_GYM_TRAINER_* satisfy trainerDefeated without + -- stamping defeatedTrainers (which would also open the quiz gates). + Data:seedCinnabarGymTrainerHeaders() + check(Data.trainer_headers.CinnabarGym ~= nil, + "CinnabarGym trainer headers seeded for deactivate flags") + local nerd1 = Data:trainerHeader("CinnabarGym", 2) + check(nerd1 and nerd1.event == "EVENT_BEAT_CINNABAR_GYM_TRAINER_0" + and nerd1.range == 0, + "Cinnabar SUPER_NERD1 header: event + no sight range") + + require("src.render.Font").load(Data) + local Game = require("src.core.Game") + local Input = require("src.core.Input") + local StateStack = require("src.core.StateStack") + local Renderer = require("src.render.Renderer") + local SaveData = require("src.core.SaveData") + local OW = require("src.world.OverworldController") + local realTB = package.loaded["src.render.TextBox"] + package.loaded["src.render.TextBox"] = { + new = function(game, text, done) return { text = text, onDone = done } end, + } + Game.data = Data + Game.input = Input; Input:init() + Game.renderer = Renderer; Renderer:init() + Game.stack = StateStack; StateStack:init() + while Game.stack:top() do Game.stack:pop() end + Game.save = SaveData.newGame() + Game.save.flags = {} + Game.save.inventory = {} + Game.save.defeatedTrainers = {} + Game.stack:push(OW, "PEWTER_GYM", 4, 13, "up") + local ow = Game.stack:top() + ow:checkVictoryRewards("OPP_BROCK", 1) + check(Game.save.flags.EVENT_BEAT_BROCK, "Brock victory sets EVENT_BEAT_BROCK") + check(Game.save.inventory.BOULDERBADGE == 1, "Brock victory awards BOULDERBADGE") + check(Game.save.flags.EVENT_BEAT_PEWTER_GYM_TRAINER_0, + "Brock victory deactivates the Pewter Gym Cooltrainer") + local pewterNpc + for _, npc in ipairs(ow.npcs) do + if npc.def.index == 2 then pewterNpc = npc break end + end + check(pewterNpc and ow:trainerDefeated(pewterNpc), + "unfought Pewter gym trainer is defeated after badge") + + while Game.stack:top() do Game.stack:pop() end + Game.save = SaveData.newGame() + Game.save.flags = {} + Game.save.inventory = {} + Game.save.defeatedTrainers = {} + Game.stack:push(OW, "CINNABAR_GYM", 16, 15, "up") + ow = Game.stack:top() + ow:checkVictoryRewards("OPP_BLAINE", 1) + check(Game.save.flags.EVENT_BEAT_CINNABAR_GYM_TRAINER_3, + "Blaine victory deactivates Cinnabar quiz trainers") + check(next(Game.save.defeatedTrainers) == nil, + "Blaine deactivate does not stamp defeatedTrainers (gates stay put)") + local cinnabarNpc + for _, npc in ipairs(ow.npcs) do + if npc.def.index == 5 then cinnabarNpc = npc break end + end + check(cinnabarNpc and ow:trainerDefeated(cinnabarNpc), + "unfought Cinnabar trainer is defeated via seeded header event") + + while Game.stack:top() do Game.stack:pop() end + package.loaded["src.render.TextBox"] = realTB +end + S.finish() diff --git a/tests/parity_connection_collision.lua b/tests/parity_connection_collision.lua new file mode 100644 index 00000000..9ac5bd87 --- /dev/null +++ b/tests/parity_connection_collision.lua @@ -0,0 +1,95 @@ +-- Regression: map-edge crossings must read the NEIGHBOR map's collision. +-- +-- Without that read, Pallet Town's south shore land spit (cells x=2,3 at +-- y=17) walked straight onto ROUTE_21 (2,0)/(3,0) -- solid tiles. From +-- (2,0) a further step south lands on walkable (2,1), after which the +-- solid edge cell blocks the only path back to the seam, so the player +-- cannot return north. pokered's CollisionCheckOnLand reads the neighbor +-- strip's tile bytes and bumps; the port must do the same via +-- Map.defPassable before crossConnection commits. +-- +-- Self-contained; run via `luajit tests/parity_connection_collision.lua`. +package.path = "./?.lua;./?/init.lua;" .. package.path +if not _G.love then _G.love = require("tests.love_stub") end + +local Data = require("src.core.Data") +if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end + +local Game = require("src.core.Game") +local Input = require("src.core.Input") +local Map = require("src.world.Map") +local MapLoader = require("src.world.MapLoader") +local Renderer = require("src.render.Renderer") +local SaveData = require("src.core.SaveData") +local StateStack = require("src.core.StateStack") +local OW = require("src.world.OverworldController") +local S = require("tests.harness").suite("parity connection collision") +local check, eq = S.check, S.eq + +local pallet = MapLoader.load(Data, "PALLET_TOWN") +local route21 = MapLoader.load(Data, "ROUTE_21") +local ts = Data.tilesets[route21.def.tileset] + +-- ground truth: the spit is land, the Route 21 landings are solid, and +-- the water column next to it is surf-only +for _, x in ipairs({ 2, 3 }) do + check(pallet:isWalkableCell(x, 17), + ("Pallet south shore (%d,17) is walkable land"):format(x)) + check(not route21:isWalkableCell(x, 0), + ("ROUTE_21 (%d,0) is a solid landing"):format(x)) + check(not Map.defPassable(route21.def, ts, x, 0, false), + ("defPassable refuses land->solid at ROUTE_21 (%d,0)"):format(x)) +end +check(route21:isWaterCell(5, 0), "ROUTE_21 (5,0) is water") +check(not Map.defPassable(route21.def, ts, 5, 0, false), + "water landing without Surf is refused") +check(Map.defPassable(route21.def, ts, 5, 0, true), + "water landing with Surf is allowed") +check(not Map.defPassable(route21.def, nil, 2, 0, false), + "missing tileset fails closed (no permissive fallback)") + +-- Live edge: standing on the spit and pressing into the seam must bump, +-- not swap maps. Pallet -> Route 1 north still crosses on grass. +Game.data = Data +Game.input = Input; Input:init() +Game.renderer = Renderer; Renderer:init() +Game.stack = StateStack +StateStack:init() +Game.save = SaveData.newGame() +Game.overworld = OW + +while Game.stack:top() do Game.stack:pop() end +Game.stack:push(OW, "PALLET_TOWN", 2, 17, "down") +local ow = Game.stack:top() +eq(ow.map.id, "PALLET_TOWN", "start on Pallet south shore") +eq(ow.player.cellX, 2, "spit x") +eq(ow.player.cellY, 17, "spit y") + +local conn = ow.map:connection("south") +check(conn and conn.map == "ROUTE_21", "Pallet south connects to ROUTE_21") +check(ow:crossConnection("down", conn) == false, + "crossConnection bumps on ROUTE_21 solid landing") +eq(ow.map.id, "PALLET_TOWN", "still on Pallet after the refused cross") +eq(ow.player.cellX, 2, "x unchanged after bump") +eq(ow.player.cellY, 17, "y unchanged after bump") + +-- surfing the water column still crosses +ow.player.cellX, ow.player.cellY = 5, 17 +ow.player.px, ow.player.py = 5 * 16, 17 * 16 +ow.player.surfing = true +ow.player.moving = false +check(ow:crossConnection("down", conn) == true, + "surfing Pallet water still crosses onto ROUTE_21") +eq(ow.map.id, "ROUTE_21", "surf cross lands on ROUTE_21") + +-- north seam onto Route 1 grass still works on foot +while Game.stack:top() do Game.stack:pop() end +Game.stack:push(OW, "PALLET_TOWN", 10, 0, "up") +ow = Game.stack:top() +local north = ow.map:connection("north") +check(north and north.map == "ROUTE_1", "Pallet north connects to ROUTE_1") +check(ow:crossConnection("up", north) == true, + "Pallet -> Route 1 grass crossing still allowed") +eq(ow.map.id, "ROUTE_1", "north cross lands on ROUTE_1") + +S.finish() diff --git a/tests/run_tests.lua b/tests/run_tests.lua index ccccc589..85b08d76 100644 --- a/tests/run_tests.lua +++ b/tests/run_tests.lua @@ -38,6 +38,19 @@ check(pallet:isWarpTileCell(5, 5), "Red's house door is a door tile") local w = pallet:warpAtCell(5, 5) eq(w.def.destMap, "REDS_HOUSE_1F", "door warp goes to Red's house") +-- Pallet south shore spit: land that faces ROUTE_21 solids. Crossing +-- without reading the neighbor tile stranded players (see +-- tests/parity_connection_collision.lua). +local Map = require("src.world.Map") +local route21Def = Data.maps.ROUTE_21 +local route21Tileset = Data.tilesets[route21Def.tileset] +check(pallet:isWalkableCell(2, 17), "Pallet south shore spit (2,17) walkable") +check(pallet:isWalkableCell(3, 17), "Pallet south shore spit (3,17) walkable") +check(not Map.defPassable(route21Def, route21Tileset, 2, 0, false), + "ROUTE_21 (2,0) refuses a land edge cross") +check(not Map.defPassable(route21Def, route21Tileset, 3, 0, false), + "ROUTE_21 (3,0) refuses a land edge cross") + -- signs local sign = pallet:signAtCell(13, 13) eq(sign.text, "TEXT_PALLETTOWN_OAKSLAB_SIGN", "Oak's lab sign at (13,13)") @@ -1132,6 +1145,20 @@ do tb.trainer.name)), "trainer defeat uses ' defeated !'") + -- pret GetTrainerName_: rival classes show wRivalName, not "RIVAL1" + do + local savedRival = Game.save.player.rival + Game.save.player.rival = "GARY" + Game.save.party = { Pokemon.new(Data, "BULBASAUR", 30) } + local rb = BattleState.newTrainer(Game, "OPP_RIVAL1", 1) + eq(rb.trainer.name, "GARY", "rival battle uses saved rival name") + check(rb.introText:find("GARY", 1, true), + "rival intro wants-to-fight uses rival name") + check(Data.trainers.OPP_RIVAL1.name == "RIVAL1", + "shared trainer data keeps the RIVAL1 placeholder") + Game.save.player.rival = savedRival + end + -- PartyMenu onCancel fires when backing out without a pick do local PartyMenu = require("src.ui.PartyMenu") @@ -1999,8 +2026,8 @@ do -- option boxes (the port rows plus the MODS/CONTROLS entries) through a 4-box -- viewport with a $EE ▼ marker; MUSIC VOL / SFX VOL clamp at 0..7 like -- the text-speed cursor clamps at its ends (.pressedLeftInTextSpeed), --- MUSIC FILTER cycles OFF/1X/2X/3X, and COLORS / TILT / GBC FX cycle --- their display modes. +-- MUSIC FILTER cycles OFF/1X/2X/3X, and COLORS / TILT / GBC FX / VIDEO MODE +-- cycle their display modes. do local OptionsMenu = require("src.ui.OptionsMenu") local OInput = require("src.core.Input") @@ -2008,6 +2035,7 @@ do local Tilt = require("src.render.Tilt") local GBCFX = require("src.render.GBCFX") local GameSpeed = require("src.core.GameSpeed") + local VideoMode = require("src.core.VideoMode") local SD = require("src.core.SaveData") -- Isolate from earlier save/options writes in this suite SD.saveOptions(SD.defaultOptions()) @@ -2026,6 +2054,8 @@ do eq(og.save.options.colors, "gbc", "new saves default COLORS to GBC") eq(og.save.options.tilt, 0, "new saves default TILT to OFF") eq(og.save.options.gbcfx, 0, "new saves default GBC FX to OFF") + eq(og.save.options.videoMode, "windowed", + "new saves default VIDEO MODE to WINDOWED") eq(om.scroll, 0, "options viewport starts at the top") for _ = 1, 4 do press("down") end eq(om.index, 5, "cursor reaches MUSIC VOL") @@ -2065,7 +2095,15 @@ do for _ = 1, 4 do press("a") end eq(og.save.options.gbcfx, 0, "GBC FX wraps back to OFF") press("down") - eq(om.index, 11, "cursor reaches GAME SPEED") + eq(om.index, 11, "cursor reaches VIDEO MODE") + press("a") + eq(og.save.options.videoMode, "borderless", + "A cycles VIDEO MODE to BORDERLESS") + press("a") + eq(og.save.options.videoMode, "windowed", + "VIDEO MODE wraps back to WINDOWED") + press("down") + eq(om.index, 12, "cursor reaches GAME SPEED") press("a") eq(og.save.options.speed, 2, "A cycles GAME SPEED to 2X") -- Driven by the level list rather than a literal press count: adding a @@ -2074,25 +2112,26 @@ do for _ = 1, #GameSpeed.LEVELS - 1 do press("a") end eq(og.save.options.speed, 1, "GAME SPEED wraps back to NORMAL") press("down") - eq(om.index, 12, "cursor reaches MODS") + eq(om.index, 13, "cursor reaches MODS") press("down") - eq(om.index, 13, "cursor reaches CONTROLS") + eq(om.index, 14, "cursor reaches CONTROLS") press("down") - eq(om.index, 14, "CANCEL stays the fixed final row") - eq(om.scroll, 9, "CANCEL keeps the last option boxes on screen") + eq(om.index, 15, "CANCEL stays the fixed final row") + eq(om.scroll, 10, "CANCEL keeps the last option boxes on screen") om:draw() -- smoke: scrolled layout draws under the headless stub press("a") check(popped, "A on CANCEL closes the options menu") local om2 = OptionsMenu.new(og) OInput.pressed = { up = true }; om2:update(1 / 60); OInput.pressed = {} - eq(om2.index, 14, "up from the top wraps to CANCEL") - eq(om2.scroll, 9, "wrapping to CANCEL scrolls to the tail") + eq(om2.index, 15, "up from the top wraps to CANCEL") + eq(om2.scroll, 10, "wrapping to CANCEL scrolls to the tail") -- headless-safe: no love.audio, setters only update internal state require("src.core.Music").applyOptions(og.save.options) require("src.core.Sound").applyOptions(og.save.options) PaletteFX.applyOptions(og.save.options) Tilt.applyOptions(og.save.options) GBCFX.applyOptions(og.save.options) + VideoMode.applyOptions(og.save.options) end end diff --git a/tools/extract/palettes.py b/tools/extract/palettes.py index 9be7d694..6be5a70e 100644 --- a/tools/extract/palettes.py +++ b/tools/extract/palettes.py @@ -18,11 +18,44 @@ Output: data/generated/palettes.lua pokemon[SPECIES] = NAME (PAL_ prefix stripped) Output: data/palettes_gbc.lua (committed; not wiped by ROM import) - same shape, plus per-species palette entries (BULBASAUR, …) + same shape, plus per-species palette entries (BULBASAUR, …), plus a + `world` table (below) for true overworld tile/roof/sprite coloring. 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). + +Sources (pokered-gbc color/**, the real GBC overworld coloring engine): + color/data/map_palette_assignments.asm per tileset: 96 tile-graphic-id + -> palette-group (0-7) bytes + color/data/map_palette_sets.asm per tileset: 8 group -> palette- + constant bytes (positional) + color/data/map_palettes.asm palette-constant -> 4 RGB colors + color/data/roofpalettes.asm per pokered map ID (positional, + matches map.def.index): roof + 2-color override + the labeled + RGB blocks it points at + color/data/spritepalettes.asm 8 four-color OBJ palettes + color/sprites.asm SpritePaletteAssignments: 1-based + picture ID -> OBJ palette 0-3, or + the "db 4" random-per-instance + sentinel (ColorOverworldSprite) + +Output `world` table shape: + world.tileGroups[TILESET][tileId 0-95] = group index 0-7 + world.groupColors[TILESET][group 0-7] = { {r,g,b} x4 } + world.roofGroup[TILESET] = the town-overridable group index (OVERWORLD/ + PLATEAU only) + world.roofByMapIndex[mapIndex] = { {r,g,b} x2 } (matches map.def.index) + world.spritePalettes[0-7] = { {r,g,b} x4 } + world.spriteAssignment[pictureIndex 0-based] = group 0-3 or "random" + (pictureIndex matches the N in a sprite def's + `source = "ROM:SpriteSheetPointerTable[N]"`) + +The 3 hardcoded Celadon Mart tile-group exceptions and the Route 6/Saffron +roof y-split (LoadTilesetPalette / LoadTownPalette control flow, not data) +are NOT extracted here -- they live as a small named table in +src/render/PaletteFX.lua next to the code that consumes `world`. """ import argparse @@ -185,6 +218,271 @@ def _parse_gbc_monster_palettes(path): return mon_pals +# ---- color/** overworld attribute data ------------------------------------ +# +# Two tiny fixed enums, hand-transcribed rather than parsed: both are +# `EQU`/`const` lists that never change (they are the hardware-shaped slot +# numbering, not game content), and each uses a different const-declaration +# style that isn't worth a bespoke parser for 4-8 entries. +# color/data/map_palette_constants.asm:69-78 (`const_value = 0` block) +_GROUP_NAMES = ["GRAY", "RED", "GREEN", "BLUE", "YELLOW", "BROWN", "ROOF", "TEXT"] +_GROUP_INDEX = {name: i for i, name in enumerate(_GROUP_NAMES)} +# color/sprites.asm:15-18 (`SPR_PAL_* EQU n`) +_SPR_PAL = {"SPR_PAL_ORANGE": 0, "SPR_PAL_BLUE": 1, "SPR_PAL_GREEN": 2, "SPR_PAL_BROWN": 3} + + +def _rgbN(nums): + return [[_scale5(nums[i]), _scale5(nums[i + 1]), _scale5(nums[i + 2])] + for i in range(0, len(nums), 3)] + + +def _parse_rgb_table(path, header_re, n_nums): + """name -> n_nums/3 RGB colors, from blocks of `header_re` + RGB lines + (one or more `RGB r,g,b[,r,g,b...]` lines per block, however they're + wrapped -- matches both the 1-color-per-line and 4-colors-per-line + styles pokered-gbc uses in different files).""" + out = {} + order = [] + pending = None + buf = [] + for lineno, line in _read_raw(path): + s = line.strip() + m = header_re.match(s) + if m: + if pending: + util.die(f"{path}:{lineno}: incomplete RGB block for " + f"{pending} ({len(buf)}/{n_nums})") + pending = m.group(1) + buf = [] + continue + m = re.match(r"RGB\s+([\d,\s]+)", s) + if not m or not pending: + continue + nums = [n for n in re.split(r"[,\s]+", m.group(1).strip()) if n] + buf.extend(nums) + if len(buf) < n_nums: + continue + if len(buf) != n_nums: + util.die(f"{path}:{lineno}: expected {n_nums} components for " + f"{pending}, got {len(buf)}") + out[pending] = _rgbN(buf) + order.append(pending) + pending = None + buf = [] + if pending: + util.die(f"{path}: incomplete RGB block for {pending}") + return out, order + + +_TILESET_HEADER_RE = re.compile(r"^;\s*([A-Z][A-Z0-9_]*)\s*$") + + +def _parse_map_palette_assignments(path): + """color/data/map_palette_assignments.asm: per tileset, 96 tile-graphic + palette-group bytes (0-7, symbolic), 16 per line / 6 lines.""" + tile_groups = {} + order = [] + current = None + vals = [] + + def flush(lineno): + if len(vals) != 96: + util.die(f"{path}:{lineno}: {current} has {len(vals)} tile " + f"groups (want 96)") + tile_groups[current] = vals[:] + order.append(current) + + for lineno, line in _read_raw(path): + s = line.strip() + m = _TILESET_HEADER_RE.match(s) + if m: + if current: + flush(lineno) + current, vals = m.group(1), [] + continue + m = re.match(r"db\s+(.*)$", s) + if not m or not current: + continue + for tok in m.group(1).split(","): + tok = tok.strip() + if tok not in _GROUP_INDEX: + util.die(f"{path}:{lineno}: unknown palette group {tok!r}") + vals.append(_GROUP_INDEX[tok]) + if current: + flush("EOF") + return tile_groups, order + + +def _parse_map_palette_sets(path): + """color/data/map_palette_sets.asm: per tileset, 8 palette-constant + names, positional (index = the palette-group 0-7 above).""" + sets = {} + order = [] + current = None + vals = [] + + def flush(lineno): + if len(vals) != 8: + util.die(f"{path}:{lineno}: {current} has {len(vals)} palette " + f"sets (want 8)") + sets[current] = vals[:] + order.append(current) + + for lineno, line in _read_raw(path): + s = line.strip() + m = _TILESET_HEADER_RE.match(s) + if m: + if current: + flush(lineno) + current, vals = m.group(1), [] + continue + m = re.match(r"db\s+(\w+)", s) + if m and current: + vals.append(m.group(1)) + if current: + flush("EOF") + return sets, order + + +def _parse_roof_order(path): + """color/data/roofpalettes.asm's RoofPalettes: dw list, positional by + pokered map ID (matches map.def.index in the recomp project).""" + names = [] + in_table = False + for lineno, line in _read_raw(path): + s = line.strip() + if s.startswith("RoofPalettes:"): + in_table = True + continue + if not in_table: + continue + m = re.match(r"dw\s+(\w+)", s) + if m: + names.append(m.group(1)) + continue + if s == "": + continue + break # first non-dw, non-blank line (a Label:) ends the table + if not names: + util.die(f"{path}: RoofPalettes table not found or empty") + return names + + +def _parse_sprite_assignments(path): + """color/sprites.asm SpritePaletteAssignments: 1-based picture ID (from + the '; 0xNN: SPRITE_NAME' comment) -> OBJ palette 0-3, or "random" for + the "db 4" per-instance sentinel (ColorOverworldSprite). Returned keyed + 0-based (pictureId - 1), matching a recomp sprite def's + `source = "ROM:SpriteSheetPointerTable[N]"` index.""" + assignments = {} + in_table = False + pending_id = None + header_re = re.compile(r"^;\s*0x([0-9a-fA-F]+):") + for lineno, line in _read_raw(path): + s = line.strip() + if s.startswith("SpritePaletteAssignments:"): + in_table = True + continue + if not in_table: + continue + m = header_re.match(s) + if m: + pending_id = int(m.group(1), 16) + continue + m = re.match(r"db\s+(\w+)", s) + if m and pending_id is not None: + tok = m.group(1) + if tok == "4": + assignments[pending_id - 1] = "random" + elif tok in _SPR_PAL: + assignments[pending_id - 1] = _SPR_PAL[tok] + else: + util.die(f"{path}:{lineno}: unknown sprite palette {tok!r}") + pending_id = None + continue + if s and not s.startswith(";"): + break # next label (AnimationTileset1Palettes:) ends the table + if not assignments: + util.die(f"{path}: SpritePaletteAssignments not found or empty") + return assignments + + +def extract_gbc_world(pokered_gbc): + """pokered-gbc color/** -> the `world` table (see module docstring): + real per-tile GBC BG-palette groups, per-town roof overrides, and + overworld sprite OBJ palettes, for true overworld parity under + COLORS=RED++ (LoadTilesetPalette / LoadTownPalette / ColorOverworldSprite).""" + base = os.path.join(pokered_gbc, "color") + + tile_groups, ts_order = _parse_map_palette_assignments( + os.path.join(base, "data/map_palette_assignments.asm")) + pal_sets, ts_order2 = _parse_map_palette_sets( + os.path.join(base, "data/map_palette_sets.asm")) + if set(ts_order) != set(ts_order2): + util.die("map_palette_assignments.asm / map_palette_sets.asm " + "tileset lists disagree") + if len(ts_order) < 20: + util.die(f"map_palette_assignments.asm: parsed only {len(ts_order)} " + f"tilesets (want ~24)") + + map_palettes, _ = _parse_rgb_table( + os.path.join(base, "data/map_palettes.asm"), + re.compile(r"^;\s*0x[0-9a-fA-F]+:\s*(\w+)\s*$"), 12) + + group_colors = {} + for ts in ts_order: + colors = [] + for slot, const in enumerate(pal_sets[ts]): + if const not in map_palettes: + util.die(f"map_palette_sets.asm: {ts} slot {slot}: unknown " + f"palette constant {const}") + colors.append(map_palettes[const]) + group_colors[ts] = colors + + # Only these two tilesets are ever town-roof-swapped + # (LoadTilesetPalette: `cp 0` / `cp PLATEAU` before calling + # LoadTownPalette) -- the ROOF slot for every other tileset is static. + roof_group = {ts: _GROUP_INDEX["ROOF"] + for ts in ("OVERWORLD", "PLATEAU") if ts in group_colors} + + roof_path = os.path.join(base, "data/roofpalettes.asm") + roof_colors, _ = _parse_rgb_table( + roof_path, re.compile(r"^(?!RoofPalettes)(\w+):\s*$"), 6) + roof_order = _parse_roof_order(roof_path) + roof_by_index = {} + for i, name in enumerate(roof_order): + if name not in roof_colors: + util.die(f"{roof_path}: RoofPalettes references unknown label " + f"{name}") + roof_by_index[i] = roof_colors[name] + + sprite_path = os.path.join(base, "data/spritepalettes.asm") + raw_sprite_pals, _ = _parse_rgb_table( + sprite_path, re.compile(r"^;\s*(\d+)\s*$"), 12) + sprite_palettes = {int(k): v for k, v in raw_sprite_pals.items()} + if len(sprite_palettes) != 8: + util.die(f"{sprite_path}: parsed {len(sprite_palettes)} sprite " + f"palettes (want 8)") + + sprite_assignment = _parse_sprite_assignments( + os.path.join(base, "sprites.asm")) + if len(sprite_assignment) != 72: + util.die(f"sprites.asm: parsed {len(sprite_assignment)} sprite " + f"palette assignments (want 72)") + + tile_groups_out = {ts: {i: v for i, v in enumerate(vals)} + for ts, vals in tile_groups.items()} + + return { + "tileGroups": tile_groups_out, + "groupColors": group_colors, + "roofGroup": roof_group, + "roofByMapIndex": roof_by_index, + "spritePalettes": sprite_palettes, + "spriteAssignment": sprite_assignment, + } + + def extract_gbc(pokered_gbc, out_path): """Build the Red++ / pokered-gbc SuperPalette pack used by COLORS=RED++.""" super_path = os.path.join(pokered_gbc, "data/super_palettes.asm") @@ -218,17 +516,23 @@ def extract_gbc(pokered_gbc, out_path): if name not in palettes: util.die(f"super_palettes.asm: PAL_{name} missing") + world = extract_gbc_world(pokered_gbc) + # write_lua stamps "Generated by tools/build_data.py"; keep that header # but point the source field at the gbc tree. util.write_lua( out_path, - {"source": "pokered-gbc data/super_palettes.asm + data/mon_palettes.asm", + {"source": "pokered-gbc data/super_palettes.asm + data/mon_palettes.asm" + " + color/**", "palettes": palettes, "order": order, - "pokemon": mon_pals}, + "pokemon": mon_pals, + "world": world}, header="Red++ / pokered-gbc SuperPalettes (COLORS=RED++): 4 8-bit RGB\n" "colors per palette (color 0 first), including per-species\n" - "pals and the GEN_2 MonsterPalettes assignment.") + "pals and the GEN_2 MonsterPalettes assignment, plus `world`\n" + "(true overworld tile/roof/sprite GBC coloring -- see this\n" + "file's module docstring for its shape).") return palettes, mon_pals