Some checks failed
Test and deploy staging / staging (push) Failing after 5m29s
82 lines
5.9 KiB
Python
82 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Build image-space map markers and assemble the three SV in-game maps.
|
|
Requires Pillow and beautifulsoup4. Cached source pages/tiles: /tmp/game-map-art.
|
|
The map artwork is from the games; source image-map coordinates are adapted from
|
|
Serebii with attribution. Hand-placed landmarks are explicitly approximate.
|
|
"""
|
|
from pathlib import Path
|
|
import argparse
|
|
import json
|
|
import re
|
|
import urllib.request
|
|
from PIL import Image
|
|
from bs4 import BeautifulSoup
|
|
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
parser=argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--cache',type=Path,default=Path('/tmp/game-map-art'))
|
|
args=parser.parse_args();args.cache.mkdir(parents=True,exist_ok=True)
|
|
def fetch(url,name):
|
|
p=args.cache/name
|
|
if not p.exists():p.write_bytes(urllib.request.urlopen(url,timeout=45).read())
|
|
return p
|
|
|
|
def slug(s):return re.sub(r'[^a-z0-9]+','-',s.lower().replace('é','e')).strip('-')
|
|
original=json.loads((ROOT/'data/pokemon-atlas.json').read_text())['regions']
|
|
locations=json.loads((ROOT/'data/pokemon-locations.json').read_text())['locations']
|
|
def node(name,x,y,region):
|
|
key=slug(name);aliases={'mount-coronet':'mt-coronet','pokemon-league':'sinnoh-pokemon-league','lake-of-rage':'lake-of-rage','mt-battle':'mt-battle','pokemon-hq-lab':'pokemon-hq-lab'}
|
|
key=aliases.get(key,key)
|
|
found=next((p for p in locations if slug(p['name'])==slug(name) and (p['region'] or '').lower()==region),None)
|
|
if not found:found=next((p for p in locations if p['slug'] in [key,region+'-'+key] and (p['region'] or '').lower()==region),None)
|
|
return dict(slug=found['slug'] if found else key,name=found['name'] if found else name,x=round(x,3),y=round(y,3),kind='town' if re.search(r'Town|City|Village|Area|League',name) else 'route' if 'Route' in name else 'landmark',cells=[])
|
|
|
|
maps={}
|
|
for region in original:
|
|
p=ROOT/f'static/game-art/maps/{region}.webp'
|
|
if region in ['paldea','kitakami','blueberry']:
|
|
remote='terarium' if region=='blueberry' else region
|
|
canvas=Image.new('RGB',(2048,2048))
|
|
for x in range(8):
|
|
for y in range(8):
|
|
tile=fetch(f'https://www.serebii.net/pokearth/{remote}/map/tile_2-{x}-{y}.png',f'{remote}-tile-{x}-{y}.png')
|
|
canvas.paste(Image.open(tile),(x*256,y*256))
|
|
canvas.save(p,format='WEBP',quality=90,method=6)
|
|
im=Image.open(p);w,h=im.size
|
|
source='https://www.serebii.net/pokearth/'+region+'/'
|
|
if region=='blueberry':source='https://www.serebii.net/pokearth/terarium/'
|
|
if region=='orre':source='https://pokemonrevolution.net/forum/topic/147410-orre-region/'
|
|
nodes=[]
|
|
if region in ['sinnoh','hoenn','kanto','johto','unova','kalos']:
|
|
if region in ['kalos','unova']:page=fetch('https://www.serebii.net/pokearth/','pokearth.html')
|
|
else:page=fetch(source,region+'.html')
|
|
soup=BeautifulSoup(page.read_bytes(),'html.parser')
|
|
area_map=soup.find('map',attrs={'name':re.compile('^'+region+'$',re.I)})
|
|
if area_map:
|
|
for a in area_map.find_all('area'):
|
|
values=list(map(float,re.findall(r'[\d.]+',a.get('coords',''))))
|
|
if len(values)<4:continue
|
|
xs=values[::2];ys=values[1::2]
|
|
# Bounding-box centre remains inside rectangular routes and town markers.
|
|
x=(min(xs)+max(xs))/2;y=(min(ys)+max(ys))/2
|
|
name=a.get('title','').strip()
|
|
if name and 0<=x<=w and 0<=y<=h:nodes.append(node(name,x,y,region))
|
|
nodes=list({n['slug']:n for n in nodes}.values())
|
|
maps[region]=dict(name=original[region]['name'],image=f'/game-art/maps/{region}.webp',width=w,height=h,nodes=nodes,source=source,credit='Nintendo / The Pokémon Company; map imagery and location coordinates via Serebii' if region!='orre' else 'Nintendo / Genius Sonority; Pokémon XD screenshot via Pokémon Revolution forum',precision='Regional map · location markers aligned to the source image' if nodes else 'Game map · approximate landmark markers',pixelated=w<500)
|
|
|
|
# Reviewed landmark centres as percentages of the image, not the old schematic.
|
|
manual={
|
|
'paldea':'Cabo Poco,52,93;Los Platos,51,85;Mesagoza,49,78;Cortondo,31,77;Artazon,69,78;Levincia,83,58;Cascarrafa,30,58;Porto Marinada,14,48;Medali,35,40;Montenevera,51,27;Alfornada,15,89;Glaseado Mountain,55,31;Casseroya Lake,25,28;Area Zero,48,61;Pokemon League,45,72',
|
|
'kitakami':'Mossui Town,56,77;Loyalty Plaza,43,80;Apple Hills,38,75;Kitakami Hall,67,58;Oni Mountain,49,49;Crystal Pool,48,50;Timeless Woods,75,28;Fellhorn Gorge,81,46;Paradise Barrens,27,29;Revelers Road,64,70',
|
|
'blueberry':'Central Plaza,50,50;Savanna Biome,72,70;Coastal Biome,72,30;Canyon Biome,30,70;Polar Biome,30,30',
|
|
'hisui':'Jubilife Village,31,55;Obsidian Fieldlands,27,70;Crimson Mirelands,70,68;Cobalt Coastlands,86,35;Coronet Highlands,52,43;Alabaster Icelands,37.5,18;Temple of Sinnoh,36,10;Prelude Beach,26,61',
|
|
'galar':'Postwick,27.5,68.5;Wedgehurst,27.5,63.5;Wild Area,26.1,54.2;Motostoke,25.7,46.3;Turffield,12.4,40.9;Hulbury,38.5,40.8;Hammerlocke,26.2,34.5;Stow on Side,12.6,31.3;Ballonlea,10.4,24.3;Circhester,43.3,26.5;Spikemuth,47.4,34.7;Wyndon,26.2,9.4;Isle of Armor,76.9,35.2;Crown Tundra,26.1,84.5',
|
|
'alola':'Iki Town,14.5,40.4;Hauoli City,12,48.3;Melemele Sea,18.8,55;Heahea City,30.8,32;Paniola Town,34.3,25.8;Royal Avenue,37.8,32.5;Konikoni City,26.3,28.8;Malie City,91.3,62.9;Tapu Village,78.5,73.3;Po Town,79,55;Mount Lanakila,82.8,64.6;Seafolk Village,56.8,37.9;Poni Wilds,57.8,33.3;Vast Poni Canyon,67,19.6;Aether Paradise,38.3,61.7',
|
|
'orre':'Phenac City,73,61.2;Pyrite Town,56,44;Agate Village,39,30;Mt Battle,54,19.8;Realgam Tower,59,55.6;Outskirt Stand,87.8,61.7;Snagem Hideout,84,36;Cipher Lab,73.4,27.7;Gateon Port,12.8,31.9;Pokemon HQ Lab,19.5,19.6;Rock Poke Spot,86.7,48.5;Oasis Poke Spot,57.5,31.9;Cave Poke Spot,38.3,47;Citadark Isle,31,57.7;Orre Colosseum,22.6,46.5'
|
|
}
|
|
for region,places in manual.items():
|
|
m=maps[region]
|
|
m['nodes']=[node(name,float(x)*m['width']/100,float(y)*m['height']/100,region) for name,x,y in (p.split(',') for p in places.split(';'))]
|
|
(ROOT/'data/pokemon-map-art.json').write_text(json.dumps(maps,ensure_ascii=False,indent=2)+'\n')
|
|
print({r:len(m['nodes']) for r,m in maps.items()})
|