163 lines
5.1 KiB
TypeScript
163 lines
5.1 KiB
TypeScript
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// GunCircle.io — Gun System
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
import { readFileSync } from 'fs';
|
|
import { dirname, join } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import {
|
|
vec2,
|
|
type Vec2,
|
|
type GunConfig,
|
|
Bullet,
|
|
GUN_BARREL_LENGTH,
|
|
STAT_COUNT,
|
|
Stat,
|
|
randSign,
|
|
} from '@guncircle/shared';
|
|
|
|
// ─── Gun Config Container ────────────────────────────────────────────────────
|
|
|
|
interface GunsJson {
|
|
guns: GunConfig[];
|
|
}
|
|
|
|
// ─── GunSystem ───────────────────────────────────────────────────────────────
|
|
|
|
export class GunSystem {
|
|
private guns: GunConfig[] = [];
|
|
|
|
constructor() {
|
|
this.loadConfigs();
|
|
}
|
|
|
|
private loadConfigs(): void {
|
|
try {
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const configPath = join(__dirname, '../config/guns.json');
|
|
const raw = readFileSync(configPath, 'utf-8');
|
|
const parsed: GunsJson = JSON.parse(raw) as GunsJson;
|
|
this.guns = parsed.guns ?? [];
|
|
} catch {
|
|
this.guns = [];
|
|
}
|
|
}
|
|
|
|
/** Number of gun configs loaded */
|
|
getGunCount(): number {
|
|
return this.guns.length;
|
|
}
|
|
|
|
/** Get gun config by 0-based index (used for gunType field) */
|
|
getGunConfig(index: number): GunConfig | undefined {
|
|
if (index < 0 || index >= this.guns.length) return undefined;
|
|
return this.guns[index];
|
|
}
|
|
|
|
/** Get gun config by string id */
|
|
getGunById(id: string): GunConfig | undefined {
|
|
return this.guns.find((g) => g.id === id);
|
|
}
|
|
|
|
/** Get the index of a gun by its string id */
|
|
getGunIndexById(id: string): number {
|
|
return this.guns.findIndex((g) => g.id === id);
|
|
}
|
|
|
|
/** Get all loaded gun configs */
|
|
getAllGuns(): readonly GunConfig[] {
|
|
return this.guns;
|
|
}
|
|
|
|
/**
|
|
* Calculate recoil offset when firing.
|
|
* Returns signed offset: ±gun.recoilOffset * (1 - recoilStabilityStat * 0.1)
|
|
*/
|
|
calculateRecoil(gun: GunConfig, recoilStabilityStat: number): number {
|
|
const stabilityFactor: number = 1 - recoilStabilityStat * 0.1;
|
|
const clampedStability: number = Math.max(0, stabilityFactor);
|
|
const signedOffset: number = randSign() * gun.recoilOffset * clampedStability;
|
|
return signedOffset;
|
|
}
|
|
|
|
/**
|
|
* Calculate kickback velocity vector applied to player on shoot.
|
|
* Returns vector in direction opposite to aim.
|
|
*/
|
|
calculateKickback(gun: GunConfig, aimAngle: number): Vec2 {
|
|
const aimVector: Vec2 = vec2.fromAngle(aimAngle, 1);
|
|
return vec2.mul(aimVector, -gun.kickbackForce);
|
|
}
|
|
|
|
/**
|
|
* Get the world-space position where a bullet should spawn (barrel tip).
|
|
*/
|
|
getBulletSpawnPos(
|
|
playerX: number,
|
|
playerY: number,
|
|
aimAngle: number,
|
|
recoilOffset: number
|
|
): Vec2 {
|
|
const totalAngle: number = aimAngle + recoilOffset;
|
|
const barrelOffset: Vec2 = vec2.fromAngle(totalAngle, GUN_BARREL_LENGTH);
|
|
return {
|
|
x: playerX + barrelOffset.x,
|
|
y: playerY + barrelOffset.y,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a Bullet schema instance from gun parameters.
|
|
*/
|
|
createBullet(
|
|
gun: GunConfig,
|
|
ownerId: string,
|
|
spawnPos: Vec2,
|
|
angle: number,
|
|
damage: number,
|
|
isCrit: boolean
|
|
): Bullet {
|
|
const bullet: Bullet = new Bullet();
|
|
bullet.x = spawnPos.x;
|
|
bullet.y = spawnPos.y;
|
|
bullet.angle = angle;
|
|
bullet.ownerId = ownerId;
|
|
bullet.damage = damage;
|
|
bullet.penetration = gun.penetration;
|
|
bullet.bulletType = this.getGunIndexById(gun.id);
|
|
bullet.isCritical = isCrit;
|
|
bullet.vx = Math.cos(angle) * gun.bulletSpeed;
|
|
bullet.vy = Math.sin(angle) * gun.bulletSpeed;
|
|
bullet.size = gun.bulletSize;
|
|
bullet.color = gun.bulletColor;
|
|
return bullet;
|
|
}
|
|
|
|
/**
|
|
* Calculate cooldown between shots in ms, adjusted by reload speed stat.
|
|
* cooldown = 1000 / fireRate / (1 + reloadSpdStat * 0.1)
|
|
*/
|
|
calculateCooldown(gun: GunConfig, reloadSpdStat: number): number {
|
|
const baseCooldown: number = 1000 / gun.fireRate;
|
|
const reloadBonus: number = 1 + reloadSpdStat * 0.1;
|
|
return baseCooldown / reloadBonus;
|
|
}
|
|
|
|
/**
|
|
* Calculate reload time in ms, adjusted by reload speed stat.
|
|
*/
|
|
calculateReloadTime(gun: GunConfig, reloadSpdStat: number): number {
|
|
const reloadBonus: number = 1 + reloadSpdStat * 0.1;
|
|
return gun.reloadTimeMs / reloadBonus;
|
|
}
|
|
|
|
/**
|
|
* Calculate damage per pellet, accounting for shotgun pellet count distribution.
|
|
* Total damage is split evenly across pellets.
|
|
*/
|
|
calculatePelletDamage(baseDamage: number, pelletCount: number): number {
|
|
if (pelletCount <= 0) return baseDamage;
|
|
return baseDamage / pelletCount;
|
|
}
|
|
}
|