226 lines
9.4 KiB
Markdown
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
|