Modular 3D race character editor with equipment toggling, stat allocation, combat simulation, and persistent saves. Built for Grudge Studio by Racalvin The Pirate King.
Live: playground-teal-zeta.vercel.app
Manifest API: grudge-models-worker.grudge.workers.dev
- 6 Faction Races — Human (WK_), Barbarian (BRB_), Elf (ELF_), Dwarf (DWF_), Orc (ORC_), Undead (UD_)
- 42 Equipment Slots Per Race — Armor (body×5, arms×4, legs×3, head×9, shoulders×2), weapons, shields, utility
- Equipment Resolution — D1 manifest maps equipment state → mesh names → R2 GLB URLs, identical to the Unity game
- 8-Attribute Stats — STR/DEX/INT/VIT/WIS/LCK/CHA/END with diminishing returns and 37 derived stats
- 14 Animation Packs — 1H Sword+Shield, 2H Melee, Longbow, Magic, Great Sword, Rifle, and 8 more
- Combat Simulation — 8-step pipeline with crit, block, dodge, reflect, absorb
- Persistent Saves — Grudge UUID auth via api.grudge-studio.com (guest + Discord + Google)
- Class & Profession Trees — Warrior, Ranger, Mage, Worge with skill trees and 5 harvesting professions
┌─────────────┐ ┌──────────────────────────────┐ ┌─────────────────┐
│ Vercel │─────▷│ Cloudflare Worker (D1 API) │ │ Cloudflare R2 │
│ (Frontend) │ │ grudge-models-worker │ │ assets.grudge- │
│ Vite SPA │ │ /api/manifest → D1 query │ │ studio.com │
└──────┬──────┘ └──────────────────────────────┘ └────────┬────────┘
│ │
│ Equipment state GLB models │
│ (faction + race + slot + variant) served via │
│ │ CDN URL │
│ ▼ │
│ EquipmentManager.js │
│ toggles child mesh visibility │
│ by name (e.g. WK_Units_Body_A) ◁─────────────────┘
│
└──────▷ api.grudge-studio.com
(Grudge backend — auth + character CRUD)
npm install
npm run devRuns at http://localhost:3000. FBX models are served from the parent directory via the Vite plugin in vite.config.js. No R2/D1 needed — the app falls back to bundled FactionRegistry.js data.
playground/
├── d1/ # Cloudflare D1 schema + seed data
│ ├── schema.sql # Tables: models, equipment_slots, animation_packs, weapon_model_packs
│ └── seed.sql # Generated: 6 models, 252 equipment slots, 14 animation packs
├── worker/
│ └── index.js # Cloudflare Worker — D1 manifest API
├── scripts/
│ ├── convert-models.mjs # FBX → GLB pipeline (preserves mesh hierarchy)
│ ├── upload-r2.mjs # Upload GLBs to R2 CDN
│ └── gen-seed-sql.mjs # Generate d1/seed.sql from faction data
├── src/
│ ├── main.js # App entry — scene, UI, boot sequence
│ └── modules/
│ ├── AssetConfig.js # R2 URL builder (VITE_R2_BASE_URL)
│ ├── GrudgeAuth.js # Grudge backend auth (guest + Discord + Google)
│ ├── CharacterStore.js # Character CRUD via GrudgeAuth
│ ├── EquipmentManager.js # Prefix-based mesh toggle (WK_Units_Body_A)
│ ├── FactionRegistry.js # Fetches D1 manifest, falls back to bundled data
│ ├── SmartLoader.js # Auto-detect FBX/GLTF loader
│ ├── StatsEngine.js # 8 attrs → 37 derived stats + combat sim
│ ├── GameData.js # Classes, skills, professions, weapon types
│ ├── WeaponAnimController.js # Weapon ↔ animation pack binding
│ ├── BoneAttachment.js # Attach weapons to bone containers
│ ├── PostFX.js # Bloom, tone mapping post-processing
│ ├── VFXManager.js # Particle effects
│ ├── BossFight.js # Boss arena mode
│ └── AssetCache.js # Asset preloading
├── wrangler.toml # Cloudflare Worker + D1 config
├── vercel.json # Vercel deployment config
├── vite.config.js # Vite + local asset serving plugin
├── server.js # Express server for local production testing
└── index.html # Single-page app shell
- Wrangler CLI authenticated with Cloudflare
- Vercel CLI linked to the project
- FBX source models (Toon_RTS pack) for GLB conversion
# Create database (one-time)
npx wrangler d1 create grudge-models
# → Put the database_id in wrangler.toml
# Apply schema
npx wrangler d1 execute grudge-models --remote --file=d1/schema.sql
# Seed data
node scripts/gen-seed-sql.mjs
npx wrangler d1 execute grudge-models --remote --file=d1/seed.sqlnpx wrangler deploy
# → https://grudge-models-worker.grudge.workers.dev# Convert FBX models to optimized GLB (preserves mesh hierarchy)
npm run convert
# → Output: dist-models/*.glb
# Upload to R2 CDN
npm run upload
# → https://assets.grudge-studio.com/models/characters/*.glbCritical: The conversion skips gltf-transform join and flatten to preserve per-mesh toggleability. All child mesh names (e.g. WK_Units_Body_A) and bone containers (R_hand_container, L_shield_container) must survive intact.
Env vars (set via vercel env add or Vercel dashboard):
VITE_R2_BASE_URL=https://assets.grudge-studio.comVITE_MANIFEST_API=https://grudge-models-worker.grudge.workers.devVITE_GRUDGE_API=https://api.grudge-studio.com
npx vercel --prodThe Cloudflare Worker serves the model manifest from D1 at edge, so the frontend knows which meshes to toggle for each equipment combination.
| Endpoint | Description |
|---|---|
GET /api/manifest |
Full manifest (factions + equipment + animations) |
GET /api/models |
All 6 race models |
GET /api/models/:id |
Single model with equipment slots |
GET /api/models/:id/equip |
Equipment slots for a model |
GET /api/animations |
All animation packs |
GET /api/weapons |
Weapon model packs |
GET /health |
Health check |
All 6 race models share identical mesh naming with race-specific prefixes:
| Slot | Group | Variants | Mesh Name Pattern | Bone Container |
|---|---|---|---|---|
| body | armor | A–E | {PREFIX}Units_Body_{V} |
— |
| arms | armor | A–D | {PREFIX}Units_Arms_{V} |
— |
| legs | armor | A–C | {PREFIX}Units_Legs_{V} |
— |
| head | armor | A–I | {PREFIX}Units_head_{V} |
— |
| shoulders | armor | A–B | {PREFIX}Units_shoulderpads_{V} |
— |
| sword | weapon_r | A–B | {PREFIX}Units_sword_{V} |
R_hand_container |
| axe | weapon_r | A–B | {PREFIX}Units_axe_{V} |
R_hand_container |
| hammer | weapon_r | A–B | {PREFIX}Units_hammer_{V} |
R_hand_container |
| pick | weapon_r | — | {PREFIX}Units_pick |
R_hand_container |
| spear | weapon_r | — | {PREFIX}Units_spear |
R_hand_container |
| bow | weapon_l | — | {PREFIX}Units_Bow |
L_hand_container |
| staff | weapon_l | A–C | {PREFIX}Units_staff_{V} |
L_hand_container |
| shield | shield | A–D | {PREFIX}Units_shield_{V} |
L_shield_container |
| bag | utility | — | {PREFIX}Xtra_bag |
Bone_bag |
| wood | utility | — | {PREFIX}Xtra_wood |
Bone_wood |
| quiver | utility | — | {PREFIX}Xtra_quiver |
Quiver_container |
Prefixes: WK_ (Human), BRB_ (Barbarian), ELF_ (Elf), DWF_ (Dwarf), ORC_ (Orc), UD_ (Undead)
Authentication is handled by GrudgeAuth.js which connects to api.grudge-studio.com:
- Guest Login — Auto-creates a Grudge UUID, returns JWT
- Discord OAuth — Redirects to backend
/auth/discord, returns with token - Google OAuth — Redirects to backend
/auth/google - Session — Token stored in sessionStorage, restored on page load
Character CRUD (create/read/update/delete) goes through the Grudge backend, not local storage.
| Script | Description |
|---|---|
npm run dev |
Start Vite dev server (localhost:3000) |
npm run build |
Build for production |
npm run serve |
Build + run Express server (localhost:4010) |
npm run convert |
FBX → GLB conversion |
npm run upload |
Upload GLBs to R2 |
npm run seed |
Seed D1 (uses old script) |
npm run d1:init |
Apply D1 schema (remote) |
npm run worker:dev |
Run Worker locally |
npm run worker:deploy |
Deploy Worker to Cloudflare |
- Frontend: Vanilla JS + Three.js, Vite
- 3D: FBXLoader + GLTFLoader (SmartLoader auto-detects), EquipmentManager mesh toggling
- Backend: Grudge API (api.grudge-studio.com) — Express, PostgreSQL, Drizzle ORM
- Model Manifest: Cloudflare D1 (SQLite at edge) + Worker API
- Asset CDN: Cloudflare R2 (assets.grudge-studio.com)
- Hosting: Vercel (SPA deployment)
- Auth: Grudge UUID + Discord/Google OAuth via backend