# app.py — Voxel Game Concept Generator • PRO (Bilingual, SAFE) import streamlit as st, pandas as pd, datetime, random from io import BytesIO from reportlab.lib.pagesizes import A4 from reportlab.pdfgen import canvas from reportlab.lib.units import cm APP_TITLE = "Voxel Game Concept Generator — PRO" VERSION = "2.2.1 (EN/FR)" GENRES = ["Extraction-Lite PvEvP (short sessions)","Bullet Heaven / Survivors-like","Prop Hunt (Asymmetric Hide & Seek)","Twin-Stick Arena (Waves/Boss Rush)","Boss Rush Co-op (3 vs AI)","Arcade Sport Arena (2v2 / 3v3)","Arena Brawler (Ability Draft)","RTS (Real-Time Strategy)","City Builder","4X / Grand Strategy (lite)","Action RPG (Top-down)","Souls-lite (Top-down)","Metroidvania (Top-down adaptation)","Open-World Sandbox (GTA-like Top-down)","Farming / Life Sim","Party Micro-Challenges (30–90s)","Wave Defense (Objective Control)","Platformer (Top-down hybrid challenges)","Beat'em up (Top-down lanes)","Rhythm / Music Action"] AGE_BRACKETS = ["7+ Family/Casual","10+ Pre-Teen/Teen","13+ Teen","16+ Mature-lite"] PUBLIC_DOMAIN_IP = ["Greek & Roman Myth (Heracles, Perseus, etc.)","Norse Myth (Valkyries, Loki, Jormungandr)","King Arthur & Knights of the Round Table","Robin Hood (medieval ballads/legends)","Beowulf (Old English epic)","Aesop's Fables (Tortoise & Hare, etc.)","Dracula (Bram Stoker, 1897)","Frankenstein (Mary Shelley, 1818)","Treasure Island (R. L. Stevenson, 1883)","The Count of Monte Cristo (A. Dumas, 1844)","The Three Musketeers (A. Dumas, 1844)","Journey to the Center of the Earth (Jules Verne, 1864)","20,000 Leagues Under the Seas (Jules Verne, 1870)","The War of the Worlds (H. G. Wells, 1898)","The Time Machine (H. G. Wells, 1895)","Moby-Dick (Herman Melville, 1851)","The Wizard of Oz (L. Frank Baum, 1900 — avoid 1939 film look)","Sherlock Holmes & Watson (stories; avoid modern screen versions)","Winnie-the-Pooh (A. A. Milne, 1926; avoid Disney styling)","The Jungle Book (R. Kipling, 1894)","Tarzan of the Apes (E. R. Burroughs, 1912 — trademarks exist)","Arabian Nights (Aladdin, Sinbad)","Journey to the West (Monkey King)","Grimm-style Folk Tales (older variants)","Little Red Riding Hood (older variants)","The Little Mermaid (H. C. Andersen, 1837)","H. P. Lovecraft core works (avoid third-party marks)"] STREAMER_MECHS = ["Clip-worthy chain reactions","Hype round timer (60–180s)","Creator polls that affect next round","Spectator cam / KillCam / Replay","Daily seeds & weekly mutators","Emote wheel & meme-ready VFX","Party codes (1–6 players)","Creator cosmetic codes"] MULTI_CHOICES = ["Solo","1 vs 1","2 vs 2","3 vs 3"] def now_ymd(): return datetime.datetime.now().strftime("%Y-%m-%d") def slugify(t): return "".join(c.lower() if c.isalnum() else "-" for c in t).strip("-") def auto_title(genre, multiplayer, ip): a = ["Vox","Pixel","Neon","Turbo","Echo","Quantum","Hyper","Nova","Shadow"]; b = ["Rush","Arena","Circuit","Outbreak","Gauntlet","Station","Mall","Trials","Heist","Frontier","Odyssey"] base = random.choice(a)+random.choice(b); tag="" if "Extraction" in genre: tag="Extract" elif "Survivors" in genre: tag="Survivors" elif "Prop Hunt" in genre: tag="Prop" elif "Boss Rush" in genre: tag="Boss" elif "Sport" in genre: tag="ArcSport" elif "Brawler" in genre: tag="Brawl" elif "RTS" in genre: tag="RTS" elif "City Builder" in genre: tag="City" elif "RPG" in genre: tag="ARPG" elif "Souls" in genre: tag="Souls" elif "Open-World" in genre: tag="Sandbox" elif "Farming" in genre: tag="Life" elif "Wave Defense" in genre: tag="Defense" elif "Beat'em" in genre: tag="Beat" elif "Rhythm" in genre: tag="Rhythm" if multiplayer!="Solo": tag+=f" {multiplayer.replace(' ','')}" if ip: tag+=f" • {ip.split('(')[0].strip().replace(' ','')}" return f"{base} — {tag}".strip(" —") def loops_table_en(genre): core = "Short, readable actions per 2–6s; aim, dodge, ability use" if any(k in genre for k in ["Arcade","Arena","Survivors"]) else "Plan, build, execute; resource/position trade-offs" meta = "Cosmetic unlocks, dailies/weekly seeds, badges, soft progression"; viral="Clip last 10s, creator polls, party codes, weekly mutators" return pd.DataFrame([["Core Loop",core],["Meta Loop",meta],["Viral Loop",viral]], columns=["Loop","Description"]) def loops_table_fr(genre): core = "Actions lisibles toutes les 2–6s : viser, esquiver, capacités" if any(k in genre for k in ["Arcade","Arena","Survivors"]) else "Planifier, construire, exécuter ; arbitrages ressources/position" meta = "Déblocages cosmétiques, défis quotidiens/hebdo, badges, progression douce"; viral="Clip des 10s, sondages créateurs, codes party, mutateurs hebdo" return pd.DataFrame([["Boucle cœur",core],["Boucle meta",meta],["Boucle virale",viral]], columns=["Boucle","Description"]) def pitch_and_scenario_en(genre, ip): pitches={"Extraction-Lite PvEvP (short sessions)":"Grab fast, extract faster — micro-squad heists where every second is content.","Bullet Heaven / Survivors-like":"A build-crafter where your synergies snowball into screen-melting chaos.","Prop Hunt (Asymmetric Hide & Seek)":"Outsmart scans and tells; laughter-first deception with clutch reveals.","Twin-Stick Arena (Waves/Boss Rush)":"Tight arenas, crisp kits, and boss phases tuned for clips.","Boss Rush Co-op (3 vs AI)":"Team pop-offs and clutch revives against learnable three-phase bosses.","Arcade Sport Arena (2v2 / 3v3)":"Pick, pass, score — highlight saves and buzzer-beaters every round.","RTS (Real-Time Strategy)":"Snappy macro with decisive micro — win the fight and the economy.","City Builder":"Readable vox-city growth where layout choices create dramatic stories.","4X / Grand Strategy (lite)":"Explore, expand, exploit, exterminate — compressed into punchy matches.","Action RPG (Top-down)":"Build a kit, chain abilities, and carve a path through themed arenas.","Souls-lite (Top-down)":"Pattern learning meets snappy resets; fair punishments, big triumphs.","Metroidvania (Top-down adaptation)":"Gated progression with compact arenas and clean ability reveals.","Open-World Sandbox (GTA-like Top-down)":"Toy-box chaos in digestible missions; short-form heists and stunts.","Farming / Life Sim":"Cozy loops with visible payoff and share-worthy milestones.","Party Micro-Challenges (30–90s)":"Zero-to-fun in seconds; slapstick physics and fast rematches.","Wave Defense (Objective Control)":"Hold points as hazards escalate; team combos make the clip.","Platformer (Top-down hybrid challenges)":"Readable routes, jump timing, ability swaps under pressure.","Beat'em up (Top-down lanes)":"Crowd control, cancels, and stylish juggles in compact arenas.","Rhythm / Music Action":"Pattern-driven highs where performance looks incredible on stream.","Arena Brawler (Ability Draft)":"Pick-ban mind games and wild ability synergies."} pitch=pitches.get(genre,"Fast, readable, shareable chaos tailored for creators.") scenario=f"Theme: {ip or 'Original setting'}. You lead a small cast through compact voxel locations. Sessions are short for watchability & replay. Each arena exposes clear goals, rising hazards, and instant feedback tuned for creator highlights." return pitch, scenario def pitch_and_scenario_fr(genre, ip): pitches={"Extraction-Lite PvEvP (short sessions)":"Attrape vite, extrais plus vite — braquages micro-escouade où chaque seconde est clipable.","Bullet Heaven / Survivors-like":"Un build-crafter où tes synergies déclenchent un chaos lisible à l'écran.","Prop Hunt (Asymmetric Hide & Seek)":"Duper les scans et les indices ; révélations hilarantes et clutches.","Twin-Stick Arena (Waves/Boss Rush)":"Arènes nerveuses, kits lisibles, phases de boss calibrées pour les clips.","Boss Rush Co-op (3 vs AI)":"Explosions d'équipe et revives clutch face à des boss apprenables.","Arcade Sport Arena (2v2 / 3v3)":"Passe, tire, marque — parades héroïques et buzzer-beaters à chaque manche.","RTS (Real-Time Strategy)":"Macro nerveuse et micro décisif — dominer l'économie et les escarmouches.","City Builder":"Croissance vox-urbaine lisible où tes plans racontent une histoire.","4X / Grand Strategy (lite)":"Explorer, étendre, exploiter, exterminer — compressé en matchs punchy.","Action RPG (Top-down)":"Compose un kit, enchaîne tes capacités, traverse des arènes thématiques.","Souls-lite (Top-down)":"Apprentissage de patterns, resets nerveux ; punition juste, triomphe fort.","Metroidvania (Top-down adaptation)":"Progression à clés dans des arènes compactes et révélations d'aptitudes.","Open-World Sandbox (GTA-like Top-down)":"Bac à sable de cascades et braquages courts, parfaits à partager.","Farming / Life Sim":"Boucles cozy avec récompenses visibles et milestones partageables.","Party Micro-Challenges (30–90s)":"Fun instantané ; physique burlesque et rematchs rapides.","Wave Defense (Objective Control)":"Tenir des points sous pression ; combos d'équipe qui font le clip.","Platformer (Top-down hybrid challenges)":"Itinéraires lisibles, timing des sauts, swaps d'aptitudes sous pression.","Beat'em up (Top-down lanes)":"Contrôle de foule, cancels et juggles stylés en arènes compactes.","Rhythm / Music Action":"Pics de performance qui rendent super en stream.","Arena Brawler (Ability Draft)":"Mind-games de pick-ban et synergies déjantées."} pitch=pitches.get(genre,"Chaos lisible et partageable, taillé pour les créateurs.") scenario=f"Thème : {ip or 'univers original'}. Tu guides un petit cast dans des lieux voxel compacts. Sessions courtes pour la regardabilité et l'envie de relancer. Chaque arène expose des objectifs clairs, des dangers qui montent et un feedback instant calibré pour les highlights." return pitch, scenario def match_flow_en(genre, multiplayer): win = "Extract with artifact / highest score" if any(k in genre for k in ["Extraction","Arcade Sport","Arena"]) else "Clear objectives or defeat boss" lose="Team wipe / timer expiry / fail objective" flow=["Lobby: party code or quick match (<=10s).","Draft: pick 1–2 perks / kit piece (20–30s).","Play: objective + hazards escalate (60–180s).","Climax: boss/extract/score race (20–40s).","Highlight: instant replay & MVP; shareable clip.","Meta: cosmetics, badges, seed of the day."] return win,lose,flow def match_flow_fr(genre, multiplayer): win = "Extraction avec artefact / meilleur score" if any(k in genre for k in ["Extraction","Arcade Sport","Arena"]) else "Objectifs validés ou boss vaincu" lose="Équipe éliminée / timer écoulé / objectif raté" flow=["Lobby : code de partie ou matchmaking rapide (<=10s).","Draft : choisir 1–2 perks / élément de kit (20–30s).","Play : objectif + dangers qui montent (60–180s).","Climax : boss/extraction/course au score (20–40s).","Highlight : replay instantané & MVP ; clip partageable.","Meta : cosmétiques, badges, seed du jour."] return win,lose,flow def viral_tips_en(_): return ["Add a one-button Clip Last 10s (local replay buffer).","Daily/weekly seed so creators compete on the same layout.","Mutators that change physics/abilities weekly (surprise!).","Creator polls: chat votes next hazard/perk between rounds.","Short buzzer moments (overtime, sudden death, clutch saves).","Creator codes for cosmetics and giveaway items.","Built-in spectator/killcam + freecam for editors.","Auto-generate challenge prompts ('No dash run', 'Score 5 goals').","Small-team modes (1v1/2v2/3v3) for easy collabs."] def viral_tips_fr(_): return ["Un bouton Clip 10s (buffer replay local).","Un seed quotidien/hebdo pour des défis entre créateurs.","Des mutateurs hebdo (physique, aptitudes) pour la surprise.","Sondages créateur : le chat vote le prochain hazard/perk.","Des moments buzzer courts (prolongations, mort subite, clutch).","Codes créateur pour cosmétiques & giveaways.","Spectator/killcam intégrés + freecam pour le montage.","Générer des défis auto ('Sans dash', 'Marquer 5 buts').","Modes 1v1/2v2/3v3 pour collabs faciles."] def tasks_table(lang="en"): data_en=[["3D Artist (Voxel)","Player/enemy kits; modular tiles; props; LODs; voxel VFX; export pipeline (MagicaVoxel/GLTF)."], ["2D/UI Artist","HUD mockups; icon sets; menus; shop/inventory; key art; store capsules; thumbnails."], ["Menu/UI Implementer","UGUI/UI Toolkit layouts; responsive scaling; state machines; settings (graphics/audio/accessibility)."], ["Frontend Programmer (Unity/C#)","Camera rig; input/aim-assist; ability/perk system; replay buffer; procedural arena loader."], ["Backend/Netcode Programmer","Matchmaking; session sync (Fusion/Relay); profiles/cosmetics; leaderboards; analytics; basic anti-cheat."], ["QA","Test plans; net variability (50–200ms); perf budgets; regression; device matrix."], ["Promotion/Marketing","Teaser/trailer; creator outreach & keys; store page; social cadence; creator-code program; press kit."]] data_fr=[["Artiste 3D (Voxel)","Kits joueur/ennemis ; tuiles modulaires ; props ; LOD ; VFX voxel ; pipeline export (MagicaVoxel/GLTF)."], ["Artiste 2D / UI","Maquettes HUD ; sets d'icônes ; menus ; boutique/inventaire ; key art ; vignettes store ; miniatures."], ["Intégrateur Menu/UI","Layouts UGUI/UI Toolkit ; mise à l'échelle responsive ; machines d'états ; réglages (graph/audio/accessibilité)."], ["Programmeur Front (Unity/C#)","Rig caméra ; input/assist-visee ; système d'aptitudes/perks ; buffer replay ; loader d'arènes procédural."], ["Programmeur Back/Netcode","Matchmaking ; synchro session (Fusion/Relay) ; profils/cosmétiques ; leaderboards ; analytics ; anti-cheat basique."], ["QA","Plans de tests ; variabilité réseau (50–200ms) ; budgets perf ; régressions ; matrice devices."], ["Promotion/Marketing","Teaser/trailer ; contact créateurs & clés ; page store ; calendrier social ; programme codes créateur ; press kit."]] return pd.DataFrame(data_en if lang=="en" else data_fr, columns=(["Role","Responsibilities (2-month scope)"] if lang=="en" else ["Rôle","Responsabilités (portée 2 mois)"])) def pdf_export(title, md_text, df): buf=BytesIO(); c=canvas.Canvas(buf, pagesize=A4); W,H=A4; x=2*cm; y=H-2*cm c.setTitle(title); c.setFont("Helvetica-Bold",14); c.drawString(x,y,title); y-=18; c.setFont("Helvetica",9) for line in md_text.split("\n"): while len(line)>100: c.drawString(x,y,line[:100]); y-=12; line=line[100:] if y<2*cm: c.showPage(); y=H-2*cm; c.setFont("Helvetica",9) c.drawString(x,y,line); y-=12 if y<2*cm: c.showPage(); y=H-2*cm; c.setFont("Helvetica",9) if y<4*cm: c.showPage(); y=H-2*cm; c.setFont("Helvetica",9) c.setFont("Helvetica-Bold",11); c.drawString(x,y,"Tasks by Role"); y-=14; c.setFont("Helvetica",9) for _,row in df.iterrows(): role=str(row.iloc[0]); resp=str(row.iloc[1]) c.setFont("Helvetica-Bold",9); c.drawString(x,y,f"- {role}"); y-=12; c.setFont("Helvetica",9) while len(resp)>100: c.drawString(x+12,y,resp[:100]); y-=12; resp=resp[100:] if y<2*cm: c.showPage(); y=H-2*cm; c.setFont("Helvetica",9) c.drawString(x+12,y,resp); y-=12 if y<2*cm: c.showPage(); y=H-2*cm; c.setFont("Helvetica",9) c.showPage(); c.save(); return buf.getvalue() st.set_page_config(page_title=APP_TITLE, page_icon="🎮", layout="wide") st.title(APP_TITLE); st.caption(f"{VERSION} · Unity · Top-down · Voxel · Streamer-first · PDF/Markdown export") with st.sidebar: st.header("Setup") genre = st.selectbox("🕹️ Genre", GENRES, index=0) age = st.selectbox("🎯 Target Age", AGE_BRACKETS, index=2) ip = st.selectbox("📚 Public-Domain IP (optional)", [""] + PUBLIC_DOMAIN_IP, index=0) multiplayer = st.selectbox("👥 Mode", MULTI_CHOICES, index=0) st.subheader("Streamer / Viral Mechanics") streamer_flags={m: st.checkbox(m, value=("Hype" in m or "Clip" in m or "Spectator" in m)) for m in STREAMER_MECHS} st.subheader("Notes / Constraints") notes = st.text_area("Notes", value="Always top-down voxel in Unity; aim for 1–5 minute rounds.", height=90) st.divider() auto_title_on = st.checkbox("Auto-generate title", value=True) default_title = auto_title(genre, multiplayer, ip) if auto_title_on else "Custom Title" manual_title = st.text_input("Title (editable)", value=default_title) if st.button("🎲 Regenerate Title"): st.experimental_rerun() col1,col2 = st.columns(2) with col1: gen_en = st.button("🇺🇸 GENERATE GDD (US)", use_container_width=True) with col2: gen_fr = st.button("🇫🇷 Générer GD (FR)", use_container_width=True) def build_md(lang): title = manual_title.strip() or auto_title(genre, multiplayer, ip) camera = "Top-down fixed/soft-follow (45–60°)"; art="Voxel, stylized, readability-first" if lang=="en": pitch,scenario = pitch_and_scenario_en(genre, ip); win,lose,flow = match_flow_en(genre, multiplayer); hooks=[m for m,v in streamer_flags.items() if v] or ["Clip-worthy chain reactions","Hype timer","Spectator/Killcam"]; tips=viral_tips_en(genre) md=[f"# {title}", f"**Date:** {now_ymd()} · **Engine:** Unity (URP), C# · **Camera:** {camera} · **Art:** {art}", f"**Mode:** {multiplayer} · **Target:** {age}"] if ip: md.append(f"**Theme/IP:** {ip} (public-domain; avoid protected looks)") md+=["","## 🎯 Pitch", pitch, "", "## 🎬 Scenario / World", scenario, "", "## 📣 Streamer & Viral Hooks"]; md+=[f"- {h}" for h in hooks] md+=["","## 🔁 Match Flow", f"- **Win:** {win}", f"- **Lose:** {lose}"] + [f"- {s}" for s in flow] md+=["","## 🚀 How to Buzz Online"] + [f"- {t}" for t in tips] + ["","## ⚙️ Loops (Core / Meta / Viral)"] return title, "\n".join(md) else: pitch,scenario = pitch_and_scenario_fr(genre, ip); win,lose,flow = match_flow_fr(genre, multiplayer); hooks=[m for m,v in streamer_flags.items() if v] or ["Réactions en chaîne clipables","Timer hype","Spectator/Killcam"]; tips=viral_tips_fr(genre) md=[f"# {title}", f"**Date :** {now_ymd()} · **Moteur :** Unity (URP), C# · **Caméra :** {camera} · **Direction artistique :** {art}", f"**Mode :** {multiplayer} · **Cible :** {age}"] if ip: md.append(f"**Thème/IP :** {ip} (domaine public ; éviter les looks protégés)") md+=["","## 🎯 Pitch", pitch, "", "## 🎬 Scénario / Univers", scenario, "", "## 📣 Mécaniques Streamer & Virales"]; md+=[f"- {h}" for h in hooks] md+=["","## 🔁 Déroulé d'une partie", f"- **Gagner :** {win}", f"- **Perdre :** {lose}"] + [f"- {s}" for s in flow] md+=["","## 🚀 Comment faire buzz"] + [f"- {t}" for t in tips] + ["","## ⚙️ Boucles (Core / Meta / Viral)"] return title, "\n".join(md) tabs = st.tabs(["📄 Document","📋 Tables","📦 Export"]) if gen_en or gen_fr: lang = "en" if gen_en else "fr"; title, md_text = build_md(lang) loops_df = loops_table_en(genre) if lang=="en" else loops_table_fr(genre) roles_df = tasks_table("en" if lang=="en" else "fr") with tabs[0]: st.markdown(md_text) with tabs[1]: st.subheader("Loops" if lang=="en" else "Boucles"); st.dataframe(loops_df, use_container_width=True) st.subheader("Tasks by Role" if lang=="en" else "Tâches par rôle"); st.dataframe(roles_df, use_container_width=True) with tabs[2]: st.download_button("⬇️ Markdown (.md)", data=md_text.encode("utf-8"), file_name=f"{slugify(title)}.md", use_container_width=True) st.download_button("📄 PDF", data=pdf_export(title, md_text, roles_df), file_name=f"{slugify(title)}.pdf", use_container_width=True) else: with tabs[0]: st.info("Choose options, then click 🇺🇸 or 🇫🇷 to generate.") with tabs[1]: st.caption("Tables will appear here after generation.") with tabs[2]: st.caption("Export buttons will appear here after generation.")