"""Build the factual region atlas and authored overview maps. Run from the repository root. Coordinate sources are cached in /tmp/pokemon-atlas. Later regions are explicitly schematic and do not imply exact route geometry. """ import argparse, hashlib, json, re, urllib.request from pathlib import Path parser=argparse.ArgumentParser() parser.add_argument('--cache',type=Path,default=Path('/tmp/pokemon-atlas')) args=parser.parse_args();args.cache.mkdir(parents=True,exist_ok=True) sources={ 'pokeemerald-region.json':'https://raw.githubusercontent.com/pret/pokeemerald/master/src/data/region_map/region_map_sections.json', 'pokefirered-region.json':'https://raw.githubusercontent.com/pret/pokefirered/master/src/data/region_map/region_map_sections.json', 'platinum-map.json':'https://raw.githubusercontent.com/pret/pokeplatinum/main/res/town_map/town_map_data.json', 'johto-landmarks.asm':'https://raw.githubusercontent.com/pret/pokecrystal/master/data/maps/landmarks.asm' } checksums={'pokeemerald-region.json': '3a5f8c07c85c17f05cec06d049305962e9a9a1d2bd6eae935f3887866a4ad491', 'pokefirered-region.json': '82e456cbe86a57ebc4bd0d88d6e000e83e28340895ac038079c38d415bc6de5f', 'platinum-map.json': 'b5f6792e7993afa57df3d5b86c0af57488d23642576c4a224ba401bd762ba6b8', 'johto-landmarks.asm': 'bf61258379901daf794555b1a8f73bcf634d50ef3ca6e067f88f599b9df9816f'} for filename,url in sources.items(): path=args.cache/filename if not path.exists(): with urllib.request.urlopen(url,timeout=60) as response: path.write_bytes(response.read()) if hashlib.sha256(path.read_bytes()).hexdigest()!=checksums[filename]: raise ValueError(f'{filename}: upstream coordinates changed; review the map and update its checksum before rebuilding') from pathlib import Path locations=json.load(open('data/pokemon-locations.json'))['locations'] def slug(s):return re.sub(r'[^a-z0-9]+','-',s.lower()).strip('-') def location(key,region): key=key.replace('eternia','eterna').replace('mount-coronet','mt-coronet').replace('tin-tower','bell-tower') keys=[key,f'{region}-{key}',key.replace('route-','sea-route-'),f'{region}-{key.replace("route-","sea-route-")}'] return next((p for p in locations if p['slug'] in keys and (p['region'] or '').lower()==region),None) def node(key,x,y,region,cells=None): p=location(key,region) return {'slug':p['slug'] if p else key,'name':p['name'] if p else key.replace('-',' ').title(),'x':x,'y':y,'kind':'town' if re.search('town|city|village|area$|league',key) else 'route' if 'route' in key else 'landmark','cells':cells or []} regions={} for region,file in [('hoenn',args.cache/'pokeemerald-region.json'),('kanto',args.cache/'pokefirered-region.json')]: d=json.load(open(file))['map_sections'];nodes=[] for p in d: if 'x' not in p:continue key=slug(p['name']);x=p['x'];y=p['y'];w=p['width'];h=p['height'] if region=='hoenn' and key=='none':break if region=='kanto' and key=='one-island':break nodes.append(node(key,x+w/2,y+h/2,region,[[x,y,w,h]])) regions[region]={'name':region.title(),'width':32,'height':20,'nodes':nodes,'source':f'https://github.com/pret/poke{ "emerald" if region=="hoenn" else "firered" }/blob/master/src/data/region_map/region_map_sections.json','precision':'Game map coordinates'} # Platinum town map blocks preserve branching paths and geographic placement. d=json.load(open(args.cache/'platinum-map.json'));groups={} for b in d['blocks']: key=re.sub(r'_(north|south)$','',b['area'] or '').replace('_','-') if not key:continue groups.setdefault(key,[]).append([b['x'],b['z'],1,1]) if b.get('landmark') and b['landmark'].replace('_','-')!=key: land=b['landmark'].replace('_','-') if location(land,'sinnoh'):groups.setdefault(land,[]).append([b['x'],b['z'],1,1]) nodes=[node(k,sum(c[0]+.5 for c in cells)/len(cells),sum(c[1]+.5 for c in cells)/len(cells),'sinnoh',cells) for k,cells in groups.items()] regions['sinnoh']={'name':'Sinnoh','width':32,'height':30,'nodes':nodes,'source':'https://github.com/pret/pokeplatinum/blob/main/res/town_map/town_map_data.json','precision':'Platinum town map coordinates'} s=(args.cache/'johto-landmarks.asm').read_text();nodes=[] for x,y,key in re.findall(r'landmark\s+(\d+),\s*(\d+),\s*(\w+)Name',s): if key=='PalletTown':break name=re.sub(r'([a-z])([A-Z])',r'\1-\2',key);name=re.sub(r'([A-Za-z])(\d)',r'\1-\2',name) nodes.append(node(name.lower(),int(x)/5,int(y)/5,'johto')) regions['johto']={'name':'Johto','width':32,'height':30,'nodes':nodes,'source':'https://github.com/pret/pokecrystal/blob/master/data/maps/landmarks.asm','precision':'Crystal landmark coordinates'} # Later-region overview points: deliberately identified as schematic rather than exact route maps. landmarks={ 'unova':'Nuvema Town,23,26;Accumula Town,23,23;Striaton City,26,20;Nacrene City,21,20;Castelia City,16,23;Nimbasa City,16,16;Driftveil City,9,17;Mistralton City,7,10;Icirrus City,11,5;Opelucid City,20,5;Lacunosa Town,26,6;Undella Town,28,12;Black City,26,17;White Forest,28,18;Aspertia City,4,26;Floccesy Town,7,25;Virbank City,10,25;Humilau City,29,4;Pokemon League,28,1', 'kalos':'Vaniville Town,23,25;Aquacorde Town,23,22;Santalune City,24,19;Lumiose City,16,12;Camphrier Town,12,19;Ambrette Town,5,24;Cyllage City,4,18;Geosenge Town,3,12;Shalour City,6,8;Coumarine City,11,6;Laverre City,17,7;Dendemille Town,24,8;Anistar City,28,12;Couriway Town,28,20;Snowbelle City,27,25;Pokemon Village,24,28;Pokemon League,30,17', 'alola':'Iki Town,6,6;Hauoli City,3,9;Melemele Sea,6,12;Heahea City,22,10;Paniola Town,24,7;Royal Avenue,26,11;Konikoni City,21,13;Malie City,26,20;Tapu Village,22,23;Po Town,21,17;Mount Lanakila,21,21;Seafolk Village,5,23;Poni Wilds,4,20;Vast Poni Canyon,7,17;Aether Paradise,14,15', 'galar':'Postwick,15,27;Wedgehurst,15,24;Wild Area,15,20;Motostoke,15,17;Turffield,8,14;Hulbury,23,14;Hammerlocke,15,10;Stow on Side,6,10;Ballonlea,7,5;Circhester,25,8;Spikemuth,26,17;Wyndon,15,3;Isle of Armor,29,23;Crown Tundra,7,28', 'hisui':'Jubilife Village,6,15;Obsidian Fieldlands,9,22;Crimson Mirelands,19,22;Cobalt Coastlands,26,15;Coronet Highlands,16,13;Alabaster Icelands,16,5;Temple of Sinnoh,16,10;Prelude Beach,4,19', 'paldea':'Cabo Poco,17,28;Los Platos,17,24;Mesagoza,17,21;Cortondo,11,22;Artazon,24,22;Levincia,28,15;Cascarrafa,9,16;Porto Marinada,4,11;Medali,12,10;Montenevera,20,6;Alfornada,5,27;Glaseado Mountain,21,9;Casseroya Lake,9,6;Area Zero,17,15;Pokemon League,14,19', 'orre':'Phenac City,21,21;Pyrite Town,12,20;Agate Village,11,6;Mt Battle,22,4;Realgam Tower,19,15;The Under,10,23;Outskirt Stand,26,25;Snagem Hideout,27,15;Cipher Lab,27,8;Gateon Port,5,15;Pokemon HQ Lab,5,22;Rock Poke Spot,16,18;Oasis Poke Spot,23,11;Cave Poke Spot,16,10;Citadark Isle,4,28', 'kitakami':'Mossui Town,16,23;Loyalty Plaza,11,24;Apple Hills,9,21;Kitakami Hall,22,17;Oni Mountain,16,10;Crystal Pool,16,9;Timeless Woods,26,6;Fellhorn Gorge,26,14;Paradise Barrens,6,8;Revelers Road,20,21', 'blueberry':'Central Plaza,16,15;Savanna Biome,8,22;Coastal Biome,24,22;Canyon Biome,8,8;Polar Biome,24,8' } for region,raw in landmarks.items(): ns=[] for point in raw.split(';'): name,x,y=point.split(',');n=node(slug(name),int(x),int(y),region);n['name']=name.replace('Pokemon','Pokémon');ns.append(n) regions[region]={'name':{'blueberry':'Blueberry Academy'}.get(region,region.title()),'width':32,'height':30,'nodes':ns,'source':f'https://bulbapedia.bulbagarden.net/wiki/{region.title() if region!="blueberry" else "Terarium"}','precision':'Schematic landmark overview; use the location list for routes'} # Multiple blocks can describe one route or cave; retain one selectable marker. for region in regions.values(): merged={} for point in region['nodes']: if point['slug'] in merged: merged[point['slug']]['cells'].extend(point['cells']) else: merged[point['slug']]=point region['nodes']=list(merged.values()) Path('data/pokemon-atlas.json').write_text(json.dumps({'source':'Coordinates adapted from pret decompilations; later-region landmark overviews are authored schematics. No game or wiki prose is reproduced.','regions':regions},ensure_ascii=False,indent=2)+'\n') print({k:len(v['nodes']) for k,v in regions.items()})