A full-stack interactive story generator set in the world of Runeterra — the universe of League of Legends. Users choose a realm and navigate branching narratives generated by an LLM acting as a Chronicler of Runeterra. Powered by Google Gemini via LangChain, built with FastAPI and React.
- Runeterra-Immersive Stories — Every tale is set in the world of Runeterra, featuring its regions, champions, factions, and magic system. The LLM is prompted as a Chronicler of Runeterra to deliver lore-rich narratives.
- Realm-Based Generation — Enter any realm of Runeterra (e.g., Piltover, Demacia, Shadow Isles) and get a unique branching story steeped in that region's lore.
- Branching Narratives — Each non-ending node offers 2–3 meaningful choices, creating a tree structure 3–4 levels deep with consequences that feel real.
- Multiple Endings — Stories include both victorious and tragic endings across paths of varying lengths.
- Async Job Processing — Long-running LLM inference runs in the background via FastAPI's
BackgroundTasks; the frontend polls for status in real time. - LLM-Powered — Uses Google Gemini (via LangChain) with Pydantic-validated structured output to guarantee deterministic, tree-shaped JSON.
- League of Legends Design System — A custom LoL-inspired UI with deep viridian backgrounds, gilded gold accents, and steel-trimmed panels. Features glassmorphism cards, grid overlays reminiscent of the LoL client, and a hextech-themed spinner.
| Service | URL |
|---|---|
| Frontend (Vercel) | https://lore-of-legends-pi.vercel.app/ |
| Backend API (Render) | https://lore-of-legends.onrender.com |
| API Docs (Swagger UI) | https://lore-of-legends.onrender.com/docs |
| Technology | Purpose |
|---|---|
| FastAPI | Web framework & REST API |
| PostgreSQL | Relational database |
| SQLAlchemy | ORM & database models |
| LangChain | LLM orchestration framework |
| LangChain Google GenAI | Google Gemini integration |
| Pydantic | Data validation & structured output parsing |
| Uvicorn | ASGI server |
| Technology | Purpose |
|---|---|
| React 19 | UI library |
| Vite | Build tool & dev server |
| React Router v7 | Client-side routing |
| Axios | HTTP client for API calls |
| Cinzel | Display & heading typeface |
| Plus Jakarta Sans | Body typeface |
- Google Gemini (
gemini-2.5-flash-lite) via LangChain
lore-of-legends/
├── backend/
│ ├── main.py # FastAPI app entry point
│ ├── pyproject.toml # Python project config & dependencies
│ ├── requirements.txt # pip dependencies
│ ├── core/
│ │ ├── config.py # Settings / environment config
│ │ ├── models.py # Pydantic models for LLM output parsing
│ │ ├── prompts.py # LLM system prompts (Runeterra storyteller)
│ │ └── story_generator.py # Story generation logic (LangChain chain)
│ ├── db/
│ │ └── database.py # SQLAlchemy engine & session setup
│ ├── models/
│ │ ├── story.py # Story & StoryNode ORM models
│ │ └── job.py # StoryJob ORM model (async processing)
│ ├── routers/
│ │ ├── story.py # /api/stories/ endpoints
│ │ └── job.py # /api/jobs/ endpoints
│ └── schemas/
│ ├── story.py # Pydantic schemas for story API
│ └── job.py # Pydantic schemas for job API
├── frontend/
│ ├── index.html # HTML entry point (Runeterra-themed)
│ ├── vite.config.js # Vite configuration
│ ├── package.json # Node dependencies
│ └── src/
│ ├── main.jsx # React entry point
│ ├── App.jsx # Root component with router & Runeterra branding
│ ├── App.css # LoL-inspired dark fantasy design system
│ ├── index.css # Reset & base styles (dark viridian palette)
│ ├── util.js # API base URL helper
│ └── components/
│ ├── StoryGenerator.jsx # Realm input & job polling logic
│ ├── StoryLoader.jsx # Fetches and loads a completed chronicle
│ ├── StoryGame.jsx # Interactive branching narrative UI
│ ├── ThemeInput.jsx # Realm submission form with Runeterra regions
│ └── LoadingStatus.jsx # Hextech spinner while legend is forged
└── README.md
- Python 3.14+
- Node.js 20+ and npm
- PostgreSQL instance (local or cloud)
- Google Gemini API key (get one from Google AI Studio)
-
Clone the repository
git clone https://github.com/jnrgxx/lore-of-legends.git cd lore-of-legends/backend -
Create and activate a virtual environment
python -m venv venv # Windows: venv\Scripts\activate # macOS / Linux: source venv/bin/activate
-
Install dependencies
pip install -r requirements.txt
-
Configure environment variables
Create a
.envfile inbackend/:DATABASE_URL=postgresql://user:password@localhost:5432/lore_of_legends API_KEY=your_gemini_api_key_here ALLOWED_ORIGINS=http://localhost:5173
-
Run the server
uvicorn main:app --reload
The API will be available at
http://localhost:8000and interactive docs athttp://localhost:8000/docs.
-
Navigate to the frontend directory
cd lore-of-legends/frontend -
Install dependencies
npm install
-
Configure API URL (optional)
Create a
.envfile infrontend/:VITE_API_URL=http://localhost:8000
-
Start the dev server
npm run dev
The app will be available at
http://localhost:5173.
Tip: Run both the backend and frontend simultaneously in separate terminals for the full experience.
The UI is inspired by the League of Legends client:
- Palette: Deep viridian backgrounds (
#0A0E0C), gilded gold accents (#C89B3C— Riot's official gold), steel blue-gray secondary tones (#5B7A6E) - Typography: Cinzel Decorative for display text, Cinzel for headings, Plus Jakarta Sans for body copy
- Effects: Glassmorphism cards with backdrop blur, subtle grid overlay reminiscent of the LoL client, gold line dividers, runic decorative corner elements
- Animations: Fade-in entrance animations on all panels, animated gold gradient buttons, options that slide on hover
Create a new story generation job.
Request body:
{
"theme": "Piltover"
}Response (immediate — job queued):
{
"job_id": "uuid-string",
"status": "pending",
"created_at": "2026-05-07T12:00:00Z"
}Poll for job status.
Response:
{
"job_id": "uuid-string",
"status": "completed",
"story_id": 42,
"created_at": "2026-05-07T12:00:00Z",
"completed_at": "2026-05-07T12:00:15Z"
}Status values: pending → processing → completed / failed
Retrieve the full branching story tree.
Response:
{
"id": 42,
"title": "Shadows of Piltover",
"session_id": "uuid-string",
"created_at": "2026-05-07T12:00:15Z",
"root_node": {
"id": 1,
"content": "The grey glow of hextech lamps flickers across the rain-slicked cobblestones of Piltover's Promenade...",
"is_ending": false,
"is_winning_ending": false,
"options": [
{ "text": "Slip into the back alleys of the Undercity", "node_id": 2 },
{ "text": "Approach the Enforcer with confidence", "node_id": 3 }
]
},
"all_nodes": { ... }
}- User chooses a realm — The React frontend sends a
POST /api/stories/createrequest with a Runeterra region (e.g., "Piltover", "Demacia"). - Job is queued — The backend creates a
StoryJobrecord with statuspendingand returns ajob_idimmediately. - Background processing — FastAPI's
BackgroundTaskscallsStoryGenerator.generate_story()in a separate database session. - Chronicler invocation — LangChain sends the realm + structured prompt to Google Gemini, instructing it as a Chronicler of Runeterra to return a complete branching story set in the League of Legends universe.
- Pydantic validation — The raw LLM output is parsed and validated against
StoryLLMResponse, ensuring deterministic structure. - Database persistence — The story tree is stored as
StoryandStoryNoderecords (with options as JSON), forming a recursive graph. - Frontend polling — The React app polls
GET /api/jobs/{job_id}every 5 seconds until status iscompleted. - Story gameplay — Once complete, the frontend fetches the full story tree via
GET /api/stories/{story_id}/completeand renders the interactive branching narrative with Runeterra-flavored UI.