Files

226 lines
9.4 KiB
Markdown

# GunCircle.io — Technical Specification
## Overview
A browser-based multiplayer .io arena shooter (diep.io-style). Continuous FFA — no matches, no timers. Players join, spawn immediately, fight, earn XP, level up, choose class branches, die and respawn.
## Architecture
- **Monorepo**: `packages/shared`, `packages/server`, `packages/client`
- **Client**: TypeScript + Vite + HTML5 Canvas 2D (PC only, WASD + mouse)
- **Server**: Node.js + TypeScript + Colyseus (uWebSockets transport)
- **Physics**: Custom circle-circle + spatial hash grid (no external physics)
- **Network**: Client-side prediction + server reconciliation + entity interpolation
## Shared Package (`packages/shared/`)
Already implemented. Contains:
- `schema.ts` — Colyseus Schema classes (Player, Bullet, XPOrb, Obstacle, LeaderboardEntry, RoomState)
- `types.ts` — TypeScript interfaces (GunConfig, ClassBranch, PlayerInput, Skin, etc.)
- `constants.ts` — Game constants (TICK_RATE, ARENA, COLORS, STATS, XP_LEVELS, etc.)
- `math.ts` — Vector math, spatial hash grid, scalar utilities
- `config/guns.json` — 5 gun definitions (pistol, rifle, shotgun, sniper, SMG)
- `config/branches.json` — 19 class branch definitions
## Server (`packages/server/src/`)
### Entry Point: `index.ts`
Sets up Colyseus server with uWebSockets transport, listens on port 3000, registers `ArenaRoom`.
### Room: `room.ts` — `ArenaRoom extends Room<RoomState>`
- **maxClients**: 50
- **Game loop**: `setInterval` at TICK_RATE (60Hz), increments `state.tick`
- **On join**: create Player, spawn at random position, assign base gun (pistol), send gun configs
- **On leave**: mark player dead, drop 50% XP as orbs after 60s room destroy timer
- **On message**: deserialize PlayerInput, validate, apply
### Game Loop: `game-loop.ts`
Per tick (order matters):
1. Process player inputs (movement, shooting, upgrades)
2. Update bullet positions (linear velocity)
3. Update player positions (velocity + friction)
4. Detect collisions (bullets vs players, players vs obstacles, players vs XP orbs)
5. Apply damage, handle deaths
6. Update recoil recovery (lerp recoilOffset toward 0)
7. Update reload timers
8. Regenerate HP
9. Update leaderboard
10. Broadcast state (Colyseus handles delta compression)
### Physics: `physics.ts`
- `SpatialHashGrid` for O(1) queries
- `circleCollisionResolve()` for player-player and player-obstacle
- `lineCircleIntersect()` for bullet hit detection
- Arena bounds clamping
### Player Manager: `player-manager.ts`
- `spawnPlayer()`: random position, base stats, apply class bonuses
- `applyInput()`: validate speed, update velocity, handle shooting
- `handleShoot()`: apply recoil, spawn bullet(s), apply kickback, consume ammo
- `takeDamage()`: apply damage, check crit, check death
- `onDeath()`: drop XP orbs, reset level/stats, schedule respawn
- `respawnPlayer()`: new random position, level 1, base gun
- `applyUpgrade()`: validate stat choice, apply bonus
- `checkLevelUp()`: check XP thresholds, award upgrade points, prompt branch choice
### Collision: `collision.ts`
- `checkBulletPlayerCollisions()`: spatial hash query, damage on overlap
- `checkPlayerOrbCollisions()`: collect orbs within pickup radius
- `checkPlayerObstacleCollisions()`: resolve overlap, apply slow zone effect
- `checkPlayerPlayerCollisions()`: soft collision (push apart)
### Gun System: `gun-system.ts`
- Load `config/guns.json` at startup
- `getGunConfig(id)` / `getGunConfigByIndex(idx)`
- `calculateRecoil()`: random sign * recoilOffset * (1 - recoilStatBonus)
- `calculateKickback()`: -aimVector * kickbackForce
- `calculateBulletSpawn()`: barrel tip position at aimAngle + recoilOffset
### Class Branch System: `branch-system.ts`
- Load `config/branches.json` at startup
- `getAvailableBranches(level, currentBranch)` — filters by levelRequired and parentId
- `applyBranchBonuses(player, branchId)` — applies passive stat multipliers
- `getUnlockedGunCategories(branchIds)` — union of categories
### Validation: `validation.ts`
- `validateMovement()`: speed <= max * (1 + moveSpeedStat)
- `validateAimAngle()`: change <= max_turn_rate per tick
- `validateFireRate()`: timeSinceLastShot >= cooldown / (1 + reloadStat)
- `validateRecoil()`: server-calculated vs client-reported within tolerance
## Client (`packages/client/src/`)
### Entry Point: `main.ts`
- Wait for DOM ready
- Show menu, get player name
- On PLAY click: connect to Colyseus room, hide menu, start game loop
### Renderer: `renderer.ts`
Canvas 2D rendering engine:
- `render()`: called every requestAnimationFrame
- Clear canvas → save → apply camera transform → render world → restore → render HUD
**Render order (world space):**
1. Arena background (solid color + grid lines)
2. Obstacles (walls = grey rects, crates = brown rects, slow zones = blue tint, cover = green)
3. XP orbs (small colored squares/circles)
4. Bullets (filled circles with slight glow, semi-transparent trail circles behind)
5. Players (circles: blue = self, red = others)
6. Gun rendering (rectangle barrel + square body, rotated at aimAngle + recoilOffset)
7. Name tags (above circle, always visible)
8. HP bars (below name tag, green fill / dark bg)
**Gun rendering details:**
- Barrel: rectangle, `GUN_BARREL_WIDTH` x `GUN_BARREL_LENGTH`, attached to player center
- Body: small square `GUN_BODY_SIZE` behind barrel
- Rotation: around player center at `angle + recoilOffset`
- Recovery: lerp recoilOffset toward 0 each frame
**Bullet rendering:**
- Main circle: radius = bulletSize, filled with bulletColor
- Trail: 3-5 semi-transparent smaller circles behind at velocity * -dt positions
- Crit: red outline stroke (2px) when isCritical = true
### Camera: `camera.ts`
- Follow local player with slight lag (lerp at ~0.1 per frame)
- Clamp to arena bounds + viewport padding
- Transform: translate(canvas/2 - camX, canvas/2 - camY)
### Input: `input.ts`
- WASD: track key states, compute moveAngle from active keys
- Mouse: track position, convert screen → world for aimAngle
- Left click: isShooting flag
- Send input at 60Hz (setInterval)
### Networking: `network.ts`
- Connect to Colyseus server via WebSocket
- Client-side prediction: immediately move on WASD, apply velocity
- Server reconciliation: compare predicted pos to server pos, smooth correction
- Entity interpolation: for other players/bullets, lerp between prev and current state
- Bullet confirmation: predict spawn locally, server confirms trajectory
### HUD: `hud.ts`
- HP bar (top-left): green fill, shows current/max
- XP bar (below HP): yellow fill, shows progress to next level
- Ammo counter (bottom-right): current/max, reload indicator
- Level indicator (top-right): large number
- Leaderboard (top-right below level): sorted by XP, shows top 10
- Name above local player: white text, centered
- Upgrade popup: appears on level-up, shows stat buttons + branch choice at milestones
### Menu: `menu.ts`
- Name input (max 16 chars)
- PLAY button → connect → hide menu → show HUD
### Game Loop: `game.ts`
- `update(dt)`: process input, apply prediction, update camera, update HUD
- `render()`: call renderer
- Uses requestAnimationFrame with delta time
- Interpolate entity positions between server ticks
## Networking Protocol
### Client → Server (60Hz)
```
PlayerInput {
seq: uint32 // frame counter
moveAngle: float32 // -1 = none
aimAngle: float32
isShooting: bool
upgradeChoice?: uint8
}
```
### Server → Client (60Hz, delta compressed via Colyseus)
Full RoomState with Player, Bullet, XPOrb maps. Colyseus automatically sends only changed fields.
### Bandwidth Budget
- Target: < 4 KB/s per player downstream
- With 50 players + 100 bullets + 50 orbs, delta should be ~2-3 KB per tick
- Colyseus Schema uses binary encoding + delta compression
## File Structure
```
packages/server/src/
index.ts — server entry point
room.ts — ArenaRoom definition
game-loop.ts — tick loop
physics.ts — spatial hash, collision resolution
player-manager.ts — spawn, death, respawn, upgrades
collision.ts — bullet-player, player-orb, player-obstacle
gun-system.ts — gun config loading, recoil calc
branch-system.ts — class branch loading, bonus application
validation.ts — input validation, anti-cheat
bot-player.ts — AI bot for testing (optional)
packages/client/src/
main.ts — client entry, menu, connect
renderer.ts — Canvas 2D rendering
camera.ts — camera follow + smoothing
input.ts — WASD + mouse input
network.ts — Colyseus client, prediction, reconciliation
hud.ts — UI overlays (HP, XP, leaderboard, upgrades)
game.ts — client game loop (update + render)
interpolation.ts — entity interpolation between ticks
asset-loader.ts — optional image/font loading
```
## Quality Gates
1. Client: 60 FPS with 50 entities (Chrome, mid-tier laptop)
2. Server: 60 Hz tick with 50 players (<10ms per tick)
3. Bandwidth: < 4 KB/s per player downstream
4. 100ms latency feels playable (prediction hides lag)
5. No memory leaks over 10-minute session
## Visual Style (diep.io-like)
- Self: blue circle (#3498db)
- Enemies: red circle (#e74c3c)
- Background: dark (#1a1a2e) with subtle grid
- No particles, no screen shake, no muzzle flash
- Only visual feedback: gun recoil animation
- Clean, minimal, readable
## Skin System (Cosmetic Only)
- Circle color/pattern overrides
- Gun appearance overrides
- Name tag style overrides
- MVP: simple hardcoded palette, shop UI stub