JA
games/doom-jumphp / tools / site / pages.py · commit 8e1cb9f
"""Writes the game's web site pages: site/index.html (French) and site/en/index.html (English).   python3 games/doom-jumphp/tools/site/pages.py"""import json, osGAME = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))SITE = os.path.join(GAME, 'site')T = { 'fr': {  'title': 'IRON ABYSS – FPS rétro en PHP, jouer gratuitement en ligne',  'desc': "IRON ABYSS : un FPS rétro des années 90 écrit en PHP, jouable gratuitement dans le navigateur. Moteur de rendu logiciel BSP porté du code source GPL de 1993, monstres, armes et niveaux originaux.",  'tag': 'Un FPS rétro écrit en PHP. Il tourne dans votre navigateur.',  'play': 'JOUER MAINTENANT', 'free': 'Gratuit, sans installation, clavier ou manette.',  'h_shots': 'Captures', 'h_what': 'Le jeu', 'h_engine': 'Le moteur', 'h_bestiary': 'Bestiaire et arsenal',  'h_levels': 'Les niveaux', 'h_controls': 'Commandes', 'h_faq': 'Questions', 'h_credits': 'Crédits',  'what': [   "Station minière 7. Les machines ont creusé trop profond, et ce qu'elles ont réveillé a pris le contrôle des droïdes.",   "Vous traversez trois niveaux : HANGAR 7, PROCESSING PLANT et THE CORE. Il faut trouver les cartes d'accès, actionner les ascenseurs, éviter l'acide et la lave, dénicher les secrets et abattre le Warden qui garde le cœur.",  ],  'engine': [   "Le moteur est un portage en PHP du rendu logiciel et de la logique de jeu du moteur de 1993, dont le code C a été publié sous licence GNU GPL. On y retrouve le parcours d'un arbre BSP, les murs dessinés colonne par colonne, les sols et plafonds en « visplanes », la lumière par tables de couleurs, les sprites à 8 angles, les 35 tics par seconde et l'IA des monstres.",   "Le même code PHP tourne sur le bureau et dans le navigateur, compilé en WebAssembly avec JumPHP. Une image de 320 × 168 pixels est calculée en PHP à chaque frame, puis envoyée à l'écran par WebGL.",   "Tout le contenu est original et généré par du code : les textures, le ciel, les monstres modélisés en 3D puis photographiés sous 8 angles, les armes et les sons.",  ],  'beasts': [   ('Husk', "un droïde de forage détourné qui tire des rafales."), ('Gnasher', "une bête rapide qui mord au corps à corps."),   ('Spitter', "griffes de près, boules d'acide de loin."), ('Warden', "le gardien blindé du cœur, avec son canon à plasma."),  ],  'weapons': "Clé à molette, pistolet, fusil à pompe, riveteuse et lanceur ionique.",  'levels': [('HANGAR 7', "hangar, cour à ciel ouvert, salle des ordinateurs, carte rouge."),             ('PROCESSING PLANT', "canaux d'acide, entrepôt, cour avec escalier, cartes bleue et jaune."),             ('THE CORE', "caverne de lave, antichambre, arène du Warden.")],  'controls': [('Flèches / W S (Z S)', 'avancer, reculer, tourner'), ('A D (Q D)', 'pas de côté'), ('Maj', 'courir'),               ('Ctrl / F / clic', 'tirer'), ('Espace / E', 'ouvrir, actionner'), ('1 à 5', 'armes'), ('Tab', 'carte'),               ('Échap', 'menu'), ('Manette', 'stick, RT tirer, A utiliser'), ('Téléphone', 'stick tactile et boutons FIRE / USE')],  'faq': [   ("Est-ce gratuit ?", "Oui. Le jeu se lance dans le navigateur, sans installation ni compte."),   ("Est-ce le jeu de 1993 ?", "Non. Seul le moteur est un portage en PHP du code source publié sous GPL. Le nom, les monstres, les armes, les niveaux, les graphismes et les sons sont originaux, et aucun fichier du jeu d'origine n'est utilisé."),   ("Pourquoi en PHP ?", "Pour montrer qu'un langage qu'on associe au web peut faire tourner un vrai moteur 3D logiciel, à 60 images par seconde dans le navigateur."),   ("Où est le code ?", "Le code est libre, sous licence GNU GPL v2. Les sources sont consultables sur la page du projet."),  ],  'credits': "Moteur porté en PHP à partir du code source du moteur de 1993 publié par id Software sous licence GNU GPL. Framework JumPHP. Contenu original.",  'langname': 'Français', 'other': 'English', }, 'en': {  'title': 'IRON ABYSS – retro FPS in PHP, play free online',  'desc': "IRON ABYSS: a 90s retro first-person shooter written in PHP, free to play in your browser. A software BSP renderer ported from the 1993 GPL source, original monsters, weapons and levels.",  'tag': 'A retro FPS written in PHP. It runs in your browser.',  'play': 'PLAY NOW', 'free': 'Free, nothing to install, keyboard or gamepad.',  'h_shots': 'Screenshots', 'h_what': 'The game', 'h_engine': 'The engine', 'h_bestiary': 'Monsters and weapons',  'h_levels': 'Levels', 'h_controls': 'Controls', 'h_faq': 'Questions', 'h_credits': 'Credits',  'what': [   "Mining station 7. The machines dug too deep, and whatever they woke up has taken over the droids.",   "You fight through three levels: HANGAR 7, PROCESSING PLANT and THE CORE. Find the keycards, ride the lifts, avoid the acid and the lava, find the secrets and take down the Warden that guards the core.",  ],  'engine': [   "The engine is a PHP port of the software renderer and game logic of the 1993 engine, whose C source was released under the GNU GPL. It has the BSP walk, walls drawn column by column, floors and ceilings as visplanes, colormap lighting, 8-angle sprites, 35 tics a second and the monster AI.",   "The same PHP code runs on the desktop and in the browser, compiled to WebAssembly with JumPHP. Every frame, PHP computes a 320 × 168 picture, then WebGL puts it on the screen.",   "All the content is original and made by code: the textures, the sky, the monsters (modelled in 3D, then photographed from 8 angles), the weapons and the sounds.",  ],  'beasts': [   ('Husk', "a hijacked drilling droid that fires bursts."), ('Gnasher', "a fast beast that bites up close."),   ('Spitter', "claws up close, acid balls from afar."), ('Warden', "the armoured guardian of the core, with a plasma cannon."),  ],  'weapons': "Wrench, sidearm, scattergun, riveter and ion launcher.",  'levels': [('HANGAR 7', "hangar, open-air courtyard, computer room, red keycard."),             ('PROCESSING PLANT', "acid channels, storage, a yard with stairs, blue and yellow keycards."),             ('THE CORE', "lava cavern, antechamber, the Warden's arena.")],  'controls': [('Arrows / W S', 'move, turn'), ('A D', 'strafe'), ('Shift', 'run'), ('Ctrl / F / click', 'fire'),               ('Space / E', 'open, use'), ('1 to 5', 'weapons'), ('Tab', 'map'), ('Escape', 'menu'),               ('Gamepad', 'stick, RT fire, A use'), ('Phone', 'touch stick and FIRE / USE buttons')],  'faq': [   ("Is it free?", "Yes. It runs in your browser, with nothing to install and no account."),   ("Is it the 1993 game?", "No. Only the engine is a PHP port of the source code released under the GPL. The name, the monsters, the weapons, the levels, the graphics and the sounds are original, and no file of the original game is used."),   ("Why PHP?", "To show that a language people think of as a web language can run a real software 3D engine, at 60 frames a second in the browser."),   ("Where is the code?", "The code is free software, under the GNU GPL v2. You can browse the sources on the project page."),  ],  'credits': "Engine ported to PHP from the source code of the 1993 engine released by id Software under the GNU GPL. JumPHP framework. Original content.",  'langname': 'English', 'other': 'Français', },}SHOTS = [('hall', 'HANGAR 7'), ('courtyard', 'HANGAR 7'), ('fight', 'HANGAR 7'), ('plant', 'PROCESSING PLANT'),         ('yard', 'PROCESSING PLANT'), ('cavern', 'THE CORE'), ('warden', 'THE CORE')]CSS = """*{box-sizing:border-box}html{background:#0b0606;color:#e8dccf;font:16px/1.6 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}body{margin:0}a{color:#ffb347}img{image-rendering:pixelated;display:block;max-width:100%;height:auto}.wrap{max-width:1040px;margin:0 auto;padding:0 16px}header.hero{position:relative;background:#000 url(IMG/hero.png) center/cover no-repeat;border-bottom:4px solid #6b1a10}header.hero:after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,.15),rgba(11,6,6,.95))}.hero .wrap{position:relative;z-index:1;padding:200px 16px 40px;text-align:center}h1{margin:0;font-size:56px;letter-spacing:4px;color:#ffd166;text-shadow:3px 3px 0 #7a1a0c,6px 6px 0 #000}.tag{font-size:20px;margin:8px 0 24px;color:#fff}.play{display:inline-block;background:#c0391f;color:#fff;font-weight:800;font-size:22px;letter-spacing:2px;text-decoration:none; padding:16px 40px;border:3px solid #ffd166;box-shadow:0 6px 0 #5e140a;border-radius:4px}.play:hover{background:#e04a2a}.small{color:#bfae9c;font-size:14px;margin-top:12px}nav.lang{position:absolute;top:12px;right:16px;z-index:2;font-size:14px}nav.lang a{margin-left:10px}section{padding:36px 0;border-bottom:1px solid #2a1612}h2{color:#ffb347;font-size:28px;margin:0 0 16px;letter-spacing:1px}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px}figure{margin:0;background:#140b09;border:2px solid #3a1d16}figcaption{font-size:13px;padding:4px 8px;color:#bfae9c}.cols{display:grid;grid-template-columns:1fr 1fr;gap:24px}@media(max-width:700px){.cols{grid-template-columns:1fr}h1{font-size:36px}.hero .wrap{padding-top:120px}}ul.list{padding-left:20px}table{border-collapse:collapse;width:100%}td{padding:6px 8px;border-bottom:1px solid #2a1612;vertical-align:top}td:first-child{color:#ffd166;white-space:nowrap;font-weight:700}dt{font-weight:700;color:#ffd166;margin-top:12px}dd{margin:4px 0 0}footer{padding:28px 0;color:#9b8a78;font-size:14px;text-align:center}"""def esc(s):    return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')def page(lang):    t = T[lang]    sub = lang != 'fr'    img = '../images' if sub else 'images'    play = '../play/' if sub else 'play/'    url = 'https://{{DOMAIN}}/' + ('en/' if sub else '')    other = '../' if sub else 'en/'    ld = {        '@context': 'https://schema.org', '@type': 'VideoGame', 'name': 'IRON ABYSS', 'description': t['desc'],        'genre': ['First-person shooter', 'Retro'], 'gamePlatform': ['Web browser', 'Linux', 'Windows', 'macOS'],        'applicationCategory': 'Game', 'operatingSystem': 'Any', 'inLanguage': lang, 'isAccessibleForFree': True,        'image': 'https://{{DOMAIN}}/images/og.png',        'screenshot': ['https://{{DOMAIN}}/images/%s.png' % s for s, _ in SHOTS],        'url': url, 'license': 'https://www.gnu.org/licenses/old-licenses/gpl-2.0.html',        'potentialAction': {'@type': 'PlayAction', 'target': 'https://{{DOMAIN}}/play/'},    }    faq = {'@context': 'https://schema.org', '@type': 'FAQPage', 'mainEntity': [        {'@type': 'Question', 'name': q, 'acceptedAnswer': {'@type': 'Answer', 'text': a}} for q, a in t['faq']]}    h = []    h.append('<!doctype html>\n<html lang="%s">\n<head>\n<meta charset="utf-8">' % lang)    h.append('<meta name="viewport" content="width=device-width,initial-scale=1">')    h.append('<title>%s</title>' % esc(t['title']))    h.append('<meta name="description" content="%s">' % esc(t['desc']))    h.append('<link rel="canonical" href="%s">' % url)    h.append('<link rel="alternate" hreflang="fr" href="https://{{DOMAIN}}/">')    h.append('<link rel="alternate" hreflang="en" href="https://{{DOMAIN}}/en/">')    h.append('<link rel="alternate" hreflang="x-default" href="https://{{DOMAIN}}/">')    h.append('<link rel="icon" type="image/png" href="%s/favicon.png">' % img)    for k, v in [('og:type', 'website'), ('og:title', t['title']), ('og:description', t['desc']), ('og:url', url),                 ('og:image', 'https://{{DOMAIN}}/images/og.png'), ('og:locale', 'fr_FR' if lang == 'fr' else 'en_US'),                 ('og:site_name', 'IRON ABYSS')]:        h.append('<meta property="%s" content="%s">' % (k, esc(v)))    for k, v in [('twitter:card', 'summary_large_image'), ('twitter:title', t['title']), ('twitter:description', t['desc']),                 ('twitter:image', 'https://{{DOMAIN}}/images/og.png')]:        h.append('<meta name="%s" content="%s">' % (k, esc(v)))    h.append('<script type="application/ld+json">%s</script>' % json.dumps(ld, ensure_ascii=False))    h.append('<script type="application/ld+json">%s</script>' % json.dumps(faq, ensure_ascii=False))    h.append('<style>%s</style>\n</head>\n<body>' % CSS.replace('IMG', img))    h.append('<header class="hero"><nav class="lang"><a href="%s" hreflang="%s">%s</a></nav><div class="wrap">' % (other, 'fr' if sub else 'en', t['other']))    h.append('<h1>IRON ABYSS</h1><p class="tag">%s</p>' % esc(t['tag']))    h.append('<a class="play" href="%s">&#9654; %s</a><p class="small">%s</p></div></header>' % (play, t['play'], esc(t['free'])))    h.append('<main class="wrap">')    h.append('<section><h2>%s</h2>%s</section>' % (t['h_what'], ''.join('<p>%s</p>' % esc(p) for p in t['what'])))    h.append('<section><h2>%s</h2><div class="grid">' % t['h_shots'])    for s, cap in SHOTS:        h.append('<figure><img src="%s/%s.png" width="960" height="600" loading="lazy" alt="IRON ABYSS – %s"><figcaption>%s</figcaption></figure>' % (img, s, esc(cap), esc(cap)))    h.append('</div></section>')    h.append('<section><h2>%s</h2>%s</section>' % (t['h_engine'], ''.join('<p>%s</p>' % esc(p) for p in t['engine'])))    h.append('<section class="cols"><div><h2>%s</h2><ul class="list">%s</ul><p>%s</p></div>' % (        t['h_bestiary'], ''.join('<li><strong>%s</strong> : %s</li>' % (n, esc(d)) if lang == 'fr' else '<li><strong>%s</strong>: %s</li>' % (n, esc(d)) for n, d in t['beasts']), esc(t['weapons'])))    h.append('<div><h2>%s</h2><ul class="list">%s</ul></div></section>' % (        t['h_levels'], ''.join('<li><strong>%s</strong> : %s</li>' % (n, esc(d)) if lang == 'fr' else '<li><strong>%s</strong>: %s</li>' % (n, esc(d)) for n, d in t['levels'])))    h.append('<section><h2>%s</h2><table>%s</table><p style="text-align:center;margin-top:24px"><a class="play" href="%s">&#9654; %s</a></p></section>' % (        t['h_controls'], ''.join('<tr><td>%s</td><td>%s</td></tr>' % (esc(k), esc(v)) for k, v in t['controls']), play, t['play']))    h.append('<section><h2>%s</h2><dl>%s</dl></section>' % (t['h_faq'], ''.join('<dt>%s</dt><dd>%s</dd>' % (esc(q), esc(a)) for q, a in t['faq'])))    h.append('<section><h2>%s</h2><p>%s</p></section>' % (t['h_credits'], esc(t['credits'])))    h.append('</main><footer>IRON ABYSS · GNU GPL v2 · <a href="%s">%s</a> · <a href="%s">%s</a></footer>' % (        '../' if sub else './', 'Français', 'en/' if not sub else './', 'English'))    h.append('</body>\n</html>\n')    return '\n'.join(h)with open(os.path.join(SITE, 'index.html'), 'w') as f:    f.write(page('fr'))os.makedirs(os.path.join(SITE, 'en'), exist_ok=True)with open(os.path.join(SITE, 'en', 'index.html'), 'w') as f:    f.write(page('en'))with open(os.path.join(SITE, 'sitemap.xml'), 'w') as f:    f.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')    for path in ['', 'en/']:        f.write('<url><loc>https://{{DOMAIN}}/%s</loc>' % path)        f.write('<xhtml:link rel="alternate" hreflang="fr" href="https://{{DOMAIN}}/"/><xhtml:link rel="alternate" hreflang="en" href="https://{{DOMAIN}}/en/"/>')        f.write('<xhtml:link rel="alternate" hreflang="x-default" href="https://{{DOMAIN}}/"/></url>\n')    f.write('</urlset>\n')print('pages ok')