JA
games/open-lemmings / tools / site / build.py · commit 04e2e2a
#!/usr/bin/env python3"""Writes the game's web site (site/, one page per language) from the game's own texts and screenshots:tools/site/shots.sh && python3 tools/site/build.py"""import html, json, os, shutil, sysfrom PIL import ImageHERE = os.path.dirname(os.path.abspath(__file__))GAME = os.path.join(HERE, '..', '..')sys.path.insert(0, os.path.join(HERE, '..'))from lang import KEYS, T, DESCSITE = os.path.join(GAME, 'site')SHOTS = os.path.join(GAME, 'var', 'screenshots', 'site')MAIN = 'en'OG = {'en': 'en_US', 'fr': 'fr_FR', 'es': 'es_ES', 'de': 'de_DE', 'it': 'it_IT', 'pt': 'pt_BR', 'nl': 'nl_NL', 'pl': 'pl_PL', 'ru': 'ru_RU', 'uk': 'uk_UA',      'tr': 'tr_TR', 'ja': 'ja_JP', 'zh': 'zh_CN', 'ko': 'ko_KR'}# [page title, genre, how to play, the 8 skills, worlds heading, controls, features, free line]X = { 'en': ['Lumikins – puzzle game, play online free', 'Puzzle game', 'How to play', 'The 8 skills', '24 levels, 11 worlds',        'Mouse or finger: pick a skill, then a Lumikin. Drag or arrow keys to scroll. Keys 1–8: skills, F: fast, P: pause.',        'HD or retro graphics · original music · 14 languages · phone, tablet and computer · installable, plays offline', 'Free, in your browser, nothing to install.'], 'fr': ['Lumikins – jeu de réflexion, jouer en ligne gratuitement', 'Jeu de réflexion', 'Comment jouer', 'Les 8 compétences', '24 niveaux, 11 mondes',        'Souris ou doigt : choisissez une compétence, puis un Lumikin. Glissez ou utilisez les flèches pour défiler. Touches 1–8 : compétences, F : accéléré, P : pause.',        'Graphismes HD ou rétro · musique originale · 14 langues · téléphone, tablette et ordinateur · installable, jouable hors ligne', 'Gratuit, dans votre navigateur, rien à installer.'], 'es': ['Lumikins – juego de puzles, jugar online gratis', 'Juego de puzles', 'Cómo jugar', 'Las 8 habilidades', '24 niveles, 11 mundos',        'Ratón o dedo: elige una habilidad y luego un Lumikin. Arrastra o usa las flechas para desplazarte. Teclas 1–8: habilidades, F: rápido, P: pausa.',        'Gráficos HD o retro · música original · 14 idiomas · móvil, tableta y ordenador · instalable, funciona sin conexión', 'Gratis, en tu navegador, sin instalar nada.'], 'de': ['Lumikins – Rätselspiel, kostenlos online spielen', 'Rätselspiel', 'So wird gespielt', 'Die 8 Fähigkeiten', '24 Level, 11 Welten',        'Maus oder Finger: Fähigkeit wählen, dann einen Lumikin. Ziehen oder Pfeiltasten zum Scrollen. Tasten 1–8: Fähigkeiten, F: schnell, P: Pause.',        'HD- oder Retro-Grafik · eigene Musik · 14 Sprachen · Handy, Tablet und Computer · installierbar, offline spielbar', 'Kostenlos im Browser, ohne Installation.'], 'it': ['Lumikins – gioco di logica, gioca online gratis', 'Gioco di logica', 'Come si gioca', 'Le 8 abilità', '24 livelli, 11 mondi',        'Mouse o dito: scegli un\'abilità, poi un Lumikin. Trascina o usa le frecce per scorrere. Tasti 1–8: abilità, F: veloce, P: pausa.',        'Grafica HD o retrò · musica originale · 14 lingue · telefono, tablet e computer · installabile, giocabile offline', 'Gratis, nel browser, niente da installare.'], 'pt': ['Lumikins – jogo de quebra-cabeça, jogue online grátis', 'Jogo de quebra-cabeça', 'Como jogar', 'As 8 habilidades', '24 níveis, 11 mundos',        'Mouse ou dedo: escolha uma habilidade e depois um Lumikin. Arraste ou use as setas para rolar. Teclas 1–8: habilidades, F: rápido, P: pausa.',        'Gráficos HD ou retrô · música original · 14 idiomas · celular, tablet e computador · instalável, funciona offline', 'Grátis, no navegador, sem instalar nada.'], 'nl': ['Lumikins – puzzelspel, gratis online spelen', 'Puzzelspel', 'Zo speel je', 'De 8 vaardigheden', '24 levels, 11 werelden',        'Muis of vinger: kies een vaardigheid en dan een Lumikin. Sleep of gebruik de pijltjes om te scrollen. Toetsen 1–8: vaardigheden, F: snel, P: pauze.',        'HD- of retrobeeld · eigen muziek · 14 talen · telefoon, tablet en computer · installeerbaar, offline speelbaar', 'Gratis in je browser, niets te installeren.'], 'pl': ['Lumikins – gra logiczna, graj online za darmo', 'Gra logiczna', 'Jak grać', '8 umiejętności', '24 poziomy, 11 światów',        'Mysz lub palec: wybierz umiejętność, potem Lumikina. Przeciągnij lub użyj strzałek, by przewijać. Klawisze 1–8: umiejętności, F: szybko, P: pauza.',        'Grafika HD lub retro · oryginalna muzyka · 14 języków · telefon, tablet i komputer · do zainstalowania, działa offline', 'Za darmo, w przeglądarce, bez instalacji.'], 'ru': ['Lumikins – головоломка, играть онлайн бесплатно', 'Головоломка', 'Как играть', '8 умений', '24 уровня, 11 миров',        'Мышь или палец: выберите умение, затем Люмикина. Тяните или жмите стрелки для прокрутки. Клавиши 1–8: умения, F: быстро, P: пауза.',        'Графика HD или ретро · своя музыка · 14 языков · телефон, планшет и компьютер · можно установить, работает офлайн', 'Бесплатно, в браузере, без установки.'], 'uk': ['Lumikins – головоломка, грати онлайн безкоштовно', 'Головоломка', 'Як грати', '8 умінь', '24 рівні, 11 світів',        'Миша або палець: оберіть уміння, потім Люмікіна. Тягніть або тисніть стрілки для прокрутки. Клавіші 1–8: уміння, F: швидко, P: пауза.',        'Графіка HD або ретро · власна музика · 14 мов · телефон, планшет і комп\'ютер · можна встановити, працює офлайн', 'Безкоштовно, у браузері, без встановлення.'], 'tr': ['Lumikins – bulmaca oyunu, ücretsiz çevrimiçi oyna', 'Bulmaca oyunu', 'Nasıl oynanır', '8 yetenek', '24 bölüm, 11 dünya',        'Fare ya da parmak: bir yetenek, sonra bir Lumikin seç. Kaydırmak için sürükle ya da ok tuşlarını kullan. 1–8: yetenekler, F: hızlı, P: duraklat.',        'HD ya da retro grafik · özgün müzik · 14 dil · telefon, tablet ve bilgisayar · yüklenebilir, çevrimdışı oynanır', 'Ücretsiz, tarayıcında, kurulum yok.'], 'ja': ['Lumikins – パズルゲーム、無料でオンラインプレイ', 'パズルゲーム', 'あそびかた', '8つのスキル', '24ステージ、11の世界',        'マウスか指でスキルを選び、ルミキンをタップ。ドラッグか矢印キーでスクロール。1–8: スキル、F: 早送り、P: ポーズ。',        'HD / レトロ画面 · オリジナル音楽 · 14言語 · スマホ、タブレット、PC · インストール可、オフライン対応', '無料、ブラウザですぐ遊べます。'], 'zh': ['Lumikins – 益智游戏, 免费在线玩', '益智游戏', '玩法', '8 种技能', '24 个关卡, 11 个世界',        '用鼠标或手指选择技能, 再点一个小灯灵。拖动或用方向键滚动。1–8: 技能, F: 加速, P: 暂停。',        '高清或复古画面 · 原创音乐 · 14 种语言 · 手机、平板和电脑 · 可安装, 可离线游玩', '免费, 打开浏览器就能玩。'], 'ko': ['Lumikins – 퍼즐 게임, 무료 온라인 플레이', '퍼즐 게임', '플레이 방법', '8가지 기술', '24개 레벨, 11개 세계',        '마우스나 손가락으로 기술을 고르고 루미킨을 누르세요. 드래그하거나 방향키로 스크롤. 1–8: 기술, F: 빠르게, P: 일시정지.',        'HD 또는 레트로 그래픽 · 오리지널 음악 · 14개 언어 · 휴대폰, 태블릿, 컴퓨터 · 설치 가능, 오프라인 플레이', '무료, 브라우저에서 바로.'],}SKILLS = ['climb', 'glide', 'boom', 'stop', 'bridge', 'bash', 'mine', 'dig']WORLDS = [('forest', 'MIND THE GAP'), ('cave', 'DEEP DOWN'), ('forge', 'LAVA WORKS'), ('night', 'THE LONG WAY HOME')]CSS = """:root{--bg:#0c0818;--card:#1c1438;--rim:#3c2c6c;--gold:#ffd23c;--ink:#f4efff;--dim:#b0a6d6}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--ink);font:17px/1.6 system-ui,-apple-system,"Segoe UI",Roboto,"Noto Sans",sans-serif;overflow-x:hidden}a{color:var(--gold)}img{max-width:100%;height:auto;display:block}.wrap{max-width:1080px;margin:0 auto;padding:0 16px}.hero{position:relative;text-align:center;padding:28px 0 56px;background:#161a44 url(IMG/bg.jpg) center bottom/cover no-repeat}.hero:after{content:"";position:absolute;inset:auto 0 0 0;height:120px;background:linear-gradient(transparent,var(--bg))}.hero>*{position:relative;z-index:1}.logo{width:min(560px,88%);max-width:100%;margin:0 auto;filter:drop-shadow(0 0 28px #ffaa3c88)}h1{font-size:clamp(1.3rem,4vw,2rem);margin:.2em 0 .1em;font-weight:800;text-shadow:0 2px 0 #1c1230}.sub{color:var(--dim);margin:0 auto 22px;max-width:640px}.play{display:inline-block;padding:16px 46px;border-radius:14px;font-size:1.35rem;font-weight:800;color:#3c1404;text-decoration:none; background:linear-gradient(#ffe08a,#ffb347 45%,#e0641c);box-shadow:0 6px 0 #8c2a12,0 0 40px #ffb34799;transition:transform .12s}.play:hover{transform:translateY(-2px) scale(1.03)}.play:active{transform:translateY(4px);box-shadow:0 2px 0 #8c2a12}.shot{margin:34px auto 0;width:100%;max-width:min(880px,100%);border-radius:14px;border:2px solid var(--rim);box-shadow:0 20px 60px #000a,0 0 60px #7c64b044;overflow:hidden}.walk{display:flex;justify-content:center;gap:min(10px,2vw);margin-top:22px}.walk img{width:min(48px,11vw);height:auto;image-rendering:pixelated;animation:bob .7s infinite alternate ease-in-out}.walk img:nth-child(2n){animation-delay:.25s}.walk img:nth-child(3n){animation-delay:.45s}@keyframes bob{to{transform:translateY(-7px)}}h2{font-size:clamp(1.4rem,3.6vw,2rem);text-align:center;margin:64px 0 22px;color:var(--gold)}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(230px,1fr))}.card{background:var(--card);border:1px solid var(--rim);border-radius:14px;padding:14px 16px;display:flex;gap:12px;align-items:center}.card img{width:36px;height:36px;image-rendering:pixelated;flex:none}.steps{counter-reset:s;display:grid;gap:12px;max-width:760px;margin:0 auto;padding:0;list-style:none}.steps li{background:var(--card);border-left:4px solid var(--gold);border-radius:10px;padding:12px 16px 12px 54px;position:relative}.steps li:before{counter-increment:s;content:counter(s);position:absolute;left:16px;top:10px;font-weight:800;font-size:1.3rem;color:var(--gold)}.worlds{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(230px,1fr))}.worlds figure{margin:0;border-radius:14px;overflow:hidden;border:1px solid var(--rim);background:var(--card)}.worlds figcaption{padding:10px 14px;font-weight:700}.note{text-align:center;color:var(--dim);max-width:760px;margin:22px auto}.cta{text-align:center;margin:56px 0 30px}.langs{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;padding:0;margin:26px 0;list-style:none;font-size:.9rem}.langs a{display:block;padding:4px 10px;border:1px solid var(--rim);border-radius:99px;text-decoration:none;color:var(--dim)}.langs a[aria-current]{color:var(--bg);background:var(--gold);border-color:var(--gold)}footer{text-align:center;color:var(--dim);font-size:.9rem;padding:10px 16px 50px}"""def e(s):    return html.escape(s, quote=True)def page(lang):    t = dict(zip(KEYS, T[lang]))    title, genre, how, skills_h, worlds_h, controls, features, free = X[lang]    up = '' if lang == MAIN else '../'    img = up + 'images/'    url = 'https://{{DOMAIN}}/' + ('' if lang == MAIN else lang + '/')    play = up + 'play/' + ('' if lang == MAIN else '?back=../' + lang + '/')    alts = ''.join(f'<link rel="alternate" hreflang="{c}" href="https://{{{{DOMAIN}}}}/{"" if c == MAIN else c + "/"}">\n' for c in T)    alts += '<link rel="alternate" hreflang="x-default" href="https://{{DOMAIN}}/">\n'    ld = {'@context': 'https://schema.org', '@type': 'VideoGame', 'name': 'Lumikins', 'description': DESC[lang], 'genre': genre, 'inLanguage': lang,          'gamePlatform': ['Web browser', 'Android', 'iOS', 'Windows', 'Linux', 'macOS'], 'applicationCategory': 'Game', 'isAccessibleForFree': True,          'image': 'https://{{DOMAIN}}/images/og.png', 'screenshot': [f'https://{{{{DOMAIN}}}}/images/shots/{w}.jpg' for w, _ in WORLDS], 'url': url,          'offers': {'@type': 'Offer', 'price': '0', 'priceCurrency': 'EUR'},          'potentialAction': {'@type': 'PlayAction', 'target': 'https://{{DOMAIN}}/play/'}}    langs = ''.join(f'<li><a href="{up}{"" if c == MAIN else c + "/"}" hreflang="{c}" lang="{c}"{" aria-current=\"page\"" if c == lang else ""}>{e(dict(zip(KEYS, T[c]))["LANGNAME"])}</a></li>' for c in T)    skills = ''.join(f'<div class="card"><img src="{img}skill-{s}.png" alt="" width="36" height="36"><span>{e(t["SKILL " + s])}</span></div>' for s in SKILLS)    steps = ''.join(f'<li>{e(t[k])}</li>' for k in ('HINT DIG', 'HINT BRIDGE', 'HINT GLIDE', 'HINT BOOM'))    worlds = ''.join(f'<figure><img src="{img}shots/{w}.jpg" alt="Lumikins – {e(t[name])}" width="960" height="540" loading="lazy"><figcaption>{e(t[name])}</figcaption></figure>' for w, name in WORLDS)    walkers = ''.join(f'<img src="{img}lumi.png" alt="" width="48" height="48">' for _ in range(7))    return f"""<!doctype html><html lang="{lang}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{e(title)}</title><meta name="description" content="{e(DESC[lang])}"><link rel="canonical" href="{url}">{alts}<meta property="og:type" content="website"><meta property="og:title" content="{e(title)}"><meta property="og:description" content="{e(DESC[lang])}"><meta property="og:url" content="{url}"><meta property="og:image" content="https://{{{{DOMAIN}}}}/images/og.png"><meta property="og:locale" content="{OG[lang]}"><meta name="twitter:card" content="summary_large_image"><meta name="theme-color" content="#1c1438"><link rel="icon" type="image/png" href="{img}favicon.png"><style>{CSS.replace('IMG/', img)}</style><script type="application/ld+json">{json.dumps(ld, ensure_ascii=False)}</script></head><body><header class="hero"><div class="wrap"><img class="logo" src="{img}logo.png" alt="Lumikins" width="1260" height="444"><h1>{e(t['TAGLINE'])}</h1><p class="sub">{e(DESC[lang])}</p><a class="play" href="{play}">▶ {e(t['PLAY'])}</a><div class="walk" aria-hidden="true">{walkers}</div><img class="shot" src="{img}shots/title.jpg" alt="Lumikins – {e(t['TAGLINE'])}" width="960" height="540"></div></header><main class="wrap"><h2>{e(how)}</h2><ol class="steps">{steps}</ol><p class="note">{e(controls)}</p><h2>{e(skills_h)}</h2><div class="grid">{skills}</div><h2>{e(worlds_h)}</h2><div class="worlds">{worlds}</div><p class="note">{e(features)}</p><div class="cta"><a class="play" href="{play}">▶ {e(t['PLAY'])}</a><p class="note">{e(free)}</p></div><nav aria-label="Languages"><ul class="langs">{langs}</ul></nav></main><footer>Lumikins · {e(t['CREDITS'])} · <a href="https://jumphp.net">jumphp.net</a></footer></body></html>"""def images():    im = os.path.join(SITE, 'images')    os.makedirs(os.path.join(im, 'shots'), exist_ok=True)    for name in ('title', 'forest', 'cave', 'forge', 'night', 'boom', 'stairs', 'levels'):        Image.open(os.path.join(SHOTS, name + '.png')).convert('RGB').save(os.path.join(im, 'shots', name + '.jpg'), quality=88)    a = os.path.join(GAME, 'assets')    shutil.copy(os.path.join(a, 'hd', 'sprites', 'logo.png'), os.path.join(im, 'logo.png'))    Image.open(os.path.join(a, 'sprites', 'lumi.png')).crop((0, 0, 16, 16)).resize((96, 96), Image.NEAREST).save(os.path.join(im, 'lumi.png'))    Image.open(os.path.join(a, 'icon.png')).resize((64, 64), Image.NEAREST).save(os.path.join(im, 'favicon.png'))    names = json.load(open(os.path.join(a, 'icons.json')))    sheet = Image.open(os.path.join(a, 'sprites', 'icons-gold.png'))    for s in SKILLS:        i = names.index(s)        sheet.crop((i * 16, 0, i * 16 + 16, 16)).resize((72, 72), Image.NEAREST).save(os.path.join(im, f'skill-{s}.png'))    sky = Image.open(os.path.join(a, 'hd', 'bg', 'forest-sky.jpg')).convert('RGBA')    sky.alpha_composite(Image.open(os.path.join(a, 'hd', 'bg', 'forest-far.png')).crop((0, 0, 1440, 696)))    sky.convert('RGB').save(os.path.join(im, 'bg.jpg'), quality=85)    # og.png 1200 x 630 and the directory's thumbnail (16:9): a screenshot under the logo    shot = Image.open(os.path.join(SHOTS, 'forest.png')).convert('RGBA')    logo = Image.open(os.path.join(a, 'hd', 'sprites', 'logo.png'))    og = shot.resize((1200, 675), Image.LANCZOS).crop((0, 30, 1200, 660))    og.alpha_composite(Image.new('RGBA', og.size, (12, 8, 24, 70)))    lg = logo.resize((900, int(900 * logo.height / logo.width)), Image.LANCZOS)    og.alpha_composite(lg, (150, 20))    og.convert('RGB').save(os.path.join(im, 'og.png'))    th = shot.resize((1280, 720), Image.LANCZOS)    lg = logo.resize((1000, int(1000 * logo.height / logo.width)), Image.LANCZOS)    th.alpha_composite(lg, (140, 30))    th.convert('RGB').save(os.path.join(a, 'thumbnail.png'))if __name__ == '__main__':    images()    urls = ''    for lang in T:        d = SITE if lang == MAIN else os.path.join(SITE, lang)        os.makedirs(d, exist_ok=True)        open(os.path.join(d, 'index.html'), 'w', encoding='utf-8').write(page(lang))        loc = 'https://{{DOMAIN}}/' + ('' if lang == MAIN else lang + '/')        links = ''.join(f'<xhtml:link rel="alternate" hreflang="{c}" href="https://{{{{DOMAIN}}}}/{"" if c == MAIN else c + "/"}"/>' for c in T)        urls += f'<url><loc>{loc}</loc>{links}</url>\n'    open(os.path.join(SITE, 'sitemap.xml'), 'w').write('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" '                                                     'xmlns:xhtml="http://www.w3.org/1999/xhtml">\n' + urls + '</urlset>\n')    open(os.path.join(SITE, 'robots.txt'), 'w').write('User-agent: *\nAllow: /\nSitemap: https://{{DOMAIN}}/sitemap.xml\n')    print('site written:', len(T), 'languages')