Initial commit: GunCircle.io multiplayer arena shooter
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@guncircle/shared",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@colyseus/schema": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.0",
|
||||
"vitest": "^2.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// GunCircle.io — Game Constants
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Server tick rate (Hz) */
|
||||
export const TICK_RATE = 60;
|
||||
|
||||
/** Duration of one tick in ms */
|
||||
export const TICK_MS = 1000 / TICK_RATE;
|
||||
|
||||
/** Arena dimensions */
|
||||
export const ARENA_WIDTH = 3000;
|
||||
export const ARENA_HEIGHT = 3000;
|
||||
|
||||
/** Max players per room */
|
||||
export const MAX_PLAYERS_PER_ROOM = 50;
|
||||
|
||||
/** Player base stats */
|
||||
export const PLAYER_BASE_RADIUS = 20;
|
||||
export const PLAYER_BASE_HP = 100;
|
||||
export const PLAYER_BASE_SPEED = 150; // units per second
|
||||
export const PLAYER_BASE_REGEN = 1; // HP per second
|
||||
|
||||
/** Movement physics */
|
||||
export const PLAYER_ACCELERATION = 800; // units per second^2
|
||||
export const PLAYER_FRICTION = 0.92; // velocity multiplier per tick
|
||||
export const PLAYER_MAX_SPEED_MULT = 1.0;
|
||||
|
||||
/** Gun rendering */
|
||||
export const GUN_BARREL_WIDTH = 8;
|
||||
export const GUN_BARREL_LENGTH = 22;
|
||||
export const GUN_BODY_SIZE = 10;
|
||||
export const GUN_RECOVERY_LERP = 0.15; // per tick
|
||||
|
||||
/** XP system */
|
||||
export const XP_LEVELS = [
|
||||
0, 50, 150, 300, 500, 750, 1050, 1400, 1800, 2250,
|
||||
2750, 3300, 3900, 4550, 5250, 6000, 7000, 8200, 9500, 11000,
|
||||
12700, 14500, 16500, 18700, 21100, 23700, 26500, 29500, 32700, 36100,
|
||||
40000, 45000, 51000, 58000, 66000, 75000, 85000, 96000, 108000, 121000,
|
||||
135000, 150000, 167000, 185000, 204000, 225000,
|
||||
];
|
||||
|
||||
export const MAX_LEVEL = XP_LEVELS.length; // 45
|
||||
|
||||
/** Death / respawn */
|
||||
export const DEATH_XP_DROP_PCT = 0.5;
|
||||
export const RESPAWN_DELAY_MS = 3000;
|
||||
export const XP_ORB_LIFETIME_S = 30;
|
||||
export const XP_ORB_DESPAWN_MS = XP_ORB_LIFETIME_S * 1000;
|
||||
|
||||
/** Class branch milestone levels */
|
||||
export const BRANCH_LEVELS = [5, 10, 15, 30, 45];
|
||||
|
||||
/** Stat upgrade system */
|
||||
export const STAT_MAX_LEVEL = 7;
|
||||
export const STAT_UPGRADE_COST_BASE = 1; // 1 point per upgrade
|
||||
|
||||
/** Stats enum — indexes into uint8 array */
|
||||
export enum Stat {
|
||||
MaxHp = 0,
|
||||
HpRegen = 1,
|
||||
MovementSpeed = 2,
|
||||
BulletDamage = 3,
|
||||
BulletSpeed = 4,
|
||||
ReloadSpeed = 5,
|
||||
RecoilStability = 6,
|
||||
CritChance = 7,
|
||||
CritDamage = 8,
|
||||
}
|
||||
|
||||
export const STAT_COUNT = 9;
|
||||
|
||||
/** Stat bonuses per level (additive) */
|
||||
export const STAT_BONUSES: Record<Stat, { perLevel: number; isPct: boolean }> = {
|
||||
[Stat.MaxHp]: { perLevel: 20, isPct: false },
|
||||
[Stat.HpRegen]: { perLevel: 1, isPct: false },
|
||||
[Stat.MovementSpeed]: { perLevel: 0.05, isPct: true },
|
||||
[Stat.BulletDamage]: { perLevel: 0.10, isPct: true },
|
||||
[Stat.BulletSpeed]: { perLevel: 0.08, isPct: true },
|
||||
[Stat.ReloadSpeed]: { perLevel: 0.10, isPct: true },
|
||||
[Stat.RecoilStability]: { perLevel: 0.10, isPct: true }, // -10% recoil per level
|
||||
[Stat.CritChance]: { perLevel: 0.03, isPct: true },
|
||||
[Stat.CritDamage]: { perLevel: 0.10, isPct: true },
|
||||
};
|
||||
|
||||
/** Base crit */
|
||||
export const BASE_CRIT_MULTIPLIER = 1.5; // 150%
|
||||
|
||||
/** XP rewards */
|
||||
export const XP_PER_DAMAGE = 1;
|
||||
export const XP_KILL_PCT = 0.5; // 50% of victim's held XP
|
||||
|
||||
/** Collision */
|
||||
export const SPATIAL_CELL_SIZE = 100;
|
||||
|
||||
/** Network */
|
||||
export const INPUT_RATE_HZ = 60;
|
||||
export const INPUT_MS = 1000 / INPUT_RATE_HZ;
|
||||
|
||||
/** Validation tolerances */
|
||||
export const POS_DRIFT_THRESHOLD = 5; // correct if drift > 5 units
|
||||
export const AIM_ANGLE_TOLERANCE = 0.1; // radians
|
||||
export const RECOIL_TOLERANCE = 0.15; // radians
|
||||
|
||||
/** Colors */
|
||||
export const COLORS = {
|
||||
self: '#3498db', // blue
|
||||
enemy: '#e74c3c', // red
|
||||
bullet: '#f1c40f', // yellow
|
||||
bulletCrit: '#e74c3c', // red outline for crit
|
||||
xpOrb: '#2ecc71', // green
|
||||
hpBar: '#2ecc71',
|
||||
hpBarBg: '#2c3e50',
|
||||
nameTag: '#ecf0f1',
|
||||
uiBg: 'rgba(0,0,0,0.6)',
|
||||
uiBorder: '#34495e',
|
||||
arenaBg: '#1a1a2e',
|
||||
arenaGrid: '#16213e',
|
||||
wall: '#7f8c8d',
|
||||
crate: '#8b4513',
|
||||
text: '#ecf0f1',
|
||||
} as const;
|
||||
|
||||
/** Gun categories (for class branch unlocking) */
|
||||
export enum GunCategory {
|
||||
Pistol = 0,
|
||||
Rifle = 1,
|
||||
Shotgun = 2,
|
||||
Sniper = 3,
|
||||
SMG = 4,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// GunCircle.io — Shared Package Exports
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export * from './constants.js';
|
||||
export * from './types.js';
|
||||
export * from './math.js';
|
||||
export * from './schema.js';
|
||||
@@ -0,0 +1,222 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// GunCircle.io — Math Utilities
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
import type { Vec2 } from './types.js';
|
||||
|
||||
// ─── Vector Operations ───────────────────────────────────────────────────────
|
||||
|
||||
export const vec2 = {
|
||||
zero: (): Vec2 => ({ x: 0, y: 0 }),
|
||||
|
||||
fromAngle: (angle: number, magnitude: number = 1): Vec2 => ({
|
||||
x: Math.cos(angle) * magnitude,
|
||||
y: Math.sin(angle) * magnitude,
|
||||
}),
|
||||
|
||||
add: (a: Vec2, b: Vec2): Vec2 => ({ x: a.x + b.x, y: a.y + b.y }),
|
||||
sub: (a: Vec2, b: Vec2): Vec2 => ({ x: a.x - b.x, y: a.y - b.y }),
|
||||
mul: (v: Vec2, s: number): Vec2 => ({ x: v.x * s, y: v.y * s }),
|
||||
div: (v: Vec2, s: number): Vec2 => ({ x: v.x / s, y: v.y / s }),
|
||||
|
||||
len: (v: Vec2): number => Math.sqrt(v.x * v.x + v.y * v.y),
|
||||
lenSq: (v: Vec2): number => v.x * v.x + v.y * v.y,
|
||||
|
||||
normalize: (v: Vec2): Vec2 => {
|
||||
const l = Math.sqrt(v.x * v.x + v.y * v.y);
|
||||
return l === 0 ? { x: 0, y: 0 } : { x: v.x / l, y: v.y / l };
|
||||
},
|
||||
|
||||
dot: (a: Vec2, b: Vec2): number => a.x * b.x + a.y * b.y,
|
||||
|
||||
dist: (a: Vec2, b: Vec2): number => {
|
||||
const dx = a.x - b.x;
|
||||
const dy = a.y - b.y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
},
|
||||
|
||||
distSq: (a: Vec2, b: Vec2): number => {
|
||||
const dx = a.x - b.x;
|
||||
const dy = a.y - b.y;
|
||||
return dx * dx + dy * dy;
|
||||
},
|
||||
|
||||
clamp: (v: Vec2, maxLen: number): Vec2 => {
|
||||
const l = Math.sqrt(v.x * v.x + v.y * v.y);
|
||||
if (l <= maxLen) return v;
|
||||
const scale = maxLen / l;
|
||||
return { x: v.x * scale, y: v.y * scale };
|
||||
},
|
||||
|
||||
angle: (v: Vec2): number => Math.atan2(v.y, v.x),
|
||||
angleBetween: (a: Vec2, b: Vec2): number => Math.atan2(b.y - a.y, b.x - a.x),
|
||||
|
||||
lerp: (a: Vec2, b: Vec2, t: number): Vec2 => ({
|
||||
x: a.x + (b.x - a.x) * t,
|
||||
y: a.y + (b.y - a.y) * t,
|
||||
}),
|
||||
|
||||
copy: (v: Vec2): Vec2 => ({ x: v.x, y: v.y }),
|
||||
|
||||
equals: (a: Vec2, b: Vec2, eps: number = 0.001): boolean =>
|
||||
Math.abs(a.x - b.x) < eps && Math.abs(a.y - b.y) < eps,
|
||||
};
|
||||
|
||||
// ─── Scalar Utilities ────────────────────────────────────────────────────────
|
||||
|
||||
export const lerp = (a: number, b: number, t: number): number =>
|
||||
a + (b - a) * t;
|
||||
|
||||
export const lerpAngle = (a: number, b: number, t: number): number => {
|
||||
let diff = b - a;
|
||||
while (diff > Math.PI) diff -= Math.PI * 2;
|
||||
while (diff < -Math.PI) diff += Math.PI * 2;
|
||||
return a + diff * t;
|
||||
};
|
||||
|
||||
export const clamp = (v: number, min: number, max: number): number =>
|
||||
Math.max(min, Math.min(max, v));
|
||||
|
||||
export const clamp01 = (v: number): number => clamp(v, 0, 1);
|
||||
|
||||
export const remap = (
|
||||
v: number,
|
||||
inMin: number,
|
||||
inMax: number,
|
||||
outMin: number,
|
||||
outMax: number
|
||||
): number => outMin + ((v - inMin) / (inMax - inMin)) * (outMax - outMin);
|
||||
|
||||
/** Return random float in [min, max) */
|
||||
export const randFloat = (min: number, max: number): number =>
|
||||
min + Math.random() * (max - min);
|
||||
|
||||
/** Return random int in [min, max) */
|
||||
export const randInt = (min: number, max: number): number =>
|
||||
Math.floor(randFloat(min, max));
|
||||
|
||||
/** Return 1 or -1 randomly */
|
||||
export const randSign = (): number => (Math.random() < 0.5 ? 1 : -1);
|
||||
|
||||
/** Check if circle a overlaps circle b */
|
||||
export const circleOverlap = (
|
||||
ax: number, ay: number, ar: number,
|
||||
bx: number, by: number, br: number
|
||||
): boolean => {
|
||||
const dx = ax - bx;
|
||||
const dy = ay - by;
|
||||
const r = ar + br;
|
||||
return dx * dx + dy * dy < r * r;
|
||||
};
|
||||
|
||||
/** Solve circle-circle collision: push a out of b. Returns correction vector for a. */
|
||||
export const circleCollisionResolve = (
|
||||
ax: number, ay: number, ar: number,
|
||||
bx: number, by: number, br: number
|
||||
): Vec2 | null => {
|
||||
const dx = ax - bx;
|
||||
const dy = ay - by;
|
||||
const distSq = dx * dx + dy * dy;
|
||||
const r = ar + br;
|
||||
if (distSq >= r * r || distSq === 0) return null;
|
||||
const dist = Math.sqrt(distSq);
|
||||
const overlap = r - dist;
|
||||
return {
|
||||
x: (dx / dist) * overlap,
|
||||
y: (dy / dist) * overlap,
|
||||
};
|
||||
};
|
||||
|
||||
// ─── Spatial Hash Grid ───────────────────────────────────────────────────────
|
||||
|
||||
export class SpatialHashGrid<T extends { x: number; y: number; id: number }> {
|
||||
private cells = new Map<string, T[]>();
|
||||
private itemToCell = new Map<number, string>();
|
||||
|
||||
constructor(private cellSize: number) {}
|
||||
|
||||
private hash(cx: number, cy: number): string {
|
||||
return `${cx},${cy}`;
|
||||
}
|
||||
|
||||
insert(item: T): void {
|
||||
const cx = Math.floor(item.x / this.cellSize);
|
||||
const cy = Math.floor(item.y / this.cellSize);
|
||||
const key = this.hash(cx, cy);
|
||||
|
||||
let cell = this.cells.get(key);
|
||||
if (!cell) {
|
||||
cell = [];
|
||||
this.cells.set(key, cell);
|
||||
}
|
||||
cell.push(item);
|
||||
this.itemToCell.set(item.id, key);
|
||||
}
|
||||
|
||||
remove(item: T): void {
|
||||
const key = this.itemToCell.get(item.id);
|
||||
if (!key) return;
|
||||
const cell = this.cells.get(key);
|
||||
if (!cell) return;
|
||||
const idx = cell.findIndex((i) => i.id === item.id);
|
||||
if (idx >= 0) cell.splice(idx, 1);
|
||||
this.itemToCell.delete(item.id);
|
||||
}
|
||||
|
||||
update(item: T): void {
|
||||
this.remove(item);
|
||||
this.insert(item);
|
||||
}
|
||||
|
||||
query(x: number, y: number, radius: number): T[] {
|
||||
const results: T[] = [];
|
||||
const minCx = Math.floor((x - radius) / this.cellSize);
|
||||
const maxCx = Math.floor((x + radius) / this.cellSize);
|
||||
const minCy = Math.floor((y - radius) / this.cellSize);
|
||||
const maxCy = Math.floor((y + radius) / this.cellSize);
|
||||
const rSq = radius * radius;
|
||||
|
||||
for (let cx = minCx; cx <= maxCx; cx++) {
|
||||
for (let cy = minCy; cy <= maxCy; cy++) {
|
||||
const key = this.hash(cx, cy);
|
||||
const cell = this.cells.get(key);
|
||||
if (!cell) continue;
|
||||
for (const item of cell) {
|
||||
const dx = item.x - x;
|
||||
const dy = item.y - y;
|
||||
if (dx * dx + dy * dy <= rSq) {
|
||||
results.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
queryRect(x: number, y: number, w: number, h: number): T[] {
|
||||
const results: T[] = [];
|
||||
const minCx = Math.floor(x / this.cellSize);
|
||||
const maxCx = Math.floor((x + w) / this.cellSize);
|
||||
const minCy = Math.floor(y / this.cellSize);
|
||||
const maxCy = Math.floor((y + h) / this.cellSize);
|
||||
|
||||
for (let cx = minCx; cx <= maxCx; cx++) {
|
||||
for (let cy = minCy; cy <= maxCy; cy++) {
|
||||
const key = this.hash(cx, cy);
|
||||
const cell = this.cells.get(key);
|
||||
if (!cell) continue;
|
||||
for (const item of cell) {
|
||||
if (item.x >= x && item.x <= x + w && item.y >= y && item.y <= y + h) {
|
||||
results.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.cells.clear();
|
||||
this.itemToCell.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// GunCircle.io — Colyseus Schema Definitions
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
import { Schema, type, MapSchema, ArraySchema } from '@colyseus/schema';
|
||||
|
||||
// ─── Player State ────────────────────────────────────────────────────────────
|
||||
|
||||
export class Player extends Schema {
|
||||
@type('float32') x = 0;
|
||||
@type('float32') y = 0;
|
||||
/** Current aim angle in radians */
|
||||
@type('float32') angle = 0;
|
||||
/** Current HP */
|
||||
@type('float32') hp = 100;
|
||||
@type('float32') maxHp = 100;
|
||||
/** 1-45 */
|
||||
@type('uint8') level = 1;
|
||||
@type('uint16') xp = 0;
|
||||
@type('uint16') xpToNext = 50;
|
||||
/** Index into gun configs */
|
||||
@type('uint8') gunType = 0;
|
||||
/** Index into class branches */
|
||||
@type('uint8') classType = 0;
|
||||
/** Number of kills */
|
||||
@type('uint8') score = 0;
|
||||
/** Player name, max 16 chars */
|
||||
@type('string') name = 'Player';
|
||||
/** Cosmetic skin index */
|
||||
@type('uint8') skinId = 0;
|
||||
/** Current recoil offset in radians */
|
||||
@type('float32') recoilOffset = 0;
|
||||
/** Current ammo */
|
||||
@type('uint8') ammo = 0;
|
||||
/** Max ammo */
|
||||
@type('uint8') maxAmmo = 0;
|
||||
/** Is reloading */
|
||||
@type('boolean') isReloading = false;
|
||||
/** Alive */
|
||||
@type('boolean') alive = true;
|
||||
/** Radius */
|
||||
@type('float32') radius = 20;
|
||||
/** Upgrade points available */
|
||||
@type('uint8') upgradePoints = 0;
|
||||
/** Stat levels: [maxHp, hpRegen, moveSpeed, bulletDmg, bulletSpd, reloadSpd, recoilStab, critChance, critDmg] */
|
||||
@type(['uint8']) stats = new ArraySchema<number>(0, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||
/** Velocity x (for smoother client reconciliation) */
|
||||
@type('float32') vx = 0;
|
||||
/** Velocity y */
|
||||
@type('float32') vy = 0;
|
||||
}
|
||||
|
||||
// ─── Bullet State ────────────────────────────────────────────────────────────
|
||||
|
||||
export class Bullet extends Schema {
|
||||
@type('float32') x = 0;
|
||||
@type('float32') y = 0;
|
||||
/** Travel angle in radians */
|
||||
@type('float32') angle = 0;
|
||||
/** Owner session ID */
|
||||
@type('string') ownerId = '';
|
||||
/** Damage */
|
||||
@type('float32') damage = 10;
|
||||
/** Penetration remaining */
|
||||
@type('uint8') penetration = 1;
|
||||
/** Bullet type index */
|
||||
@type('uint8') bulletType = 0;
|
||||
/** Is critical hit */
|
||||
@type('boolean') isCritical = false;
|
||||
/** Velocity x */
|
||||
@type('float32') vx = 0;
|
||||
/** Velocity y */
|
||||
@type('float32') vy = 0;
|
||||
/** Bullet radius/size */
|
||||
@type('float32') size = 4;
|
||||
/** Bullet color */
|
||||
@type('string') color = '#f1c40f';
|
||||
}
|
||||
|
||||
// ─── XP Orb State ────────────────────────────────────────────────────────────
|
||||
|
||||
export class XPOrb extends Schema {
|
||||
@type('float32') x = 0;
|
||||
@type('float32') y = 0;
|
||||
/** XP value */
|
||||
@type('uint16') value = 10;
|
||||
/** Seconds remaining */
|
||||
@type('uint8') lifetime = 30;
|
||||
}
|
||||
|
||||
// ─── Arena Obstacle State ────────────────────────────────────────────────────
|
||||
|
||||
export class Obstacle extends Schema {
|
||||
@type('float32') x = 0;
|
||||
@type('float32') y = 0;
|
||||
@type('float32') width = 50;
|
||||
@type('float32') height = 50;
|
||||
@type('uint8') type = 0;
|
||||
}
|
||||
|
||||
// ─── Leaderboard Entry ───────────────────────────────────────────────────────
|
||||
|
||||
export class LeaderboardEntry extends Schema {
|
||||
@type('string') name = '';
|
||||
@type('uint8') level = 1;
|
||||
@type('uint8') score = 0;
|
||||
@type('uint16') xp = 0;
|
||||
}
|
||||
|
||||
// ─── Room State ──────────────────────────────────────────────────────────────
|
||||
|
||||
export class RoomState extends Schema {
|
||||
@type({ map: Player }) players = new MapSchema<Player>();
|
||||
@type({ map: Bullet }) bullets = new MapSchema<Bullet>();
|
||||
@type({ map: XPOrb }) xpOrbs = new MapSchema<XPOrb>();
|
||||
@type({ map: Obstacle }) obstacles = new MapSchema<Obstacle>();
|
||||
@type('uint32') tick = 0;
|
||||
@type([LeaderboardEntry]) leaderboard = new ArraySchema<LeaderboardEntry>();
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// GunCircle.io — Shared Type Definitions
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
import type { Stat, GunCategory } from './constants.js';
|
||||
|
||||
// ─── Vector ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface Vec2 {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
// ─── Gun Configuration ───────────────────────────────────────────────────────
|
||||
|
||||
export interface GunConfig {
|
||||
/** Unique identifier */
|
||||
id: string;
|
||||
/** Display name */
|
||||
name: string;
|
||||
/** Gun category for class branch unlocking */
|
||||
category: GunCategory;
|
||||
/** Damage per bullet */
|
||||
damage: number;
|
||||
/** Shots per second */
|
||||
fireRate: number;
|
||||
/** Bullet speed (units/sec) */
|
||||
bulletSpeed: number;
|
||||
/** Number of bullets per shot (1 = single, >1 = shotgun) */
|
||||
pelletCount: number;
|
||||
/** Spread angle in radians (0 = no spread) */
|
||||
spread: number;
|
||||
/** Gun recoil rotation offset in radians (random left/right) */
|
||||
recoilOffset: number;
|
||||
/** Time to recover from recoil (ms) */
|
||||
recoilRecoveryMs: number;
|
||||
/** Backward impulse on player per shot */
|
||||
kickbackForce: number;
|
||||
/** Max ammo before reload */
|
||||
ammoMax: number;
|
||||
/** Reload time in ms */
|
||||
reloadTimeMs: number;
|
||||
/** Bullet radius */
|
||||
bulletSize: number;
|
||||
/** How many entities bullet can pass through */
|
||||
penetration: number;
|
||||
/** Bullet lifetime in ms */
|
||||
bulletLifetimeMs: number;
|
||||
/** Color for bullets */
|
||||
bulletColor: string;
|
||||
}
|
||||
|
||||
// ─── Class Branch ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ClassBranch {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
/** Level required to choose this branch */
|
||||
levelRequired: number;
|
||||
/** Parent branch ID (null for root) */
|
||||
parentId: string | null;
|
||||
/** Gun categories unlocked by this branch */
|
||||
unlocksCategories: GunCategory[];
|
||||
/** Passive stat bonuses (additive) */
|
||||
passiveBonuses: Partial<Record<Stat, number>>;
|
||||
}
|
||||
|
||||
// ─── Input ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface PlayerInput {
|
||||
/** Client frame counter */
|
||||
seq: number;
|
||||
/** Movement angle in radians, -1 if no movement */
|
||||
moveAngle: number;
|
||||
/** Aim angle in radians */
|
||||
aimAngle: number;
|
||||
/** True if mouse down / shooting */
|
||||
isShooting: boolean;
|
||||
/** Stat index or branch choice when leveling up */
|
||||
upgradeChoice?: number;
|
||||
}
|
||||
|
||||
// ─── Arena Obstacle ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface ArenaObstacle {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
type: ObstacleType;
|
||||
}
|
||||
|
||||
export enum ObstacleType {
|
||||
IndestructibleWall = 0,
|
||||
DestructibleCrate = 1,
|
||||
SlowZone = 2,
|
||||
Cover = 3,
|
||||
}
|
||||
|
||||
// ─── Skin ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface Skin {
|
||||
id: number;
|
||||
name: string;
|
||||
type: SkinType;
|
||||
/** Color/Pattern value */
|
||||
value: string;
|
||||
/** Price in coins (0 = free/achievement) */
|
||||
price: number;
|
||||
/** Achievement requirement (null = purchasable) */
|
||||
achievement?: string;
|
||||
}
|
||||
|
||||
export enum SkinType {
|
||||
CircleColor = 0,
|
||||
GunSkin = 1,
|
||||
NameTagStyle = 2,
|
||||
}
|
||||
|
||||
// ─── XP Orb ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface XpOrbData {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
value: number;
|
||||
lifetime: number; // seconds remaining
|
||||
}
|
||||
|
||||
// ─── Leaderboard Entry ───────────────────────────────────────────────────────
|
||||
|
||||
export interface LeaderboardEntry {
|
||||
name: string;
|
||||
level: number;
|
||||
score: number; // kills
|
||||
xp: number;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["./src"]
|
||||
}
|
||||
Reference in New Issue
Block a user