Art Attack is a real-time, 2-player online fighting game where you draw your own character, Gemini AI turns that sketch into a fighter with stats and moves, and you battle a friend in a synced arena.
Draw → Analyze → Fight.
Two players join a shared room (code or QR). Each draws a fighter on a canvas. When both lock in, the server uses Gemini to:
- Analyze the drawing (element, personality, stats, special abilities)
- Generate a polished 2D sprite from the sketch
- Announce the match with TTS voice
- Commentate after the fight
Match state (lobby, drawings, fighters, HP, positions) syncs live through Firebase Firestore. The client runs a 60 FPS canvas arena with local physics and networked opponent state.
- Create / join rooms with a 5-character code, shareable link (
?room=CODE), or QR code - Drawing studio with brush, eraser, color presets, size control (mouse + touch)
- Gemini multimodal analysis — character name, element, stats, 3 special moves, dialogue, stage mood
- AI sprite generation — sketch → arcade-style fighter sprite
- Announcer intro with Gemini TTS and countdown
- Real-time 2D fight arena — move, jump, block, punch, specials; projectiles & particles
- Procedural fight music and SFX via Web Audio API
- Post-fight AI commentary and rematch support
- Mobile-friendly touch controls in the arena
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, Vite 6, Tailwind CSS 4, Motion, Lucide |
| Backend | Express (Node), Vite middleware in dev |
| AI | Google Gemini (@google/genai) — vision, image gen, TTS, text |
| Multiplayer | Firebase Firestore (realtime onSnapshot) |
| Audio | Web Audio API (procedural BGM + SFX + Gemini PCM playback) |
| Tooling | Bun/npm, tsx, esbuild (production server bundle) |
LOBBY → DRAWING → ANALYZING / SPRITE_GEN → INTRO → FIGHT → FINISHED
| Phase | What happens |
|---|---|
| Lobby | Host creates a room; guest joins by code / URL / QR. Host starts when both are ready. |
| Drawing | Each player sketches a fighter and locks in. When both lock, status → ANALYZING. |
| Analyzing | Server analyzes each drawing and generates a sprite; fighter data is written to Firestore. |
| Intro | Announcer TTS introduces both fighters; countdown → fight. |
| Fight | Real-time arena combat until KO or finish; optional rematch. |
Host (player1) typically advances room status and initializes fight state.
- Node.js 20+ (or Bun)
- A Gemini API key (Google AI Studio)
- A Firebase project with Firestore enabled (or use the included applet config if you already have one provisioned)
git clone https://github.com/<your-org>/ArtAttack.git
cd ArtAttack
npm install
# or: bun installCopy the example env file and fill in secrets:
cp .env.example .env| Variable | Required | Description |
|---|---|---|
GEMINI_API_KEY |
Yes | Gemini API key used by the Express server |
APP_URL |
Optional | Public base URL of the app (useful for share links / hosting) |
The server reads GEMINI_API_KEY from the environment. Without it, AI endpoints will fail unless a key is supplied another way in your deployment setup.
Client Firebase settings live in firebase-applet-config.json. On load, src/lib/firebaseHelper.ts initializes the app from that file (or an override).
Rooms are stored under the Firestore collection:
art_attack_rooms/{roomCode}
Deploy or apply firestore.rules to your project. The checked-in rules allow open read/write for prototyping — tighten them before any public production deploy.
npm run devThis starts server.ts on http://localhost:3000 with Vite middleware (HMR + SPA).
Open two browser windows (or one desktop + one phone on the same network), create a room in one, join with the code in the other.
npm run build
npm startbuild— Vite client build + esbuild bundle ofserver.ts→dist/server.cjsstart— runs the production server and serves static files fromdist/
Other scripts:
npm run lint # tsc --noEmit
npm run clean # remove dist / build artifacts| Action | Keys |
|---|---|
| Move left / right | A / D or ← / → |
| Jump | W or ↑ |
| Block | S or ↓ |
| Basic punch | Space |
| Special 1 / 2 / 3 | 1 / 2 / 3 or Z / X / C |
On-screen D-pad buttons appear for move / jump on mobile; ability buttons are on the HUD.
Blocking reduces incoming damage (~70% reduction). Defense also mitigates damage. Abilities respect per-move cooldowns from Gemini’s analysis.
ArtAttack/
├── index.html # SPA entry
├── server.ts # Express + Gemini API routes + Vite/static
├── package.json
├── vite.config.ts
├── tsconfig.json
├── firebase-applet-config.json
├── firestore.rules
├── .env.example
├── metadata.json # App name / capability metadata
└── src/
├── main.tsx
├── App.tsx # Phase router + room state
├── index.css
├── types.ts # Shared game / room types
├── lib/
│ ├── firebaseHelper.ts # Rooms, sync, fight state
│ └── audioEngine.ts # BGM, SFX, Gemini TTS playback
└── components/
├── TopConfigBar.tsx
├── Phase0Lobby.tsx
├── Phase1DrawingRoom.tsx
├── Phase2Phase3FighterGen.tsx
├── Phase4AnnouncerIntro.tsx
└── Phase5FightArena.tsx
All Gemini calls are proxied through the Express server (keeps the API key off the client). Base URL is the same origin as the app (e.g. http://localhost:3000).
Analyzes a hand-drawn PNG (base64) and returns structured fighter JSON.
Body
{
"drawing": "data:image/png;base64,...",
"customApiKey": "optional"
}Response (success) — characterName, element, personality, stats (hp, attack, defense, speed), abilities[], musicMood, entryDialogue, victoryDialogue, environmentName.
Model: gemini-3.6-flash (structured JSON schema).
Redraws the sketch as a polished 1:1 fighting-game sprite.
Body
{
"drawing": "data:image/png;base64,...",
"characterName": "Blaze Knight",
"element": "fire",
"customApiKey": "optional"
}Response — { "success": true, "spriteUrl": "data:image/png;base64,..." }
On failure, may return the original drawing as fallbackUrl.
Model: gemini-3.1-flash-image.
Generates dramatic announcer audio (PCM base64).
Body
{
"text": "Introducing Blaze Knight!",
"voice": "Fenrir",
"customApiKey": "optional"
}Response — { "success": true, "audioBase64": "...", "sampleRate": 24000 }
Model: gemini-3.1-flash-tts-preview.
Short post-match commentary.
Body
{
"winnerName": "Blaze Knight",
"loserName": "Ice Wraith",
"duration": 42,
"remainingHp": 37,
"customApiKey": "optional"
}Response — { "success": true, "commentary": "..." }
┌─────────────┐ Firestore snapshots ┌─────────────┐
│ Player 1 │◄────────────────────────────►│ Player 2 │
│ (browser) │ │ (browser) │
└──────┬──────┘ └──────┬──────┘
│ │
│ POST /api/gemini/* │
└──────────────────►┌──────────────┐◄────────┘
│ Express + │
│ Gemini SDK │
└──────────────┘
- Authoritative sync surface: Firestore room document (
status, players,fighterData,fightState). - Local simulation: Each client runs movement/physics for their own fighter at ~60 FPS and writes updates via
syncPlayerState/ damage helpers. - Opponent rendering: Remote state is lerped for smoother motion.
- Session identity:
sessionStorageuser id (art_attack_uid); rooms cap at 2 players.
Generated fighters include:
- Identity: name, element, personality
- Stats: HP, attack, defense, speed
- Abilities: name, description, damage, cooldown, type (
projectile|melee|buff|area) - Flavor: music mood, entry/victory lines, environment name
- Art: original drawing URL + generated sprite URL
See src/types.ts for full TypeScript definitions (RoomData, FighterData, PlayerFightState, etc.).
- Firebase is auto-initialized from
firebase-applet-config.json(supports a non-defaultfirestoreDatabaseId). - Gemini is server-side only; set
GEMINI_API_KEYin.envfor local runs. - In AI Studio / Cloud Run-style hosting,
GEMINI_API_KEYandAPP_URLmay be injected at runtime (see comments in.env.example). - Vite HMR can be disabled with
DISABLE_HMR=true(used in some agent/edit environments).
| Issue | What to try |
|---|---|
| Firebase not connected | Confirm firebase-applet-config.json has a valid projectId and Firestore is enabled; check browser console. |
| Analyze / sprite / TTS fails | Ensure GEMINI_API_KEY is set and the key has access to the models used in server.ts. |
| Second player can’t join | Room may already have 2 players, or the code is wrong (codes are uppercase alphanumeric). |
| Fight feels desynced | Both clients need a stable network; fight state syncs through Firestore — high latency will lag opponent lerp. |
| No sound | Browsers require a user gesture before AudioContext; click/tap once, and ensure sound isn’t muted in the arena UI. |
| Port in use | Server listens on 3000; free the port or change PORT in server.ts. |
- Do not commit real
.envfiles (ignored via.gitignore). - Firebase client config in the repo is normal for web apps, but Firestore rules in this repo are open — replace with authenticated, room-scoped rules before production.
- Keep
GEMINI_API_KEYon the server only; never expose it in frontend bundles.
Private project ("private": true in package.json). Add a license file if you plan to open-source or redistribute.
Built with React, Firebase, and Google Gemini — draw it, AI powers it, fight it.