├── .gitignore ├── ship_stats.xlsx ├── blender ├── ships.blend ├── fbx │ ├── body_0.fbx │ ├── body_1.fbx │ ├── scout.fbx │ ├── wing_0.fbx │ ├── wing_1.fbx │ ├── bullet_0.fbx │ ├── bullet_1.fbx │ ├── crystal.fbx │ ├── engine_0.fbx │ ├── engine_1.fbx │ ├── weapon_0.fbx │ ├── weapon_1.fbx │ ├── enemy_blimp.fbx │ ├── enemies │ │ ├── Blimp.fbx │ │ ├── Fang.fbx │ │ ├── Scout.fbx │ │ ├── Tank.fbx │ │ ├── Blaster.fbx │ │ ├── Bomber.fbx │ │ ├── Hunter.fbx │ │ ├── Speeder.fbx │ │ ├── Tracker.fbx │ │ ├── Asteroid1.fbx │ │ ├── Asteroid2.fbx │ │ ├── Asteroid3.fbx │ │ └── boss │ │ │ ├── Eagle.fbx │ │ │ ├── Ravager.fbx │ │ │ └── WingedDevil.fbx │ ├── enemy_blaster.fbx │ ├── enemy_bomber.fbx │ ├── enemy_speeder.fbx │ ├── enemy_tracker.fbx │ ├── projectile_0.fbx │ ├── ships │ │ ├── scout1.fbx │ │ ├── scout2.fbx │ │ ├── scout3.fbx │ │ ├── scout4.fbx │ │ ├── scout5.fbx │ │ ├── defender1.fbx │ │ ├── defender2.fbx │ │ ├── defender3.fbx │ │ ├── defender4.fbx │ │ ├── defender5.fbx │ │ ├── explorer1.fbx │ │ ├── explorer2.fbx │ │ ├── explorer3.fbx │ │ ├── explorer4.fbx │ │ ├── explorer5.fbx │ │ ├── fighter1.fbx │ │ ├── fighter2.fbx │ │ ├── fighter3.fbx │ │ ├── fighter4.fbx │ │ ├── fighter5.fbx │ │ ├── gunship1.fbx │ │ ├── gunship2.fbx │ │ ├── gunship3.fbx │ │ ├── gunship4.fbx │ │ └── gunship5.fbx │ ├── bullet │ │ ├── missile.fbx │ │ └── torpedo.fbx │ ├── special_scatter.fbx │ ├── special_shotgun.fbx │ ├── weapon_basic_0.fbx │ ├── weapon_blaster_0.fbx │ └── special_emergency_brake.fbx ├── ships.blend1 ├── projectile.blend ├── enemy_pallette.png ├── materials │ ├── camo1.png │ ├── camo2.png │ ├── royal_fleet1.png │ ├── royal_fleet2.png │ ├── royal_fleet3.png │ ├── bone_brigade1.png │ ├── bone_brigade2.png │ ├── bone_brigade3.png │ ├── earth_defense1.png │ ├── cindertron_recruit1.png │ ├── cindertron_recruit2.png │ └── cindertron_recruit3.png ├── projectile_uv.png ├── ship_material_1.png └── projectile_uv_01.png ├── src ├── models │ ├── messages │ │ ├── index.ts │ │ ├── Statistics.ts │ │ ├── ShipList.ts │ │ ├── ErrorMessage.ts │ │ └── UnlockMessage.ts │ ├── bosses │ │ ├── index.ts │ │ ├── Disk.ts │ │ ├── WingedDevil.ts │ │ ├── Eagle.ts │ │ └── TheKiller.ts │ ├── primary │ │ ├── index.ts │ │ ├── Primary.ts │ │ ├── Blaster.ts │ │ ├── Missile.ts │ │ ├── Torpedo.ts │ │ ├── EnemyBullet.ts │ │ └── Cannon.ts │ ├── special │ │ ├── WeaponCharge.ts │ │ ├── ShieldRecharge.ts │ │ ├── HyperSpeed.ts │ │ ├── SpecialSystem.ts │ │ ├── Invisibility.ts │ │ ├── Thrusters.ts │ │ ├── index.ts │ │ ├── RammingShield.ts │ │ ├── ForceShield.ts │ │ ├── MissileBarage.ts │ │ ├── Shotgun.ts │ │ ├── ScatterShot.ts │ │ ├── Bomb.ts │ │ └── MegaBomb.ts │ ├── index.ts │ ├── enemies │ │ ├── index.ts │ │ ├── Scout.ts │ │ ├── Tracker.ts │ │ ├── Hunter.ts │ │ ├── Speeder.ts │ │ ├── Asteroid.ts │ │ ├── Blimp.ts │ │ ├── Bomber.ts │ │ ├── Fang.ts │ │ ├── Blaster.ts │ │ └── Tank.ts │ ├── TempUpgrade.ts │ ├── states │ │ ├── ShipBuilderState.ts │ │ └── GameState.ts │ ├── UnlockItem.ts │ ├── Bullet.ts │ ├── Drop.ts │ ├── Position.ts │ ├── Enemy.ts │ ├── Entity.ts │ └── Account.ts ├── spawner │ ├── Spawn.ts │ ├── TimedPosition.ts │ ├── patterns │ │ ├── index.ts │ │ ├── VerticalLine.ts │ │ ├── DiagonalLine.ts │ │ ├── SideLine.ts │ │ ├── DoubleVerticalLine.ts │ │ ├── SideDiagonalLine.ts │ │ ├── AlternatingSide.ts │ │ ├── TopTriangle.ts │ │ ├── HorizontalLine.ts │ │ ├── TripleVerticalLine.ts │ │ └── BothSideLine.ts │ └── Pattern.ts ├── helpers │ ├── Bounds.ts │ ├── JWTHelper.ts │ ├── UsernameGenerator.ts │ ├── AccountHelper.ts │ ├── CollisionHelper.ts │ └── ShipHelper.ts ├── behaviours │ ├── drop │ │ ├── DestroyedBehaviour.ts │ │ ├── DespawnAfterTime.ts │ │ └── MoveToLocationPath.ts │ ├── player │ │ ├── DestroyedBehaviour.ts │ │ ├── CollectDrop.ts │ │ ├── ShieldRechargeBehaviour.ts │ │ ├── CollidesWithDrop.ts │ │ ├── TakesDamageBehaviour.ts │ │ ├── CollidesWithEnemyBullet.ts │ │ ├── CollidesWithEnemy.ts │ │ ├── InputBehaviour.ts │ │ ├── PrimaryAttackBehaviour.ts │ │ └── SpecialAttackBehaviour.ts │ ├── enemy │ │ ├── DestroyedBehaviour.ts │ │ ├── movement │ │ │ ├── RotateInCircle.ts │ │ │ ├── LoopingPath.ts │ │ │ ├── TargetPlayerStartPath.ts │ │ │ ├── StraightLinePath.ts │ │ │ ├── MoveToLocationPath.ts │ │ │ ├── ClosestPlayerAtStartPath.ts │ │ │ ├── ClosestPlayerPath.ts │ │ │ ├── StraightAnglePath.ts │ │ │ ├── WobblePath.ts │ │ │ ├── MoveToLocationThenRotatePath.ts │ │ │ └── SimpleFlockingPath.ts │ │ ├── FiresBulletBehaviour.ts │ │ ├── TakesDamageBehaviour.ts │ │ └── CollidesWithShipBullet.ts │ ├── bullet │ │ ├── DestroyedBehaviour.ts │ │ ├── StraightLineDownPath.ts │ │ ├── StraightLineUpPath.ts │ │ ├── StraightAnglePath.ts │ │ ├── ExplodeBehaviour.ts │ │ ├── ClosestEnemyPath.ts │ │ └── MissilePath.ts │ ├── boss │ │ └── DropReward.ts │ └── behaviour.ts ├── Database.ts ├── Constants.ts ├── Material.ts ├── Special.ts ├── index.ts └── rooms │ ├── GameRoom.ts │ ├── ShipBuilderRoom.ts │ └── MatchMakerRoom.ts ├── .eslintrc.js ├── test ├── UsernameTest.ts ├── TestTrackKills.ts ├── PatternImage.ts ├── TestExplosionDistance.ts └── ServerTest.ts ├── schema-unity ├── Drop.cs ├── Account.cs ├── Statistics.cs ├── ShipList.cs ├── Position.cs ├── Enemy.cs ├── Bullet.cs ├── ErrorMessage.cs ├── UnlockMessage.cs ├── TempUpgrade.cs ├── UnlockItem.cs ├── ShipBuilderState.cs ├── Entity.cs ├── GameState.cs └── Ship.cs ├── README.md ├── tsconfig.json ├── scripts └── stat_parser.js ├── ecosystem.config.js ├── ship_stats_raw.txt └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | db 3 | build 4 | **/~* 5 | **/*.blend1 6 | -------------------------------------------------------------------------------- /ship_stats.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/ship_stats.xlsx -------------------------------------------------------------------------------- /blender/ships.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/ships.blend -------------------------------------------------------------------------------- /blender/fbx/body_0.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/body_0.fbx -------------------------------------------------------------------------------- /blender/fbx/body_1.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/body_1.fbx -------------------------------------------------------------------------------- /blender/fbx/scout.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/scout.fbx -------------------------------------------------------------------------------- /blender/fbx/wing_0.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/wing_0.fbx -------------------------------------------------------------------------------- /blender/fbx/wing_1.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/wing_1.fbx -------------------------------------------------------------------------------- /blender/ships.blend1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/ships.blend1 -------------------------------------------------------------------------------- /blender/fbx/bullet_0.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/bullet_0.fbx -------------------------------------------------------------------------------- /blender/fbx/bullet_1.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/bullet_1.fbx -------------------------------------------------------------------------------- /blender/fbx/crystal.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/crystal.fbx -------------------------------------------------------------------------------- /blender/fbx/engine_0.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/engine_0.fbx -------------------------------------------------------------------------------- /blender/fbx/engine_1.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/engine_1.fbx -------------------------------------------------------------------------------- /blender/fbx/weapon_0.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/weapon_0.fbx -------------------------------------------------------------------------------- /blender/fbx/weapon_1.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/weapon_1.fbx -------------------------------------------------------------------------------- /blender/projectile.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/projectile.blend -------------------------------------------------------------------------------- /blender/enemy_pallette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/enemy_pallette.png -------------------------------------------------------------------------------- /blender/fbx/enemy_blimp.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemy_blimp.fbx -------------------------------------------------------------------------------- /blender/materials/camo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/materials/camo1.png -------------------------------------------------------------------------------- /blender/materials/camo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/materials/camo2.png -------------------------------------------------------------------------------- /blender/projectile_uv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/projectile_uv.png -------------------------------------------------------------------------------- /blender/ship_material_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/ship_material_1.png -------------------------------------------------------------------------------- /blender/fbx/enemies/Blimp.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemies/Blimp.fbx -------------------------------------------------------------------------------- /blender/fbx/enemies/Fang.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemies/Fang.fbx -------------------------------------------------------------------------------- /blender/fbx/enemies/Scout.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemies/Scout.fbx -------------------------------------------------------------------------------- /blender/fbx/enemies/Tank.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemies/Tank.fbx -------------------------------------------------------------------------------- /blender/fbx/enemy_blaster.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemy_blaster.fbx -------------------------------------------------------------------------------- /blender/fbx/enemy_bomber.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemy_bomber.fbx -------------------------------------------------------------------------------- /blender/fbx/enemy_speeder.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemy_speeder.fbx -------------------------------------------------------------------------------- /blender/fbx/enemy_tracker.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemy_tracker.fbx -------------------------------------------------------------------------------- /blender/fbx/projectile_0.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/projectile_0.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/scout1.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/scout1.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/scout2.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/scout2.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/scout3.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/scout3.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/scout4.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/scout4.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/scout5.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/scout5.fbx -------------------------------------------------------------------------------- /blender/projectile_uv_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/projectile_uv_01.png -------------------------------------------------------------------------------- /blender/fbx/bullet/missile.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/bullet/missile.fbx -------------------------------------------------------------------------------- /blender/fbx/bullet/torpedo.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/bullet/torpedo.fbx -------------------------------------------------------------------------------- /blender/fbx/enemies/Blaster.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemies/Blaster.fbx -------------------------------------------------------------------------------- /blender/fbx/enemies/Bomber.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemies/Bomber.fbx -------------------------------------------------------------------------------- /blender/fbx/enemies/Hunter.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemies/Hunter.fbx -------------------------------------------------------------------------------- /blender/fbx/enemies/Speeder.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemies/Speeder.fbx -------------------------------------------------------------------------------- /blender/fbx/enemies/Tracker.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemies/Tracker.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/defender1.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/defender1.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/defender2.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/defender2.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/defender3.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/defender3.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/defender4.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/defender4.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/defender5.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/defender5.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/explorer1.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/explorer1.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/explorer2.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/explorer2.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/explorer3.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/explorer3.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/explorer4.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/explorer4.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/explorer5.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/explorer5.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/fighter1.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/fighter1.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/fighter2.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/fighter2.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/fighter3.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/fighter3.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/fighter4.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/fighter4.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/fighter5.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/fighter5.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/gunship1.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/gunship1.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/gunship2.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/gunship2.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/gunship3.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/gunship3.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/gunship4.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/gunship4.fbx -------------------------------------------------------------------------------- /blender/fbx/ships/gunship5.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/ships/gunship5.fbx -------------------------------------------------------------------------------- /blender/fbx/special_scatter.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/special_scatter.fbx -------------------------------------------------------------------------------- /blender/fbx/special_shotgun.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/special_shotgun.fbx -------------------------------------------------------------------------------- /blender/fbx/weapon_basic_0.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/weapon_basic_0.fbx -------------------------------------------------------------------------------- /blender/fbx/weapon_blaster_0.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/weapon_blaster_0.fbx -------------------------------------------------------------------------------- /blender/fbx/enemies/Asteroid1.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemies/Asteroid1.fbx -------------------------------------------------------------------------------- /blender/fbx/enemies/Asteroid2.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemies/Asteroid2.fbx -------------------------------------------------------------------------------- /blender/fbx/enemies/Asteroid3.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemies/Asteroid3.fbx -------------------------------------------------------------------------------- /blender/fbx/enemies/boss/Eagle.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemies/boss/Eagle.fbx -------------------------------------------------------------------------------- /blender/materials/royal_fleet1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/materials/royal_fleet1.png -------------------------------------------------------------------------------- /blender/materials/royal_fleet2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/materials/royal_fleet2.png -------------------------------------------------------------------------------- /blender/materials/royal_fleet3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/materials/royal_fleet3.png -------------------------------------------------------------------------------- /blender/fbx/enemies/boss/Ravager.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemies/boss/Ravager.fbx -------------------------------------------------------------------------------- /blender/materials/bone_brigade1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/materials/bone_brigade1.png -------------------------------------------------------------------------------- /blender/materials/bone_brigade2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/materials/bone_brigade2.png -------------------------------------------------------------------------------- /blender/materials/bone_brigade3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/materials/bone_brigade3.png -------------------------------------------------------------------------------- /blender/materials/earth_defense1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/materials/earth_defense1.png -------------------------------------------------------------------------------- /blender/fbx/special_emergency_brake.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/special_emergency_brake.fbx -------------------------------------------------------------------------------- /blender/fbx/enemies/boss/WingedDevil.fbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/fbx/enemies/boss/WingedDevil.fbx -------------------------------------------------------------------------------- /blender/materials/cindertron_recruit1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/materials/cindertron_recruit1.png -------------------------------------------------------------------------------- /blender/materials/cindertron_recruit2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/materials/cindertron_recruit2.png -------------------------------------------------------------------------------- /blender/materials/cindertron_recruit3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/space_shooter_server/master/blender/materials/cindertron_recruit3.png -------------------------------------------------------------------------------- /src/models/messages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ErrorMessage'; 2 | export * from './ShipList'; 3 | export * from './Statistics'; 4 | export * from './UnlockMessage'; 5 | -------------------------------------------------------------------------------- /src/models/bosses/index.ts: -------------------------------------------------------------------------------- 1 | export { Disk } from './Disk'; 2 | export { Eagle } from './Eagle'; 3 | export { TheKiller } from './TheKiller'; 4 | export { WingedDevil } from './WingedDevil'; 5 | -------------------------------------------------------------------------------- /src/models/primary/index.ts: -------------------------------------------------------------------------------- 1 | export { Blaster } from './Blaster'; 2 | export { Cannon } from './Cannon'; 3 | export { Missile } from './Missile'; 4 | export { Primary } from './Primary'; 5 | export { Torpedo } from './Torpedo'; 6 | -------------------------------------------------------------------------------- /src/models/special/WeaponCharge.ts: -------------------------------------------------------------------------------- 1 | import { SpecialSystem } from './SpecialSystem'; 2 | 3 | export class WeaponCharge extends SpecialSystem { 4 | 5 | handleEvent(): void { 6 | this.target.setWeaponCharge(this.amount); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/spawner/Spawn.ts: -------------------------------------------------------------------------------- 1 | import { Enemy } from '../models/Enemy'; 2 | 3 | export class Spawn { 4 | time: number; 5 | enemy: Enemy; 6 | 7 | constructor(time: number, enemy: Enemy ) { 8 | this.time = time; 9 | this.enemy = enemy; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/models/special/ShieldRecharge.ts: -------------------------------------------------------------------------------- 1 | import { SpecialSystem } from './SpecialSystem'; 2 | 3 | export class ShieldRecharge extends SpecialSystem { 4 | 5 | handleEvent(): void { 6 | this.target.shield = Math.min(this.target.shield + this.amount, this.target.maxShield); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/spawner/TimedPosition.ts: -------------------------------------------------------------------------------- 1 | import { Position } from '../models/Position'; 2 | 3 | export class TimedPosition extends Position { 4 | 5 | time: number; 6 | 7 | constructor(time: number, x: number, y: number) { 8 | super(x, y); 9 | this.time = time; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: './tsconfig.json', 5 | }, 6 | plugins: ['@typescript-eslint'], 7 | extends: [ 8 | 'plugin:@typescript-eslint/recommended', 9 | ], 10 | rules: { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/UsernameTest.ts: -------------------------------------------------------------------------------- 1 | import {UsernameGenerator} from '../src/helpers/UsernameGenerator'; 2 | 3 | for(let i = 0; i < 5; i++) { 4 | console.log('Example username: ' + UsernameGenerator.getUsername()); 5 | } 6 | 7 | for(let i = 0; i < 10; i++) { 8 | console.log('ShipName: ' + UsernameGenerator.getShipname()); 9 | } 10 | -------------------------------------------------------------------------------- /src/models/messages/Statistics.ts: -------------------------------------------------------------------------------- 1 | import {Schema, MapSchema, type} from '@colyseus/schema'; 2 | 3 | export class Statistics extends Schema { 4 | 5 | @type({map: "number"}) 6 | stats: MapSchema; 7 | 8 | constructor(stats: any = {}) { 9 | super(); 10 | this.stats = new MapSchema(stats); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/helpers/Bounds.ts: -------------------------------------------------------------------------------- 1 | export class Bounds { 2 | minX: number; 3 | maxX: number; 4 | minY: number; 5 | maxY: number; 6 | 7 | constructor(minX: number, maxX: number, minY: number, maxY: number) { 8 | this.minX = minX; 9 | this.maxX = maxX; 10 | this.minY = minY; 11 | this.maxY = maxY; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /schema-unity/Drop.cs: -------------------------------------------------------------------------------- 1 | // 2 | // THIS FILE HAS BEEN GENERATED AUTOMATICALLY 3 | // DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING 4 | // 5 | // GENERATED USING @colyseus/schema 0.5.33 6 | // 7 | 8 | using Colyseus.Schema; 9 | 10 | public class Drop : Entity { 11 | [Type(6, "string")] 12 | public string modelType = ""; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /schema-unity/Account.cs: -------------------------------------------------------------------------------- 1 | // 2 | // THIS FILE HAS BEEN GENERATED AUTOMATICALLY 3 | // DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING 4 | // 5 | // GENERATED USING @colyseus/schema 0.5.33 6 | // 7 | 8 | using Colyseus.Schema; 9 | 10 | public class Account : Schema { 11 | [Type(0, "string")] 12 | public string username = ""; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/models/messages/ShipList.ts: -------------------------------------------------------------------------------- 1 | import {Schema, MapSchema, type} from '@colyseus/schema'; 2 | import { Ship } from '../Ship'; 3 | 4 | export class ShipList extends Schema { 5 | 6 | @type({map: Ship}) 7 | ships: MapSchema; 8 | 9 | constructor(ships: any = {}) { 10 | super(); 11 | this.ships = new MapSchema(ships); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Space Shooter Server 2 | 3 | A server for a colyseus space shooter game. 4 | The client can be found at https://github.com/stats/space_shooter_client 5 | 6 | ## Requirements 7 | 8 | * node must be installed 9 | * mongo must be installed 10 | 11 | ## Setup 12 | 13 | ``` 14 | mkdir db 15 | mkdir db\development 16 | mkdir build 17 | mkdir build\codegen 18 | ``` 19 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export { Account } from './Account'; 2 | export { Bullet } from './Bullet'; 3 | export { Drop } from './Drop'; 4 | export { Enemy } from './Enemy'; 5 | export { Entity } from './Entity'; 6 | export { Position } from './Position'; 7 | export { Ship } from './Ship'; 8 | export { TempUpgrade } from './TempUpgrade'; 9 | export { UnlockItem } from './UnlockItem'; 10 | -------------------------------------------------------------------------------- /src/models/messages/ErrorMessage.ts: -------------------------------------------------------------------------------- 1 | import {Schema, type} from '@colyseus/schema'; 2 | 3 | export class ErrorMessage extends Schema { 4 | 5 | @type("string") 6 | key: string; 7 | 8 | @type("string") 9 | message: string; 10 | 11 | constructor(message: string, key = "error") { 12 | super(); 13 | this.key = key; 14 | this.message = message; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/behaviours/drop/DestroyedBehaviour.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { Drop } from '../../models'; 3 | 4 | export class DestroyedBehaviour extends Behaviour { 5 | 6 | target: Drop; 7 | 8 | constructor(target: Drop) { 9 | super('destroyed', target); 10 | } 11 | 12 | onEvent(): void { 13 | this.target.$state.removeDrop(this.target); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/TestTrackKills.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { GameState } from "../src/models/GameState"; 3 | import { Enemy } from "../src/models/Enemy"; 4 | import { Ship } from "../src/modles/Ship"; 5 | import { CollisionHelper } from "../src/helpers/CollisionHelper"; 6 | 7 | describe("Kills and Unlocks", () => { 8 | it("Should track enemies killed", () => { 9 | 10 | }); 11 | }) 12 | -------------------------------------------------------------------------------- /schema-unity/Statistics.cs: -------------------------------------------------------------------------------- 1 | // 2 | // THIS FILE HAS BEEN GENERATED AUTOMATICALLY 3 | // DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING 4 | // 5 | // GENERATED USING @colyseus/schema 0.5.33 6 | // 7 | 8 | using Colyseus.Schema; 9 | 10 | public class Statistics : Schema { 11 | [Type(0, "map", "number")] 12 | public MapSchema stats = new MapSchema(); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /schema-unity/ShipList.cs: -------------------------------------------------------------------------------- 1 | // 2 | // THIS FILE HAS BEEN GENERATED AUTOMATICALLY 3 | // DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING 4 | // 5 | // GENERATED USING @colyseus/schema 0.5.33 6 | // 7 | 8 | using Colyseus.Schema; 9 | 10 | public class ShipList : Schema { 11 | [Type(0, "map", typeof(MapSchema))] 12 | public MapSchema ships = new MapSchema(); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/behaviours/player/DestroyedBehaviour.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { Ship } from '../../models/Ship'; 3 | 4 | export class DestroyedBehaviour extends Behaviour { 5 | 6 | target: Ship; 7 | 8 | constructor(target: Ship) { 9 | super('destroyed', target); 10 | } 11 | 12 | onEvent(): void { 13 | this.target.$state.removeShip(this.target); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /schema-unity/Position.cs: -------------------------------------------------------------------------------- 1 | // 2 | // THIS FILE HAS BEEN GENERATED AUTOMATICALLY 3 | // DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING 4 | // 5 | // GENERATED USING @colyseus/schema 0.5.33 6 | // 7 | 8 | using Colyseus.Schema; 9 | 10 | public class Position : Schema { 11 | [Type(0, "number")] 12 | public float x = 0; 13 | 14 | [Type(1, "number")] 15 | public float y = 0; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/behaviours/enemy/DestroyedBehaviour.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { Enemy } from '../../models/Enemy'; 3 | 4 | export class DestroyedBehaviour extends Behaviour { 5 | 6 | target: Enemy; 7 | 8 | constructor(target: Enemy) { 9 | super('destroyed', target); 10 | } 11 | 12 | onEvent(): void { 13 | this.target.$state.removeEnemy(this.target); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/behaviours/bullet/DestroyedBehaviour.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { Bullet } from '../../models/Bullet'; 3 | 4 | export class DestroyedBehaviour extends Behaviour { 5 | 6 | target: Bullet; 7 | 8 | constructor(target: Bullet) { 9 | super('destroyed', target); 10 | } 11 | 12 | onEvent(): void { 13 | this.target.$state.removeBullet(this.target); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/models/enemies/index.ts: -------------------------------------------------------------------------------- 1 | export { Asteroid } from './Asteroid'; 2 | export { Blaster } from './Blaster'; 3 | export { Blimp } from './Blimp'; 4 | export { Bomber } from './Bomber'; 5 | export { Fang } from './Fang'; 6 | export { Hunter } from './Hunter'; 7 | export { Scout } from './Scout'; 8 | export { Speeder } from './Speeder'; 9 | export { Tank } from './Tank'; 10 | export { Tracker } from './Tracker'; 11 | -------------------------------------------------------------------------------- /src/models/messages/UnlockMessage.ts: -------------------------------------------------------------------------------- 1 | import {Schema, MapSchema, type} from '@colyseus/schema'; 2 | import { UnlockItem } from '../UnlockItem'; 3 | 4 | export class UnlockMessage extends Schema { 5 | 6 | @type({map: UnlockItem}) 7 | unlocks: MapSchema; 8 | 9 | constructor( unlocks: any = {}) { 10 | super(); 11 | this.unlocks = new MapSchema(unlocks); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /schema-unity/Enemy.cs: -------------------------------------------------------------------------------- 1 | // 2 | // THIS FILE HAS BEEN GENERATED AUTOMATICALLY 3 | // DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING 4 | // 5 | // GENERATED USING @colyseus/schema 0.5.33 6 | // 7 | 8 | using Colyseus.Schema; 9 | 10 | public class Enemy : Entity { 11 | [Type(6, "number")] 12 | public float health = 0; 13 | 14 | [Type(7, "string")] 15 | public string modelType = ""; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /schema-unity/Bullet.cs: -------------------------------------------------------------------------------- 1 | // 2 | // THIS FILE HAS BEEN GENERATED AUTOMATICALLY 3 | // DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING 4 | // 5 | // GENERATED USING @colyseus/schema 0.5.33 6 | // 7 | 8 | using Colyseus.Schema; 9 | 10 | public class Bullet : Entity { 11 | [Type(6, "number")] 12 | public float blastRadius = 0; 13 | 14 | [Type(7, "string")] 15 | public string bulletMesh = ""; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /schema-unity/ErrorMessage.cs: -------------------------------------------------------------------------------- 1 | // 2 | // THIS FILE HAS BEEN GENERATED AUTOMATICALLY 3 | // DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING 4 | // 5 | // GENERATED USING @colyseus/schema 0.5.33 6 | // 7 | 8 | using Colyseus.Schema; 9 | 10 | public class ErrorMessage : Schema { 11 | [Type(0, "string")] 12 | public string key = ""; 13 | 14 | [Type(1, "string")] 15 | public string message = ""; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "module": "commonjs", 5 | "lib": ["es6"], 6 | "target": "es2016", 7 | "declaration": true, 8 | "noImplicitAny": false, 9 | "experimentalDecorators": true, 10 | "sourceMap": false, 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true 13 | }, 14 | "include": [ 15 | "**/*.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /schema-unity/UnlockMessage.cs: -------------------------------------------------------------------------------- 1 | // 2 | // THIS FILE HAS BEEN GENERATED AUTOMATICALLY 3 | // DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING 4 | // 5 | // GENERATED USING @colyseus/schema 0.5.33 6 | // 7 | 8 | using Colyseus.Schema; 9 | 10 | public class UnlockMessage : Schema { 11 | [Type(0, "map", typeof(MapSchema))] 12 | public MapSchema unlocks = new MapSchema(); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/models/special/HyperSpeed.ts: -------------------------------------------------------------------------------- 1 | import { SpecialSystem } from './SpecialSystem'; 2 | 3 | export class HyperSpeed extends SpecialSystem { 4 | 5 | handleEvent(): void { 6 | this.target.position.x += this.amount * (this.target.lastPosition.x - this.target.position.x); 7 | this.target.position.y += this.amount * (this.target.lastPosition.y - this.target.position.y); 8 | this.target.clampToBounds(); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/models/special/SpecialSystem.ts: -------------------------------------------------------------------------------- 1 | import { Ship } from '../Ship'; 2 | 3 | export class SpecialSystem { 4 | 5 | protected target: Ship; 6 | public duration = 0; 7 | protected timer = 0; 8 | public amount = 0; 9 | protected active = false; 10 | 11 | constructor(target: Ship) { 12 | this.target = target; 13 | } 14 | 15 | public handleEvent(): void { 16 | // Do nothing. 17 | } 18 | public handleUpdate(deltaTime: number): void { 19 | // Do nothing. 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/behaviours/drop/DespawnAfterTime.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { Drop } from '../../models'; 3 | 4 | export class DespawnAfterTime extends Behaviour { 5 | 6 | target: Drop; 7 | 8 | timer = 0; 9 | cooldown = 10000; 10 | 11 | constructor(target: Drop) { 12 | super('despawnAfterTime', target); 13 | } 14 | 15 | onUpdate(deltaTime: number): void { 16 | this.timer += deltaTime; 17 | if(this.timer >= this.cooldown) { 18 | this.target.handleEvent('destroyed'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /schema-unity/TempUpgrade.cs: -------------------------------------------------------------------------------- 1 | // 2 | // THIS FILE HAS BEEN GENERATED AUTOMATICALLY 3 | // DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING 4 | // 5 | // GENERATED USING @colyseus/schema 0.5.33 6 | // 7 | 8 | using Colyseus.Schema; 9 | 10 | public class TempUpgrade : Schema { 11 | [Type(0, "string")] 12 | public string name = ""; 13 | 14 | [Type(1, "string")] 15 | public string description = ""; 16 | 17 | [Type(2, "string")] 18 | public string key = ""; 19 | 20 | [Type(3, "number")] 21 | public float value = 0; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/models/special/Invisibility.ts: -------------------------------------------------------------------------------- 1 | import { SpecialSystem } from './SpecialSystem'; 2 | 3 | export class Invisibility extends SpecialSystem { 4 | 5 | handleEvent(): void { 6 | this.target.invisible = true; 7 | this.active = true; 8 | } 9 | 10 | handleUpdate(deltaTime: number): void { 11 | if(!this.active) return; 12 | 13 | this.timer += deltaTime; 14 | if(this.timer >= this.duration) { 15 | this.target.invisible = false; 16 | this.timer = 0; 17 | this.active = false; 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/models/special/Thrusters.ts: -------------------------------------------------------------------------------- 1 | import { SpecialSystem } from './SpecialSystem'; 2 | 3 | export class Thrusters extends SpecialSystem { 4 | 5 | handleEvent(): void { 6 | this.target.setThrusters(this.amount); 7 | this.active = true; 8 | } 9 | 10 | handleUpdate(deltaTime: number): void { 11 | if(!this.active) return; 12 | 13 | this.timer += deltaTime; 14 | if(this.timer >= this.duration) { 15 | this.target.setThrusters(1); 16 | this.timer = 0; 17 | this.active = false; 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/spawner/patterns/index.ts: -------------------------------------------------------------------------------- 1 | export { AlternatingSide } from './AlternatingSide'; 2 | export { BothSideLine } from './BothSideLine'; 3 | export { DiagonalLine } from './DiagonalLine'; 4 | export { DoubleVerticalLine } from './DoubleVerticalLine'; 5 | export { HorizontalLine } from './HorizontalLine'; 6 | export { SideLine } from './SideLine'; 7 | export { SideDiagonalLine } from './SideDiagonalLine'; 8 | export { TopTriangle } from './TopTriangle'; 9 | export { TripleVerticalLine } from './TripleVerticalLine'; 10 | export { VerticalLine } from './VerticalLine'; 11 | -------------------------------------------------------------------------------- /src/behaviours/boss/DropReward.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { Drop, Enemy } from '../../models'; 3 | 4 | export class DropReward extends Behaviour { 5 | 6 | target: Enemy; 7 | 8 | constructor(target: Enemy) { 9 | super('destroyed', target); 10 | } 11 | 12 | onEvent(): void { 13 | /** 14 | * Create a reward crystal 15 | **/ 16 | const drop: Drop = new Drop({ position: this.target.position.clone()}); 17 | console.log("Drop Released: ", drop); 18 | this.target.$state.addDrop(drop); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/models/special/index.ts: -------------------------------------------------------------------------------- 1 | export {ScatterShot} from './ScatterShot'; 2 | export {Shotgun} from './Shotgun'; 3 | export {WeaponCharge} from './WeaponCharge'; 4 | export {Thrusters} from './Thrusters'; 5 | export {RammingShield} from './RammingShield'; 6 | export {HyperSpeed} from './HyperSpeed'; 7 | export {Invisibility} from './Invisibility'; 8 | export {Bomb} from './Bomb'; 9 | export {MissileBarage} from './MissileBarage'; 10 | export {MegaBomb} from './MegaBomb'; 11 | export {ShieldRecharge} from './ShieldRecharge'; 12 | export {ForceShield} from './ForceShield'; 13 | -------------------------------------------------------------------------------- /src/models/special/RammingShield.ts: -------------------------------------------------------------------------------- 1 | import { SpecialSystem } from './SpecialSystem'; 2 | 3 | export class RammingShield extends SpecialSystem { 4 | 5 | handleEvent(): void { 6 | this.target.collisionInvulnerable = true; 7 | this.active = true; 8 | } 9 | 10 | handleUpdate(deltaTime: number): void { 11 | if(!this.active) return; 12 | 13 | this.timer += deltaTime; 14 | if(this.timer >= this.duration) { 15 | this.target.collisionInvulnerable = false; 16 | this.timer = 0; 17 | this.active = false; 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/models/TempUpgrade.ts: -------------------------------------------------------------------------------- 1 | import {Schema, type} from '@colyseus/schema'; 2 | 3 | export class TempUpgrade extends Schema { 4 | 5 | @type('string') 6 | name: string; 7 | 8 | @type('string') 9 | description: string; 10 | 11 | @type('string') 12 | key: string; 13 | 14 | @type('number') 15 | value: number; 16 | 17 | constructor( options: { name: string; description: string; key: string; value: number } ) { 18 | super(); 19 | this.name = options.name; 20 | this.description = options.description; 21 | this.key = options.key; 22 | this.value = options.value; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/spawner/patterns/VerticalLine.ts: -------------------------------------------------------------------------------- 1 | import { TimedPosition } from '../TimedPosition'; 2 | import { Pattern } from '../Pattern'; 3 | 4 | export class VerticalLine extends Pattern { 5 | 6 | constructor(enemyCount: number, enemyType: any, difficulty: number ) { 7 | super(); 8 | const column = Math.floor(Math.random() * 15); 9 | this.points = []; 10 | for(let i = 0; i < 8; i++) { 11 | this.points.push(new TimedPosition(i * 2, 100 + column * 100, 1100)) 12 | } 13 | this.maxTime = (2 * enemyCount); 14 | this.enemyCount = enemyCount; 15 | this.enemyType = enemyType; 16 | this.difficulty = difficulty; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/models/special/ForceShield.ts: -------------------------------------------------------------------------------- 1 | import { SpecialSystem } from './SpecialSystem'; 2 | 3 | export class ForceShield extends SpecialSystem { 4 | 5 | handleEvent(): void { 6 | this.target.collisionInvulnerable = true; 7 | this.target.bulletInvulnerable = true; 8 | this.active = true; 9 | } 10 | 11 | handleUpdate(deltaTime: number): void { 12 | if(!this.active) return; 13 | 14 | this.timer += deltaTime; 15 | if(this.timer >= this.duration) { 16 | this.target.collisionInvulnerable = false; 17 | this.target.bulletInvulnerable = false; 18 | this.timer = 0; 19 | this.active = false; 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /scripts/stat_parser.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var array = fs.readFileSync('ship_stats_raw.txt').toString().split("\n"); 4 | var index = 0; 5 | var hash = {}; 6 | var ships = []; 7 | for(let i in array) { 8 | data = array[i].split("\t"); 9 | if(index == 0) { 10 | ships = data; 11 | for(let j = 0; j < ships.length; j++) { 12 | ships[j] = ships[j].replace("\r",""); 13 | hash[ships[j]] = { }; 14 | } 15 | index++; 16 | } else { 17 | for(let j = 1, s = 0; j < data.length; j++, s++) { 18 | hash[ships[s]][data[0]] = + data[j].replace("\r",""); 19 | } 20 | } 21 | } 22 | 23 | console.log(JSON.stringify(hash, null, 2)); 24 | -------------------------------------------------------------------------------- /src/behaviours/enemy/movement/RotateInCircle.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../../behaviour'; 2 | import { C } from '../../../Constants'; 3 | import { Enemy, Position } from '../../../models'; 4 | import { CollisionHelper } from '../../../helpers/CollisionHelper'; 5 | 6 | export class RotateInCircle extends Behaviour { 7 | 8 | rotationDirection = 1; 9 | 10 | target: Enemy; 11 | 12 | constructor(target: Enemy) { 13 | super('RotateInCircle', target); 14 | this.rotationDirection = Math.random() > 0.5 ? 1 : -1; 15 | } 16 | 17 | onUpdate(deltaTime: number): void { 18 | this.target.angle += this.rotationDirection * 0.5 * deltaTime/1000; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /schema-unity/UnlockItem.cs: -------------------------------------------------------------------------------- 1 | // 2 | // THIS FILE HAS BEEN GENERATED AUTOMATICALLY 3 | // DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING 4 | // 5 | // GENERATED USING @colyseus/schema 0.5.33 6 | // 7 | 8 | using Colyseus.Schema; 9 | 10 | public class UnlockItem : Schema { 11 | [Type(0, "boolean")] 12 | public bool unlocked = false; 13 | 14 | [Type(1, "string")] 15 | public string key = ""; 16 | 17 | [Type(2, "number")] 18 | public float count = 0; 19 | 20 | [Type(3, "string")] 21 | public string unlockType = ""; 22 | 23 | [Type(4, "string")] 24 | public string name = ""; 25 | 26 | [Type(5, "string")] 27 | public string description = ""; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /schema-unity/ShipBuilderState.cs: -------------------------------------------------------------------------------- 1 | // 2 | // THIS FILE HAS BEEN GENERATED AUTOMATICALLY 3 | // DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING 4 | // 5 | // GENERATED USING @colyseus/schema 0.5.33 6 | // 7 | 8 | using Colyseus.Schema; 9 | 10 | public class ShipBuilderState : Schema { 11 | [Type(0, "ref", typeof(Statistics))] 12 | public Statistics stats = new Statistics(); 13 | 14 | [Type(1, "ref", typeof(UnlockMessage))] 15 | public UnlockMessage unlockMessage = new UnlockMessage(); 16 | 17 | [Type(2, "ref", typeof(ErrorMessage))] 18 | public ErrorMessage error = new ErrorMessage(); 19 | 20 | [Type(3, "ref", typeof(ShipList))] 21 | public ShipList shipList = new ShipList(); 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/models/enemies/Scout.ts: -------------------------------------------------------------------------------- 1 | import { Enemy } from '../Enemy'; 2 | import { StraightLinePath } from '../../behaviours/Enemy/movement/StraightLinePath'; 3 | 4 | export class Scout extends Enemy { 5 | 6 | constructor(options: any) { 7 | super(options); 8 | this.healthBase = 12; 9 | this.healthGrowth = 0.1; 10 | 11 | this.speedBase = 85; 12 | this.speedGrowth = 5; 13 | 14 | this.collisionDamageBase = 1; 15 | this.collisionDamageGrowth = 0.1; 16 | 17 | this.modelType = "scout"; 18 | 19 | this.radius = 30; 20 | } 21 | 22 | onInitGame(state: any): void { 23 | super.onInitGame(state); 24 | this.registerBehaviour("path", new StraightLinePath(this)); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/models/states/ShipBuilderState.ts: -------------------------------------------------------------------------------- 1 | import {Schema, type} from '@colyseus/schema'; 2 | import { UnlockMessage, Statistics, ErrorMessage, ShipList } from '../messages'; 3 | 4 | export class ShipBuilderState extends Schema { 5 | 6 | /** 7 | * This just a dummy state file used to expose other schema objects. 8 | * Exposing types that will be used during OnMessage callbacks below. 9 | **/ 10 | @type(Statistics) 11 | stats: Statistics = new Statistics(); 12 | 13 | @type(UnlockMessage) 14 | unlockMessage: UnlockMessage = new UnlockMessage(); 15 | 16 | @type(ErrorMessage) 17 | error: ErrorMessage = new ErrorMessage("error"); 18 | 19 | @type(ShipList) 20 | shipList: ShipList = new ShipList(); 21 | } 22 | -------------------------------------------------------------------------------- /schema-unity/Entity.cs: -------------------------------------------------------------------------------- 1 | // 2 | // THIS FILE HAS BEEN GENERATED AUTOMATICALLY 3 | // DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING 4 | // 5 | // GENERATED USING @colyseus/schema 0.5.33 6 | // 7 | 8 | using Colyseus.Schema; 9 | 10 | public class Entity : Schema { 11 | [Type(0, "string")] 12 | public string uuid = ""; 13 | 14 | [Type(1, "ref", typeof(Position))] 15 | public Position position = new Position(); 16 | 17 | [Type(2, "number")] 18 | public float angle = 0; 19 | 20 | [Type(3, "boolean")] 21 | public bool bulletInvulnerable = false; 22 | 23 | [Type(4, "boolean")] 24 | public bool collisionInvulnerable = false; 25 | 26 | [Type(5, "boolean")] 27 | public bool invisible = false; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/models/enemies/Tracker.ts: -------------------------------------------------------------------------------- 1 | import { Enemy } from '../Enemy'; 2 | import { ClosestPlayerPath } from '../../behaviours/Enemy/movement/ClosestPlayerPath'; 3 | 4 | export class Tracker extends Enemy { 5 | 6 | constructor(options: any) { 7 | super(options); 8 | this.healthBase = 3; 9 | this.healthGrowth = 0.1; 10 | 11 | this.speedBase = 75; 12 | this.speedGrowth = 5; 13 | 14 | this.collisionDamageBase = 1; 15 | this.collisionDamageGrowth = 0.1; 16 | 17 | this.modelType = "tracker"; 18 | 19 | this.radius = 40; 20 | } 21 | 22 | onInitGame(state: any): void { 23 | super.onInitGame(state); 24 | this.registerBehaviour("path", new ClosestPlayerPath(this)); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/behaviours/bullet/StraightLineDownPath.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { Bullet } from '../../models/Bullet'; 3 | 4 | export class StraightLineDownPath extends Behaviour { 5 | 6 | startY: number; 7 | target: Bullet; 8 | 9 | constructor(target: Bullet) { 10 | super('StraightLineDownPath', target); 11 | this.startY = this.target.position.y; 12 | } 13 | 14 | onUpdate(deltaTime): void { 15 | this.target.position.y -= this.target.speed * deltaTime/1000; 16 | 17 | if(Math.abs(this.target.position.y - this.startY) >= this.target.range) { 18 | this.target.handleEvent('destroyed'); 19 | } 20 | /** TODO: Determine if we want to remove bullets when offscreen **/ 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/models/enemies/Hunter.ts: -------------------------------------------------------------------------------- 1 | import { Enemy } from '../Enemy'; 2 | import { ClosestPlayerAtStartPath } from '../../behaviours/Enemy/movement/ClosestPlayerAtStartPath'; 3 | 4 | export class Hunter extends Enemy { 5 | 6 | constructor(options: any) { 7 | super(options); 8 | this.healthBase = 4; 9 | this.healthGrowth = 0.1; 10 | 11 | this.speedBase = 75; 12 | this.speedGrowth = 5; 13 | 14 | this.collisionDamageBase = 1; 15 | this.collisionDamageGrowth = 0.1; 16 | 17 | this.modelType = "hunter"; 18 | 19 | this.radius = 40; 20 | } 21 | 22 | onInitGame(state: any): void { 23 | super.onInitGame(state); 24 | this.registerBehaviour("path", new ClosestPlayerAtStartPath(this)); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/models/enemies/Speeder.ts: -------------------------------------------------------------------------------- 1 | import { Enemy } from '../Enemy'; 2 | import { TargetPlayerStartPath } from '../../behaviours/Enemy/movement/TargetPlayerStartPath'; 3 | 4 | 5 | export class Speeder extends Enemy { 6 | 7 | constructor(options: any) { 8 | super(options); 9 | this.healthBase = 3; 10 | this.healthGrowth = 0.1; 11 | 12 | this.speedBase = 95; 13 | this.speedGrowth = 5; 14 | 15 | this.collisionDamageBase = 1; 16 | this.collisionDamageGrowth = 0.1; 17 | 18 | this.modelType = "speeder"; 19 | 20 | this.radius = 40; 21 | } 22 | 23 | onInitGame(state: any): void { 24 | super.onInitGame(state); 25 | this.registerBehaviour("path", new TargetPlayerStartPath(this)); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/models/enemies/Asteroid.ts: -------------------------------------------------------------------------------- 1 | import { Enemy } from '../Enemy'; 2 | import { StraightLinePath } from '../../behaviours/Enemy/movement/StraightLinePath'; 3 | 4 | export class Asteroid extends Enemy { 5 | 6 | constructor(options: any) { 7 | super(options); 8 | this.healthBase = 999; 9 | this.healthGrowth = 0; 10 | 11 | this.speedBase = 30; 12 | this.speedGrowth = 2; 13 | 14 | this.collisionDamageBase = 1; 15 | this.collisionDamageGrowth = 0; 16 | 17 | this.modelType = "asteroid" + Math.ceil(Math.random() * 3); 18 | 19 | this.radius = 40; 20 | } 21 | 22 | onInitGame(state: any): void { 23 | super.onInitGame(state); 24 | this.registerBehaviour("path", new StraightLinePath(this)); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/spawner/patterns/DiagonalLine.ts: -------------------------------------------------------------------------------- 1 | import { TimedPosition } from '../TimedPosition'; 2 | import { Pattern } from '../Pattern'; 3 | 4 | export class DiagonalLine extends Pattern { 5 | 6 | constructor(enemyCount: number, enemyType: any, difficulty: number ) { 7 | super(); 8 | let a: number[] = []; 9 | for(let i = 0; i < 15; i++) { 10 | a.push(i * 2); 11 | } 12 | if( Math.random() > 0.5) a = a.reverse(); 13 | this.points = []; 14 | for(let i = 0; i < 15; i++) { 15 | this.points.push(new TimedPosition(a[i], (i+1) * 100, 1100)); 16 | } 17 | this.maxTime = enemyCount * 2; 18 | this.enemyCount = enemyCount; 19 | this.enemyType = enemyType; 20 | this.difficulty = difficulty; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/behaviours/player/CollectDrop.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { CollisionHelper } from '../../helpers/CollisionHelper'; 3 | import { Drop, Ship } from '../../models'; 4 | 5 | export class CollectDrop extends Behaviour { 6 | 7 | target: Ship; 8 | 9 | constructor(target: Ship) { 10 | super('CollectDrop', target); 11 | } 12 | 13 | public onUpdate(deltaTime: number): void { 14 | if(this.target.tempUpgradeTimer > 0) { 15 | this.target.tempUpgradeTimer -= deltaTime; 16 | if(this.target.tempUpgradeTimer <= 0) { 17 | this.target.clearTempUpgrades(); 18 | } 19 | } 20 | } 21 | 22 | public onEvent(drop: Drop): void { 23 | this.target.generateTempUpgrades(drop.modelType); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/spawner/patterns/SideLine.ts: -------------------------------------------------------------------------------- 1 | import { TimedPosition } from '../TimedPosition'; 2 | import { Pattern } from '../Pattern'; 3 | 4 | export class SideLine extends Pattern { 5 | 6 | constructor(enemyCount: number, enemyType: any, difficulty: number ) { 7 | super(); 8 | const i = Math.random() > 0.5 ? 1800 : -200; 9 | this.points = [ 10 | new TimedPosition(0, i, 800), 11 | new TimedPosition(0, i, 700), 12 | new TimedPosition(0, i, 600), 13 | new TimedPosition(0, i, 500), 14 | new TimedPosition(0, i, 400), 15 | new TimedPosition(0, i, 300), 16 | ]; 17 | this.maxTime = 2; 18 | this.enemyCount = enemyCount; 19 | this.enemyType = enemyType; 20 | this.difficulty = difficulty; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/models/enemies/Blimp.ts: -------------------------------------------------------------------------------- 1 | import { Enemy } from '../Enemy'; 2 | import { SimpleFlockingPath } from '../../behaviours/Enemy/movement/SimpleFlockingPath'; 3 | 4 | export class Blimp extends Enemy { 5 | 6 | constructor(options: any) { 7 | super(options); 8 | this.healthBase = 3; 9 | this.healthGrowth = 0.1; 10 | 11 | this.speedBase = 75; 12 | this.speedGrowth = 5; 13 | 14 | this.collisionDamageBase = 1; 15 | this.collisionDamageGrowth = 0.1; 16 | 17 | this.modelType = "blimp"; 18 | 19 | this.radius = 40; 20 | } 21 | 22 | onInitGame(state: any): void { 23 | super.onInitGame(state); 24 | this.registerBehaviour("path", new SimpleFlockingPath(this, {destination:this.destination, flock:this.flock})); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/models/UnlockItem.ts: -------------------------------------------------------------------------------- 1 | import {Schema, type} from '@colyseus/schema'; 2 | 3 | export class UnlockItem extends Schema { 4 | 5 | @type("boolean") 6 | unlocked: boolean; 7 | 8 | @type("string") 9 | key: string; 10 | 11 | @type("number") 12 | count: number; 13 | 14 | @type("string") 15 | unlockType: string; 16 | 17 | @type("string") 18 | name: string; 19 | 20 | @type("string") 21 | description: string; 22 | 23 | constructor(key: string, unlocked: boolean, count: number, unlockType: string, name: string, description: string) { 24 | super(); 25 | this.key = key; 26 | this.unlocked = unlocked; 27 | this.count = count; 28 | this.unlockType = unlockType; 29 | this.name = name; 30 | this.description = description; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/behaviours/enemy/FiresBulletBehaviour.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { Primary } from '../../models/primary/Primary'; 3 | import { Enemy } from '../../models/Enemy'; 4 | 5 | export class FiresBulletBehaviour extends Behaviour { 6 | 7 | timer = 0; 8 | cooldown = 0; 9 | system: Primary; 10 | 11 | target: Enemy; 12 | 13 | constructor(target: Enemy, bulletOptions ) { 14 | super('fires_bullet', target); 15 | this.system = new bulletOptions.system(this.target, bulletOptions); 16 | this.cooldown = bulletOptions.cooldown; 17 | } 18 | 19 | onUpdate(deltaTime): void { 20 | this.timer += deltaTime; 21 | if(this.timer >= this.cooldown) { 22 | this.system.spawnBullets(this.target); 23 | this.timer = 0; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/behaviours/enemy/TakesDamageBehaviour.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { Enemy, Entity } from '../../models'; 3 | 4 | export class TakesDamageBehaviour extends Behaviour { 5 | 6 | private _destroyed = false; 7 | 8 | target: Enemy; 9 | 10 | constructor(target: Enemy) { 11 | super('take_damage', target); 12 | } 13 | 14 | public onEvent(args: {damage: number; firedBy?: Entity}): void { 15 | this.target.health = Math.max(this.target.health - args.damage, 0); 16 | if(this.target.health <= 0 && !this._destroyed) { 17 | if(args.firedBy) { 18 | args.firedBy.addKill(this.target.$state.currentWave, this.target.modelType); 19 | } 20 | this._destroyed = true; 21 | this.target.handleEvent('destroyed'); 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/behaviours/bullet/StraightLineUpPath.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { Bullet } from '../../models/Bullet'; 3 | 4 | export class StraightLineUpPath extends Behaviour { 5 | 6 | startY: number; 7 | target: Bullet; 8 | 9 | constructor(target: Bullet) { 10 | super('StraightLineUpPath', target); 11 | this.startY = this.target.position.y; 12 | } 13 | 14 | onUpdate(deltaTime): void { 15 | this.target.position.y += this.target.speed * deltaTime/1000; 16 | 17 | if(this.target.position.y - this.startY >= this.target.range) { 18 | if(this.target.blastRadius != 0) { 19 | this.target.handleEvent('explode'); 20 | } 21 | this.target.handleEvent('destroyed'); 22 | } 23 | /** TODO: Determine if we want to remove bullets when offscreen **/ 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/behaviours/player/ShieldRechargeBehaviour.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { Ship } from '../../models/Ship'; 3 | 4 | 5 | export class ShieldRechargeBehaviour extends Behaviour { 6 | 7 | private shield_cooldown: number; 8 | 9 | target: Ship; 10 | 11 | constructor(target: Ship) { 12 | super('shield_recharge', target); 13 | 14 | } 15 | 16 | public onUpdate(deltaTime: number): void { 17 | if(this.target.shieldRechargeTime < this.target.shieldRechargeCooldown && this.target.shield < this.target.maxShield) { 18 | this.target.shieldRechargeTime += deltaTime; 19 | } else if(this.target.shieldRechargeTime >= this.target.shieldRechargeCooldown && this.target.shield < this.target.maxShield) { 20 | this.target.shieldRechargeTime = 0; 21 | this.target.shield += 1; 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/behaviours/player/CollidesWithDrop.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { CollisionHelper } from '../../helpers/CollisionHelper'; 3 | import { Ship } from '../../models/Ship'; 4 | 5 | export class CollidesWithDrop extends Behaviour { 6 | 7 | target: Ship; 8 | 9 | dropsCollected: string[] = []; 10 | 11 | constructor(target: Ship) { 12 | super('CollidesWithDrop', target); 13 | } 14 | 15 | public onUpdate(deltaTime: number): void { 16 | for(const uuid in this.target.$state.drops) { 17 | if(this.dropsCollected.includes(uuid)) return; 18 | 19 | const drop = this.target.$state.drops[uuid]; 20 | if(CollisionHelper.collisionBetween(this.target, drop)) { 21 | this.dropsCollected.push(drop.uuid); 22 | this.target.handleEvent('CollectDrop', drop); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/spawner/patterns/DoubleVerticalLine.ts: -------------------------------------------------------------------------------- 1 | import { TimedPosition } from '../TimedPosition'; 2 | import { Pattern } from '../Pattern'; 3 | 4 | export class DoubleVerticalLine extends Pattern { 5 | 6 | constructor(enemyCount: number, enemyType: any, difficulty: number ) { 7 | super(); 8 | const column1 = Math.floor(Math.random() * 15); 9 | let column2 = Math.floor(Math.random() * 15); 10 | while( column1 == column2 ) { 11 | column2 = Math.floor(Math.random() * 15); 12 | } 13 | this.points = []; 14 | for(let i = 0; i < 8; i++) { 15 | this.points.push(new TimedPosition(i * 2, 100 + column1 * 100, 1100)) 16 | this.points.push(new TimedPosition(i * 2, 100 + column2 * 100, 1100)) 17 | } 18 | this.maxTime = enemyCount + 2; 19 | this.enemyCount = enemyCount; 20 | this.enemyType = enemyType; 21 | this.difficulty = difficulty; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/spawner/patterns/SideDiagonalLine.ts: -------------------------------------------------------------------------------- 1 | import { TimedPosition } from '../TimedPosition'; 2 | import { Pattern } from '../Pattern'; 3 | 4 | export class SideDiagonalLine extends Pattern { 5 | 6 | constructor(enemyCount: number, enemyType: any, difficulty: number ) { 7 | super(); 8 | 9 | const i = Math.random() > 0.5 ? 1800 : -200; 10 | 11 | let a: number[] = [0, 2, 4, 6, 8, 10]; 12 | if( Math.random() > 0.5) a = a.reverse(); 13 | 14 | this.points = [ 15 | new TimedPosition(a[0], i, 800), 16 | new TimedPosition(a[1], i, 700), 17 | new TimedPosition(a[2], i, 600), 18 | new TimedPosition(a[3], i, 500), 19 | new TimedPosition(a[3], i, 400), 20 | new TimedPosition(a[3], i, 300), 21 | ]; 22 | this.maxTime = enemyCount * 2; 23 | this.enemyCount = enemyCount; 24 | this.enemyType = enemyType; 25 | this.difficulty = difficulty; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/behaviours/bullet/StraightAnglePath.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { Bullet, Position } from '../../models'; 3 | 4 | export class StraightAnglePath extends Behaviour { 5 | 6 | start: Position; 7 | 8 | target: Bullet; 9 | 10 | constructor(target: Bullet, args: { angle: number}) { 11 | super('StraightAnglePath', target); 12 | this.start = new Position(this.target.position.x, this.target.position.y); 13 | this.target.angle = args.angle; 14 | } 15 | 16 | onUpdate(deltaTime): void { 17 | this.target.position.x += Math.cos(this.target.angle) * this.target.speed * deltaTime/1000; 18 | this.target.position.y += Math.sin(this.target.angle) * this.target.speed * deltaTime/1000; 19 | 20 | if(this.start.distanceTo(this.target.position) > this.target.range) this.target.handleEvent('destroyed'); 21 | /** TODO: Determine if we want to remove bullets when offscreen **/ 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Database.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | 3 | const DB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/development'; 4 | 5 | class Database { 6 | public $accounts: any; 7 | public $ships: any; 8 | 9 | private client: MongoClient; 10 | private db: any; 11 | 12 | async init(): Promise { 13 | if(this.client) return; 14 | 15 | try { 16 | this.client = await MongoClient.connect(DB_URI, { useNewUrlParser: true, useUnifiedTopology: true }); 17 | this.db = this.client.db('development'); 18 | } catch (err) { 19 | console.log('[Database] Error', err); 20 | process.exit(0); 21 | } 22 | 23 | this.$accounts = this.db.collection('accounts'); 24 | this.$accounts.createIndex({ username: 1 }, { unique: true }); 25 | 26 | this.$ships = this.db.collection('ships'); 27 | this.$ships.createIndex({ username: 1 }); 28 | } 29 | } 30 | 31 | export const DB = new Database(); 32 | -------------------------------------------------------------------------------- /src/behaviours/player/TakesDamageBehaviour.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { Ship } from '../../models/Ship'; 3 | 4 | export class TakesDamageBehaviour extends Behaviour { 5 | 6 | target: Ship; 7 | 8 | timer = 0; 9 | cooldown = 250; 10 | 11 | constructor(target: Ship) { 12 | super('take_damage', target); 13 | } 14 | 15 | public onUpdate(deltaTime: number) { 16 | if(this.target.justDamaged) { 17 | this.timer += deltaTime; 18 | if(this.timer >= this.cooldown) { 19 | this.target.justDamaged = false; 20 | this.timer = 0; 21 | } 22 | } 23 | } 24 | 25 | public onEvent(args: {damage: number}): void { 26 | if(this.target.justDamaged) return; 27 | 28 | this.target.shield = Math.max(this.target.shield - args.damage, 0); 29 | this.target.justDamaged = true; 30 | if(this.target.shield <= 0) { 31 | this.target.handleEvent('destroyed'); 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/spawner/patterns/AlternatingSide.ts: -------------------------------------------------------------------------------- 1 | import { TimedPosition } from '../TimedPosition'; 2 | import { Pattern } from '../Pattern'; 3 | 4 | export class AlternatingSide extends Pattern { 5 | 6 | constructor(enemyCount: number, enemyType: any, difficulty: number, timeOffset = 0, positionOffset = 0 ) { 7 | super(); 8 | const i = Math.random() > 0.5 ? 1800 : -200; 9 | const j = i == 1800 ? -200 : 1800; 10 | this.points = [ 11 | new TimedPosition(0 + timeOffset, i, 800), 12 | new TimedPosition(0 + timeOffset, j, 700), 13 | new TimedPosition(0 + timeOffset, i, 600), 14 | new TimedPosition(0 + timeOffset, j, 500), 15 | new TimedPosition(0 + timeOffset, i, 400), 16 | new TimedPosition(0 + timeOffset, j, 300), 17 | ]; 18 | this.maxTime = 2; 19 | this.enemyCount = enemyCount; 20 | this.enemyType = enemyType; 21 | this.difficulty = difficulty; 22 | this.positionOffset = positionOffset; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps : [{ 3 | name: 'API', 4 | script: 'src/index.ts', 5 | instances: 1, 6 | autorestart: true, 7 | watch: false, 8 | max_memory_restart: '1G', 9 | env: { 10 | NODE_ENV: 'development' 11 | }, 12 | env_production: { 13 | NODE_ENV: 'production' 14 | } 15 | }], 16 | 17 | deploy : { 18 | production : { 19 | user : 'deploy', 20 | host : 'cindertron7.com', 21 | key : "~/ssh/digital_ocean_private.pem", 22 | ref : 'origin/master', 23 | repo : 'https://github.com/stats/space_shooter_server.git', 24 | path : '/var/www/cindertron7.com/production', 25 | 'post-deploy' : 'export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/bash_completion" ] && "$NVM_DIR/bash_completion" && npm install && pm2 startOrRestart ecosystem.config.js --env production ', 26 | "env" : { 27 | "NODE_ENV": "production" 28 | } 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/behaviours/player/CollidesWithEnemyBullet.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { CollisionHelper } from '../../helpers/CollisionHelper'; 3 | import { C } from '../../Constants'; 4 | import { Bullet, Ship } from '../../models'; 5 | 6 | 7 | export class CollidesWithEnemyBullet extends Behaviour { 8 | 9 | target: Ship; 10 | 11 | constructor(target: Ship) { 12 | super('CollidesWithEnemyBullet', target); 13 | } 14 | 15 | public onUpdate(deltaTime: number): void { 16 | if(this.target.justDamaged) return; 17 | 18 | for(const uuid in this.target.$state.bullets) { 19 | const bullet: Bullet = this.target.$state.bullets[uuid]; 20 | if(bullet.bulletType == C.ENEMY_BULLET && CollisionHelper.collisionBetween(this.target, bullet)) { 21 | if(this.target.bulletInvulnerable == false) { 22 | this.target.handleEvent('take_damage', {damage: bullet.damage}); 23 | } 24 | bullet.handleEvent('destroyed'); 25 | } 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/behaviours/player/CollidesWithEnemy.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { CollisionHelper } from '../../helpers/CollisionHelper'; 3 | import { Ship } from '../../models/Ship'; 4 | 5 | export class CollidesWithEnemy extends Behaviour { 6 | 7 | target: Ship; 8 | 9 | constructor(target: Ship) { 10 | super('CollidesWithEnemy', target); 11 | } 12 | 13 | public onUpdate(deltaTime: number): void { 14 | if(this.target.justDamaged) return; 15 | 16 | for(const uuid in this.target.$state.enemies) { 17 | const enemy = this.target.$state.enemies[uuid]; 18 | if(CollisionHelper.collisionBetween(this.target, enemy)) { 19 | if(this.target.collisionInvulnerable == false) { 20 | this.target.handleEvent('take_damage', { damage: enemy.collisionDamage}); 21 | } 22 | 23 | if(enemy.collisionInvulnerable == false) { 24 | enemy.handleEvent('take_damage', { damage: enemy.health, firedBy: this.target }); 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/behaviours/bullet/ExplodeBehaviour.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { C } from '../../Constants'; 3 | import { Bullet, Entity } from '../../models'; 4 | 5 | export class ExplodeBehaviour extends Behaviour { 6 | 7 | public blastRadius: number; 8 | target: Bullet; 9 | 10 | constructor(target: Bullet) { 11 | super('explode', target); 12 | } 13 | 14 | onEvent(): void { 15 | if(this.target.blastRadius == null || this.target.blastRadius == 0) return; 16 | let entities: Entity[]; 17 | if(this.target.bulletType == C.SHIP_BULLET) { 18 | entities = this.target.$state.getEnemiesInRange(this.target.position.x, this.target.position.y, this.target.blastRadius, true); 19 | } else { 20 | entities = this.target.$state.getShipsInRange(this.target.position.x, this.target.position.y, this.target.blastRadius, true); 21 | } 22 | for(const entity of entities) { 23 | entity.handleEvent('take_damage', { damage: this.target.damage, firedBy: this.target.firedBy }); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /schema-unity/GameState.cs: -------------------------------------------------------------------------------- 1 | // 2 | // THIS FILE HAS BEEN GENERATED AUTOMATICALLY 3 | // DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING 4 | // 5 | // GENERATED USING @colyseus/schema 0.5.33 6 | // 7 | 8 | using Colyseus.Schema; 9 | 10 | public class GameState : Schema { 11 | [Type(0, "map", typeof(MapSchema))] 12 | public MapSchema ships = new MapSchema(); 13 | 14 | [Type(1, "map", typeof(MapSchema))] 15 | public MapSchema enemies = new MapSchema(); 16 | 17 | [Type(2, "map", typeof(MapSchema))] 18 | public MapSchema bullets = new MapSchema(); 19 | 20 | [Type(3, "map", typeof(MapSchema))] 21 | public MapSchema drops = new MapSchema(); 22 | 23 | [Type(4, "number")] 24 | public float startGame = 0; 25 | 26 | [Type(5, "int32")] 27 | public int startWave = 0; 28 | 29 | [Type(6, "int32")] 30 | public int currentWave = 0; 31 | 32 | [Type(7, "int32")] 33 | public int enemiesSpawned = 0; 34 | 35 | [Type(8, "int32")] 36 | public int enemiesKilled = 0; 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/behaviours/enemy/CollidesWithShipBullet.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { Bullet, Enemy } from '../../models'; 3 | import { CollisionHelper } from '../../helpers/CollisionHelper'; 4 | import { C } from '../../Constants'; 5 | 6 | export class CollidesWithShipBullet extends Behaviour { 7 | 8 | target: Enemy; 9 | 10 | constructor(target: Enemy) { 11 | super('CollidesWitShipBullet', target); 12 | } 13 | 14 | public onUpdate(deltaTime: number): void { 15 | for(const uuid in this.target.$state.bullets) { 16 | const bullet: Bullet = this.target.$state.bullets[uuid]; 17 | if(bullet.bulletType == C.SHIP_BULLET && CollisionHelper.collisionBetween(this.target, bullet)) { 18 | if(this.target.bulletInvulnerable == false) { 19 | this.target.handleEvent('take_damage', { damage: bullet.damage, firedBy: bullet.firedBy }); 20 | } 21 | if(bullet.explodes) { 22 | bullet.handleEvent('explode'); 23 | } 24 | bullet.handleEvent('destroyed'); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/spawner/patterns/TopTriangle.ts: -------------------------------------------------------------------------------- 1 | import { TimedPosition } from '../TimedPosition'; 2 | import { Pattern } from '../Pattern'; 3 | 4 | export class TopTriangle extends Pattern { 5 | 6 | constructor(enemyCount: number, enemyType: any, difficulty: number ) { 7 | super(); 8 | 9 | this.points = [ 10 | new TimedPosition(0, 800, 1100), 11 | new TimedPosition(2, 700, 1100), 12 | new TimedPosition(2, 900, 1100), 13 | new TimedPosition(4, 600, 1100), 14 | new TimedPosition(4, 800, 1100), 15 | new TimedPosition(4, 1000, 1100), 16 | new TimedPosition(6, 500, 1100), 17 | new TimedPosition(6, 700, 1100), 18 | new TimedPosition(6, 900, 1100), 19 | new TimedPosition(6, 1100, 1100), 20 | ]; 21 | 22 | if(enemyCount > 6) this.maxTime = 8; 23 | else if (enemyCount > 3 ) this.maxTime = 6; 24 | else if (enemyCount > 1 ) this.maxTime = 4; 25 | else this.maxTime = 2; 26 | 27 | this.enemyCount = enemyCount; 28 | this.enemyType = enemyType; 29 | this.difficulty = difficulty; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/models/special/MissileBarage.ts: -------------------------------------------------------------------------------- 1 | import { SpecialSystem } from './SpecialSystem'; 2 | import { Bullet } from '../Bullet'; 3 | import { MissilePath } from '../../behaviours/bullet/MissilePath'; 4 | import { C, CT } from '../../Constants'; 5 | 6 | export class MissileBarage extends SpecialSystem { 7 | 8 | handleEvent(): void { 9 | for(let i = 0; i < 6; i++) { 10 | const spawnLocation = this.target.getBulletSpawnLocation(); 11 | const bullet: Bullet = new Bullet({ 12 | damage: ( ( this.target.getDamage() + this.target.tempSpecialDamage ) / 3 ) * this.target.tempSpecialDamagePercent, 13 | speed: 350, 14 | range: 600, 15 | collisionType: CT.CIRCLE, 16 | radius: 15, 17 | bulletMesh: "Missile", 18 | x: spawnLocation.x - 45 + (i * 15), 19 | y: spawnLocation.y, 20 | bulletType: C.SHIP_BULLET 21 | }); 22 | 23 | bullet.registerBehaviour("path", new MissilePath(bullet, {angle: (i * Math.PI/6)})); 24 | bullet.firedBy = this.target; 25 | this.target.$state.addBullet(bullet); 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/models/special/Shotgun.ts: -------------------------------------------------------------------------------- 1 | import { SpecialSystem } from './SpecialSystem'; 2 | import { C, CT } from '../../Constants'; 3 | import { StraightAnglePath} from '../../behaviours/bullet/StraightAnglePath'; 4 | import { Bullet } from '../Bullet'; 5 | 6 | export class Shotgun extends SpecialSystem { 7 | 8 | handleEvent(): void { 9 | for(let i = 0; i < 5; i++) { 10 | const spawnLocation = this.target.getBulletSpawnLocation(); 11 | const bullet: Bullet = new Bullet({ 12 | damage: ( ( this.target.getDamage() + this.target.tempSpecialDamage ) / 3 ) * this.target.tempSpecialDamagePercent, 13 | speed: 500, 14 | range: 250, 15 | collisionType: CT.CIRCLE, 16 | radius: 15, 17 | bulletMesh: "Cannon", 18 | x: spawnLocation.x, 19 | y: spawnLocation.y, 20 | bulletType: C.SHIP_BULLET 21 | }); 22 | bullet.registerBehaviour("path", new StraightAnglePath(bullet, {angle: ((i * 10) + 70) * (Math.PI/180)})); 23 | bullet.firedBy = this.target; 24 | this.target.$state.addBullet(bullet); 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/spawner/patterns/HorizontalLine.ts: -------------------------------------------------------------------------------- 1 | import { TimedPosition } from '../TimedPosition'; 2 | import { Pattern } from '../Pattern'; 3 | 4 | export class HorizontalLine extends Pattern { 5 | 6 | constructor(enemyCount: number, enemyType: any, difficulty: number ) { 7 | super(); 8 | this.points = [ 9 | new TimedPosition(0, 800, 1100), 10 | new TimedPosition(0, 900, 1100), 11 | new TimedPosition(0, 700, 1100), 12 | new TimedPosition(0, 600, 1100), 13 | new TimedPosition(0, 1000, 1100), 14 | new TimedPosition(0, 500, 1100), 15 | new TimedPosition(0, 1100, 1100), 16 | new TimedPosition(0, 400, 1100), 17 | new TimedPosition(0, 1200, 1100), 18 | new TimedPosition(0, 300, 1100), 19 | new TimedPosition(0, 1300, 1100), 20 | new TimedPosition(0, 200, 1100), 21 | new TimedPosition(0, 1400, 1100), 22 | new TimedPosition(0, 100, 1100), 23 | new TimedPosition(0, 1500, 1100), 24 | ]; 25 | this.maxTime = 2; 26 | this.enemyCount = enemyCount; 27 | this.enemyType = enemyType; 28 | this.difficulty = difficulty; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /test/PatternImage.ts: -------------------------------------------------------------------------------- 1 | import { GameState } from '../src/models/GameState'; 2 | import { Spawner } from '../src/spawner2/Spawner'; 3 | 4 | let room = { 5 | state: new GameState(), 6 | announceNextWave: () => { 7 | //do nothing. 8 | } 9 | } 10 | room.state.currentWave = 1; 11 | 12 | let spawner = new Spawner(room); 13 | 14 | let spawns = spawner.getSpawns(); 15 | 16 | for(let i = 0; i < spawns.length; i++) { 17 | console.log(spawns[i].time, spawns[i].enemy.constructor.name, spawns[i].enemy.position.x, spawns[i].enemy.position.y); 18 | } 19 | 20 | 21 | room.state.currentWave = 5; 22 | spawner = new Spawner(room); 23 | spawns = spawner.getSpawns(); 24 | 25 | for(let i = 0; i < spawns.length; i++) { 26 | console.log(spawns[i].time, spawns[i].enemy.constructor.name, spawns[i].enemy.position.x, spawns[i].enemy.position.y); 27 | } 28 | 29 | room.state.currentWave = 10; 30 | spawner = new Spawner(room); 31 | spawns = spawner.getSpawns(); 32 | 33 | for(let i = 0; i < spawns.length; i++) { 34 | console.log(spawns[i].time, spawns[i].enemy.constructor.name, spawns[i].enemy.position.x, spawns[i].enemy.position.y); 35 | } 36 | -------------------------------------------------------------------------------- /src/models/special/ScatterShot.ts: -------------------------------------------------------------------------------- 1 | /* Fires a spread of bullets from the ship */ 2 | import { SpecialSystem } from './SpecialSystem'; 3 | import { C, CT } from '../../Constants'; 4 | import { StraightAnglePath} from '../../behaviours/bullet/StraightAnglePath'; 5 | import { Bullet } from '../Bullet'; 6 | 7 | export class ScatterShot extends SpecialSystem { 8 | 9 | handleEvent(): void { 10 | for(let i = 0; i < 18; i++) { 11 | const spawnLocation = this.target.getBulletSpawnLocation(); 12 | const bullet: Bullet = new Bullet({ 13 | damage: ( ( this.target.getDamage() + this.target.tempSpecialDamage ) / 5 ) * this.target.tempSpecialDamagePercent, 14 | speed: 500, 15 | range: 250, 16 | collisionType: CT.CIRCLE, 17 | radius: 15, 18 | bulletMesh: "Cannon", 19 | x: spawnLocation.x, 20 | y: spawnLocation.y, 21 | bulletType: C.SHIP_BULLET 22 | }); 23 | bullet.registerBehaviour("path", new StraightAnglePath(bullet, {angle: i * 20 * (Math.PI/180)})); 24 | bullet.firedBy = this.target; 25 | this.target.$state.addBullet(bullet); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/spawner/Pattern.ts: -------------------------------------------------------------------------------- 1 | import { TimedPosition } from './TimedPosition'; 2 | import { Spawn } from './Spawn'; 3 | import { cloneDeep } from 'lodash'; 4 | 5 | export class Pattern { 6 | 7 | /** 8 | * Points must be kept in reverse order that they will be spawned 9 | **/ 10 | points: TimedPosition[]; 11 | maxTime: number; 12 | enemyCount: number; 13 | enemyType: any; 14 | difficulty: number; 15 | positionOffset = 0; 16 | 17 | getSpawns(timeOffset = 0): Spawn[] { 18 | const spawns: Spawn[] = []; 19 | for(let i = this.positionOffset, l = Math.min(this.points.length, this.enemyCount); i < l; i++) { 20 | const point: TimedPosition = this.points[i]; 21 | if(point == null || point.time == null) { 22 | console.log('[Pattern (error)] ', this.constructor.name, this.enemyType.constructor.name, i); 23 | continue; 24 | } 25 | const spawn: Spawn = new Spawn( 26 | point.time + timeOffset, 27 | new this.enemyType({position: point.clone()}) 28 | ) 29 | spawns.push(spawn); 30 | } 31 | return spawns; 32 | } 33 | 34 | clone(): Pattern { 35 | return cloneDeep(this); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/spawner/patterns/TripleVerticalLine.ts: -------------------------------------------------------------------------------- 1 | import { TimedPosition } from '../TimedPosition'; 2 | import { Pattern } from '../Pattern'; 3 | 4 | export class TripleVerticalLine extends Pattern { 5 | 6 | constructor(enemyCount: number, enemyType: any, difficulty: number ) { 7 | super(); 8 | const column1 = Math.floor(Math.random() * 15); 9 | let column2 = Math.floor(Math.random() * 15); 10 | let column3 = Math.floor(Math.random() * 15); 11 | while( column1 == column2 ) { 12 | column2 = Math.floor(Math.random() * 15); 13 | } 14 | while( column3 == column2 || column3 == column1) { 15 | column3 = Math.floor(Math.random() * 15); 16 | } 17 | 18 | this.points = []; 19 | for(let i = 0; i < 8; i++) { 20 | this.points.push(new TimedPosition(i * 2, 100 + column1 * 100, 1100)) 21 | this.points.push(new TimedPosition(i * 2, 100 + column2 * 100, 1100)) 22 | this.points.push(new TimedPosition(i * 2, 100 + column3 * 100, 1100)) 23 | } 24 | this.maxTime = Math.ceil(2 * (enemyCount/3)); 25 | this.enemyCount = enemyCount; 26 | this.enemyType = enemyType; 27 | this.difficulty = difficulty; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/models/special/Bomb.ts: -------------------------------------------------------------------------------- 1 | import { SpecialSystem } from './SpecialSystem'; 2 | import { Bullet } from '../Bullet'; 3 | import { StraightLineUpPath } from '../../behaviours/bullet/StraightLineUpPath'; 4 | import { ExplodeBehaviour } from '../../behaviours/bullet/ExplodeBehaviour'; 5 | import { C, CT } from '../../Constants'; 6 | 7 | export class Bomb extends SpecialSystem { 8 | 9 | handleEvent(): void { 10 | const spawnLocation = this.target.getBulletSpawnLocation(); 11 | const bullet: Bullet = new Bullet({ 12 | damage: ( ( this.target.getDamage() + this.target.tempSpecialDamage ) / 3 ) * this.target.tempSpecialDamagePercent, 13 | speed: 250, 14 | range: 500, 15 | collisionType: CT.CIRCLE, 16 | radius: 20, 17 | x: spawnLocation.x, 18 | y: spawnLocation.y, 19 | bulletType: C.SHIP_BULLET, 20 | bulletMesh: "Bomb", 21 | blastRadius: 300, 22 | explodes: true 23 | }); 24 | bullet.registerBehaviour("path", new StraightLineUpPath(bullet)); 25 | bullet.registerBehaviour("explode", new ExplodeBehaviour(bullet)); 26 | bullet.firedBy = this.target; 27 | this.target.$state.addBullet(bullet); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/models/special/MegaBomb.ts: -------------------------------------------------------------------------------- 1 | import { SpecialSystem } from './SpecialSystem'; 2 | import { Bullet } from '../Bullet'; 3 | import { StraightLineUpPath } from '../../behaviours/bullet/StraightLineUpPath'; 4 | import { ExplodeBehaviour } from '../../behaviours/bullet/ExplodeBehaviour'; 5 | import { C, CT } from '../../Constants'; 6 | 7 | export class MegaBomb extends SpecialSystem { 8 | 9 | handleEvent(): void { 10 | const spawnLocation = this.target.getBulletSpawnLocation(); 11 | const bullet: Bullet = new Bullet({ 12 | damage: ( ( this.target.getDamage() + this.target.tempSpecialDamage ) / 3 ) * this.target.tempSpecialDamagePercent, 13 | speed: 250, 14 | range: 500, 15 | collisionType: CT.CIRCLE, 16 | radius: 25, 17 | bulletMesh: "MegaBomb", 18 | x: spawnLocation.x, 19 | y: spawnLocation.y, 20 | bulletType: C.SHIP_BULLET, 21 | explodes: true, 22 | blastRadius: 400 23 | }); 24 | bullet.registerBehaviour("path", new StraightLineUpPath(bullet)); 25 | bullet.registerBehaviour("explode", new ExplodeBehaviour(bullet)); 26 | bullet.firedBy = this.target; 27 | this.target.$state.addBullet(bullet); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/spawner/patterns/BothSideLine.ts: -------------------------------------------------------------------------------- 1 | import { TimedPosition } from '../TimedPosition'; 2 | import { Pattern } from '../Pattern'; 3 | 4 | export class BothSideLine extends Pattern { 5 | 6 | constructor(enemyCount: number, enemyType: any, difficulty: number, timeOffset = 0, positionOffset = 0 ) { 7 | super(); 8 | this.points = [ 9 | new TimedPosition(0 + timeOffset, -200, 800), 10 | new TimedPosition(0 + timeOffset, 1800, 800), 11 | new TimedPosition(0 + timeOffset, -200, 700), 12 | new TimedPosition(0 + timeOffset, 1800, 700), 13 | new TimedPosition(0 + timeOffset, -200, 600), 14 | new TimedPosition(0 + timeOffset, 1800, 600), 15 | new TimedPosition(0 + timeOffset, -200, 500), 16 | new TimedPosition(0 + timeOffset, 1800, 500), 17 | new TimedPosition(0 + timeOffset, -200, 400), 18 | new TimedPosition(0 + timeOffset, 1800, 400), 19 | new TimedPosition(0 + timeOffset, -200, 300), 20 | new TimedPosition(0 + timeOffset, 1800, 300), 21 | ]; 22 | this.maxTime = 2; 23 | this.enemyCount = enemyCount; 24 | this.enemyType = enemyType; 25 | this.difficulty = difficulty; 26 | this.positionOffset = positionOffset; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/behaviours/player/InputBehaviour.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { Ship } from '../../models/Ship'; 3 | 4 | export class InputBehaviour extends Behaviour { 5 | 6 | horizontalVector = 0; 7 | verticalVector = 0; 8 | 9 | target: Ship; 10 | 11 | constructor(target: Ship) { 12 | super('input', target); 13 | } 14 | 15 | public onEvent(args: {horizontal?: number; vertical?: number; primary_attack?: number; special_attack?: number}): void { 16 | if(args.horizontal) { 17 | this.horizontalVector = args.horizontal; 18 | } 19 | if(args.vertical) { 20 | this.verticalVector = args.vertical; 21 | } 22 | if(args.primary_attack) { 23 | this.target.handleEvent('primary_attack'); 24 | } 25 | if(args.special_attack) { 26 | this.target.handleEvent('special_attack'); 27 | } 28 | } 29 | 30 | public onUpdate(deltaTime: number): void { 31 | this.target.lastPosition = this.target.position.clone(); 32 | this.target.position.x += this.horizontalVector * this.target.speed * (deltaTime / 1000); 33 | this.target.position.y += this.verticalVector * this.target.speed * (deltaTime / 1000); 34 | this.target.clampToBounds(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/behaviours/player/PrimaryAttackBehaviour.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { PRIMARY } from '../../Primary'; 3 | import { Ship } from '../../models/Ship'; 4 | import { Primary } from '../../models/primary/Primary'; 5 | 6 | 7 | export class PrimaryAttackBehaviour extends Behaviour { 8 | 9 | private system: Primary; 10 | target: Ship; 11 | 12 | constructor(target: Ship) { 13 | super('primary_attack', target); 14 | 15 | this.system = PRIMARY.getSystem(this.target.primaryWeapon, this.target) 16 | this.target.primaryCooldownMax = this.system.fireRate; 17 | this.target.primaryCooldown = this.system.fireRate; 18 | } 19 | 20 | public onEvent(): void { 21 | if(!this.canFire()) return; 22 | this.target.primaryCooldown = 0; 23 | 24 | this.system.spawnBullets(this.target); 25 | 26 | if(this.target.weaponCharge != 1) { 27 | this.target.setWeaponCharge(1); 28 | } 29 | } 30 | 31 | public onUpdate(deltaTime: number): void { 32 | if(this.target.primaryCooldown <= this.target.primaryCooldownMax) { 33 | this.target.primaryCooldown += deltaTime; 34 | } 35 | } 36 | 37 | canFire(): boolean { 38 | return this.target.primaryCooldown >= this.target.primaryCooldownMax; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/models/Bullet.ts: -------------------------------------------------------------------------------- 1 | import { type } from "@colyseus/schema"; 2 | 3 | import { Entity } from './Entity'; 4 | 5 | import { merge } from 'lodash'; 6 | 7 | import { DestroyedBehaviour } from '../behaviours/bullet/DestroyedBehaviour'; 8 | 9 | import { CT } from '../Constants'; 10 | 11 | export class Bullet extends Entity { 12 | 13 | /* An enemy bullet or a ship bullet. This determines which collision to check */ 14 | bulletType: number; 15 | 16 | /* The damage done by this bullet */ 17 | damage: number; 18 | 19 | /* The speed the bullet travels */ 20 | speed: number; 21 | 22 | /* The distance the bullet will travel */ 23 | range: number; 24 | 25 | /* The angle for the bullet to travel */ 26 | angle: number; 27 | 28 | /* Tracks who fired this entity */ 29 | firedBy: Entity; 30 | 31 | /** If there is a splash effect **/ 32 | explodes = false; 33 | 34 | @type("number") 35 | blastRadius = 0; 36 | 37 | /* The mesh to display for the bullet */ 38 | @type("string") 39 | bulletMesh: string; 40 | 41 | collision_tpe = CT.CIRCLE; 42 | radius = 15; 43 | 44 | constructor(options: any) { 45 | super(options); 46 | merge(this, options); 47 | this.registerBehaviour("destroyed", new DestroyedBehaviour(this)); 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/models/bosses/Disk.ts: -------------------------------------------------------------------------------- 1 | import { Enemy } from '../Enemy'; 2 | import { DiskMovement } from '../../behaviours/boss/DiskMovement'; 3 | import { DropReward } from '../../behaviours/boss/DropReward'; 4 | import { Position } from '../Position'; 5 | import { CT } from '../../Constants'; 6 | 7 | export enum DiskState { 8 | ENTER_SCREEN, 9 | ATTACK, 10 | MOVE 11 | } 12 | 13 | export class Disk extends Enemy { 14 | 15 | constructor(options) { 16 | super(options); 17 | this.position = new Position(800, 1200); 18 | this.state = DiskState.ENTER_SCREEN; 19 | 20 | this.healthBase = 25; 21 | this.healthGrowth = 1; 22 | 23 | this.speedBase = 75; 24 | this.speedGrowth = 5; 25 | 26 | this.collisionDamageBase = 1; 27 | this.collisionDamageGrowth = 0.1; 28 | 29 | this.damageBase = 1; 30 | this.damageGrowth = 0.2; 31 | 32 | this.rangeBase = 1200; 33 | this.rangeGrowth = 0; 34 | 35 | this.modelType = "disk"; 36 | 37 | this.collisionType = CT.ELLIPSE; 38 | this.radiusX = 225; 39 | this.radiusY = 100; 40 | } 41 | 42 | onInitGame(state: any): void { 43 | super.onInitGame(state); 44 | this.registerBehaviour("path", new DiskMovement(this)); 45 | this.registerBehaviour("reward", new DropReward(this)); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/models/primary/Primary.ts: -------------------------------------------------------------------------------- 1 | import { merge } from 'lodash'; 2 | import { Entity } from '../Entity'; 3 | import { Ship } from '../Ship'; 4 | import { Bullet } from '../Bullet'; 5 | 6 | export class Primary { 7 | 8 | damage: number; 9 | speed: number; 10 | range: number; 11 | radius: number; 12 | fireRate: number; 13 | 14 | bulletCount: number; 15 | bulletAngle: number; 16 | bulletOffset: number; 17 | 18 | bulletMesh: string; 19 | 20 | blastRadius: number; 21 | 22 | entity: Entity; 23 | 24 | behaviour: string; 25 | 26 | constructor(entity: Entity, options: any) { 27 | merge(this, options); 28 | this.entity = entity; 29 | 30 | if(this.entity instanceof Ship) { 31 | this.damage = (this.entity as Ship).getDamage() * this.damage; 32 | this.range = (this.entity as Ship).getRange() * this.range; 33 | this.fireRate = (this.entity as Ship).getFireRate() * this.fireRate; 34 | } 35 | } 36 | 37 | getBullets(): Bullet[] { return []; } 38 | 39 | spawnBullets(firedBy?: Entity): void { 40 | const bullets = this.getBullets(); 41 | if(firedBy) { 42 | for(let i = 0, l = bullets.length; i < l; i++) { 43 | bullets[i].firedBy = firedBy; 44 | } 45 | } 46 | this.entity.$state.addBullets(bullets); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/helpers/JWTHelper.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | 3 | const AUTH_SECRET = process.env.AUTH_SECRET || 'change_me_make_me_secret'; 4 | const MAX_AGE = 3600; 5 | 6 | export class JWTHelper { 7 | public static createJWToken(details): string { 8 | if (typeof details !== 'object') details = {}; 9 | 10 | if(!details.maxAge || typeof details.maxAge !== 'number') details.maxAge = 3600; 11 | 12 | const token = jwt.sign({ 13 | data: details.sessionData 14 | }, AUTH_SECRET, { 15 | expiresIn: details.maxAge, 16 | algorithm: 'HS256' 17 | }); 18 | 19 | return token 20 | } 21 | 22 | public static verifyToken(token): boolean { 23 | try { 24 | jwt.verify(token, AUTH_SECRET, { algorithms: ['HS256']}); 25 | return true; 26 | } catch (e) { 27 | return false; 28 | } 29 | } 30 | 31 | public static extractUsernameFromToken(token): string { 32 | const decodedToken = jwt.decode(token); 33 | return decodedToken.data 34 | } 35 | 36 | public static getSuccessJSON(username): any { 37 | return { 38 | success: true, 39 | token: JWTHelper.createJWToken({ 40 | sessionData: username, 41 | maxAge: MAX_AGE 42 | }), 43 | expiresIn: MAX_AGE, 44 | username: username 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/models/bosses/WingedDevil.ts: -------------------------------------------------------------------------------- 1 | import { Enemy } from '../Enemy'; 2 | import { WingedDevilMovement } from '../../behaviours/boss/WingedDevilMovement'; 3 | import { DropReward } from '../../behaviours/boss/DropReward'; 4 | import { Position } from '../Position'; 5 | import { CT } from '../../Constants'; 6 | 7 | export enum WingedDevilState { 8 | ENTER_SCREEN, 9 | ATTACK, 10 | MOVE 11 | } 12 | 13 | export class WingedDevil extends Enemy { 14 | 15 | constructor(options) { 16 | super(options); 17 | this.position = new Position(800, 1200); 18 | this.state = WingedDevilState.ENTER_SCREEN; 19 | 20 | this.healthBase = 25; 21 | this.healthGrowth = 1; 22 | 23 | this.speedBase = 75; 24 | this.speedGrowth = 5; 25 | 26 | this.collisionDamageBase = 1; 27 | this.collisionDamageGrowth = 0.1; 28 | 29 | this.damageBase = 1; 30 | this.damageGrowth = 0.2; 31 | 32 | this.rangeBase = 1200; 33 | this.rangeGrowth = 0; 34 | 35 | this.modelType = "wingedDevil"; 36 | 37 | this.collisionType = CT.CIRCLE; 38 | this.radius = 250; 39 | } 40 | 41 | onInitGame(state: any): void { 42 | super.onInitGame(state); 43 | this.registerBehaviour("path", new WingedDevilMovement(this)); 44 | this.registerBehaviour("reward", new DropReward(this)); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/models/bosses/Eagle.ts: -------------------------------------------------------------------------------- 1 | import { Enemy } from '../Enemy'; 2 | import { EagleMovement } from '../../behaviours/boss/EagleMovement'; 3 | import { DropReward } from '../../behaviours/boss/DropReward'; 4 | import { Position } from '../Position'; 5 | import { CT } from '../../Constants'; 6 | 7 | export enum EagleState { 8 | WAIT, 9 | ENTER_SCREEN, 10 | ATTACK, 11 | SPAWN, 12 | MOVE 13 | } 14 | 15 | export class Eagle extends Enemy { 16 | 17 | constructor(options) { 18 | super(options); 19 | this.position = new Position(800, 1200); 20 | this.state = EagleState.ENTER_SCREEN; 21 | 22 | this.healthBase = 25; 23 | this.healthGrowth = 1; 24 | 25 | this.speedBase = 75; 26 | this.speedGrowth = 5; 27 | 28 | this.collisionDamageBase = 1; 29 | this.collisionDamageGrowth = 0.1; 30 | 31 | this.damageBase = 1; 32 | this.damageGrowth = 0.2; 33 | 34 | this.rangeBase = 1200; 35 | this.rangeGrowth = 0; 36 | 37 | this.modelType = "eagle"; 38 | 39 | this.collisionType = CT.ELLIPSE; 40 | this.radiusX = 225; 41 | this.radiusY = 100; 42 | } 43 | 44 | onInitGame(state: any): void { 45 | super.onInitGame(state); 46 | this.registerBehaviour("path", new EagleMovement(this)); 47 | this.registerBehaviour("reward", new DropReward(this)); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/models/bosses/TheKiller.ts: -------------------------------------------------------------------------------- 1 | import { Enemy } from '../Enemy'; 2 | import { TheKillerMovement } from '../../behaviours/boss/TheKillerMovement'; 3 | import { DropReward } from '../../behaviours/boss/DropReward'; 4 | import { Position } from '../Position'; 5 | import { CT } from '../../Constants'; 6 | 7 | export enum TheKillerState { 8 | ENTER_SCREEN, 9 | ATTACK, 10 | MOVE 11 | } 12 | 13 | export class TheKiller extends Enemy { 14 | 15 | constructor(options) { 16 | super(options); 17 | this.position = new Position(800, 1200); 18 | this.state = TheKillerState.ENTER_SCREEN; 19 | 20 | this.healthBase = 25; 21 | this.healthGrowth = 1; 22 | 23 | this.speedBase = 75; 24 | this.speedGrowth = 5; 25 | 26 | this.collisionDamageBase = 1; 27 | this.collisionDamageGrowth = 0.1; 28 | 29 | this.damageBase = 1; 30 | this.damageGrowth = 0.2; 31 | 32 | this.rangeBase = 1200; 33 | this.rangeGrowth = 0; 34 | 35 | this.modelType = "eagle"; 36 | 37 | this.collisionType = CT.ELLIPSE; 38 | this.radiusX = 225; 39 | this.radiusY = 100; 40 | } 41 | 42 | onInitGame(state: any): void { 43 | super.onInitGame(state); 44 | this.registerBehaviour("path", new TheKillerMovement(this)); 45 | this.registerBehaviour("reward", new DropReward(this)); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/models/Drop.ts: -------------------------------------------------------------------------------- 1 | import { type } from "@colyseus/schema"; 2 | import { Entity } from './Entity'; 3 | import { DestroyedBehaviour } from '../behaviours/drop/DestroyedBehaviour'; 4 | import { MoveToLocationPath } from '../behaviours/drop/MoveToLocationPath'; 5 | import { DespawnAfterTime } from '../behaviours/drop/DespawnAfterTime'; 6 | import { Crystals } from '../Crystals'; 7 | import { C } from '../Constants'; 8 | 9 | export class Drop extends Entity { 10 | 11 | speed = 65; 12 | radius = 25; 13 | 14 | @type("string") 15 | modelType = ""; 16 | 17 | constructor(options) { 18 | super(options); 19 | const r = Math.floor(Math.random() * 4); 20 | 21 | switch(r) { 22 | case 0: 23 | this.modelType = "red"; 24 | break; 25 | case 1: 26 | this.modelType = "green"; 27 | break; 28 | case 2: 29 | this.modelType = "blue"; 30 | break; 31 | case 3: 32 | this.modelType = "purple"; 33 | break; 34 | } 35 | } 36 | 37 | onInitGame(state: any): void { 38 | super.onInitGame(state); 39 | this.registerBehaviour("destroyed", new DestroyedBehaviour(this)); 40 | this.registerBehaviour("path", new MoveToLocationPath(this, C.CENTER_OF_SCREEN.clone())); 41 | this.registerBehaviour("despawn", new DespawnAfterTime(this)); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/models/primary/Blaster.ts: -------------------------------------------------------------------------------- 1 | import { Bullet } from '../Bullet'; 2 | import { Entity } from '../Entity'; 3 | import { C, CT } from '../../Constants'; 4 | import { StraightLineUpPath } from '../../behaviours/bullet/StraightLineUpPath'; 5 | import { Primary } from './Primary'; 6 | 7 | export class Blaster extends Primary{ 8 | 9 | constructor(entity: Entity, options: any) { 10 | super(entity, options); 11 | } 12 | 13 | getBullets(): Bullet[] { 14 | const bullets: Bullet[] = []; 15 | 16 | let offsetStart = 0; 17 | if(this.bulletOffset != 0) { 18 | offsetStart = 10 - ((this.bulletCount * this.bulletOffset) / 2); 19 | } 20 | 21 | for(let i = 0; i < this.bulletCount; i++){ 22 | 23 | const options = { 24 | damage: this.damage, 25 | speed: this.speed, 26 | range: this.range, 27 | collisionType: CT.CIRCLE, 28 | radius: this.radius, 29 | bulletMesh: this.bulletMesh, 30 | position: this.entity.getBulletSpawnLocation(), 31 | bulletType: C.SHIP_BULLET 32 | } 33 | 34 | const bullet = new Bullet(options); 35 | bullet.position.x = bullet.position.x + offsetStart + (i * this.bulletOffset); 36 | bullet.registerBehaviour("path", new StraightLineUpPath(bullet)); 37 | bullets.push(bullet); 38 | } 39 | 40 | return bullets; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/models/enemies/Bomber.ts: -------------------------------------------------------------------------------- 1 | import { Enemy } from '../Enemy'; 2 | import { LoopingPath } from '../../behaviours/Enemy/movement/LoopingPath'; 3 | import { FiresBulletBehaviour } from '../../behaviours/Enemy/FiresBulletBehaviour'; 4 | import { EnemyBullet } from '../../models/primary/EnemyBullet'; 5 | import { C, CT } from '../../Constants'; 6 | 7 | 8 | export class Bomber extends Enemy { 9 | 10 | constructor(options: any) { 11 | super(options); 12 | this.healthBase = 4; 13 | this.healthGrowth = 0.1; 14 | 15 | this.speedBase = 65; 16 | this.speedGrowth = 5; 17 | 18 | this.collisionDamageBase = 1; 19 | this.collisionDamageGrowth = 0.1; 20 | 21 | this.modelType = "bomber"; 22 | 23 | this.radius = 40; 24 | } 25 | 26 | onInitGame(state: any): void { 27 | super.onInitGame(state); 28 | this.registerBehaviour("path", new LoopingPath(this)); 29 | 30 | const bulletOptions = { 31 | system: EnemyBullet, 32 | damage: 1, 33 | speed: 200, 34 | range: 700, 35 | collisionType: CT.CIRCLE, 36 | radius: 25, 37 | bulletMesh: "Enemy1", 38 | position: this.position.clone(), 39 | bulletType: C.ENEMY_BULLET, 40 | cooldown: 3000, 41 | behaviour: 'drops' 42 | } 43 | this.registerBehaviour("primary", new FiresBulletBehaviour(this, bulletOptions)); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/models/enemies/Fang.ts: -------------------------------------------------------------------------------- 1 | import { Enemy } from '../Enemy'; 2 | import { ClosestPlayerPath } from '../../behaviours/Enemy/movement/ClosestPlayerPath'; 3 | import { FiresBulletBehaviour } from '../../behaviours/Enemy/FiresBulletBehaviour'; 4 | import { EnemyBullet } from '../../models/primary/EnemyBullet'; 5 | import { C, CT } from '../../Constants'; 6 | 7 | 8 | export class Fang extends Enemy { 9 | 10 | constructor(options: any) { 11 | super(options); 12 | this.healthBase = 5; 13 | this.healthGrowth = 0.2; 14 | 15 | this.speedBase = 75; 16 | this.speedGrowth = 5; 17 | 18 | this.collisionDamageBase = 1; 19 | this.collisionDamageGrowth = 0.1; 20 | 21 | this.modelType = "fang"; 22 | 23 | this.radius = 40; 24 | } 25 | 26 | onInitGame(state: any): void { 27 | super.onInitGame(state); 28 | this.registerBehaviour("path", new ClosestPlayerPath(this)); 29 | 30 | const bulletOptions = { 31 | system: EnemyBullet, 32 | damage: 1, 33 | speed: 300, 34 | range: 500, 35 | collisionType: CT.CIRCLE, 36 | radius: 25, 37 | bulletMesh: "Enemy1", 38 | position: this.position.clone(), 39 | bulletType: C.ENEMY_BULLET, 40 | cooldown: 3000, 41 | behaviour: 'fires' 42 | } 43 | this.registerBehaviour("primary", new FiresBulletBehaviour(this, bulletOptions)); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/behaviours/behaviour.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../models/Entity'; 2 | /** 3 | * A base class to be extended from when creating custom behaviours. 4 | * A behaviour could be handling input, movement patterns, or 5 | * tracking damage taken. 6 | **/ 7 | export class Behaviour { 8 | public target: Entity; 9 | public eventType: string; 10 | 11 | private _enabled = true; 12 | 13 | constructor(type: string, target: any, onCompleteState?: number) { 14 | this.eventType = type; 15 | this.target = target; 16 | } 17 | 18 | /** 19 | * Methods to override in an extended Behaviour. 20 | * Not all methods must be overridden, only those used 21 | * by the behaviour. 22 | **/ 23 | public onEvent(args: any): void { 24 | // do nothing. 25 | } 26 | public onUpdate(deltaTime: number): void { 27 | // do nothing. 28 | } 29 | public onEnabled(): void { 30 | // do nothing. 31 | } 32 | public onDisabled(): void { 33 | // do nothing. 34 | } 35 | public onRegistered(): void { 36 | // do nothing. 37 | } 38 | public onRemoved(): void { 39 | // do nothing. 40 | } 41 | 42 | public enable(): void { 43 | this._enabled = true; 44 | this.onEnabled(); 45 | } 46 | 47 | public disable(): void { 48 | this._enabled = false; 49 | this.onDisabled(); 50 | } 51 | 52 | public isEnabled(): boolean { 53 | return this._enabled; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/behaviours/enemy/movement/LoopingPath.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../../behaviour'; 2 | import { CollisionHelper } from '../../../helpers/CollisionHelper'; 3 | import { Enemy } from '../../../models/Enemy'; 4 | 5 | export class LoopingPath extends Behaviour { 6 | 7 | dir = 0; 8 | 9 | enteredScreen = false; 10 | 11 | target: Enemy; 12 | 13 | constructor(target: Enemy) { 14 | super('LoopingPath', target); 15 | 16 | const dx = 800 - this.target.position.x; 17 | const dy = 450 - this.target.position.y; 18 | 19 | this.target.angle = Math.atan2(dy,dx); 20 | this.dir = Math.random() < 0.5 ? 1 : -1; 21 | } 22 | 23 | onUpdate(deltaTime): void { 24 | 25 | this.target.angle += this.dir * Math.PI / 20 * (deltaTime/1000); 26 | this.target.angle = this.target.angle; 27 | 28 | this.target.position.x += this.target.speed * Math.cos(this.target.angle) * (deltaTime/1000); 29 | this.target.position.y += this.target.speed * Math.sin(this.target.angle) * (deltaTime/1000); 30 | 31 | if(!this.enteredScreen && CollisionHelper.insideBounds(this.target)){ 32 | this.enteredScreen = true; 33 | } 34 | if(this.enteredScreen && CollisionHelper.outsideBounds(this.target) || CollisionHelper.tooFar(this.target)) { 35 | this.target.handleEvent('destroyed'); 36 | } 37 | } 38 | 39 | remove(): void { 40 | this.target.$state.removeEnemy(this.target); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/behaviours/enemy/movement/TargetPlayerStartPath.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../../behaviour'; 2 | import { CollisionHelper } from '../../../helpers/CollisionHelper'; 3 | import { Enemy, Ship } from '../../../models'; 4 | 5 | export class TargetPlayerStartPath extends Behaviour { 6 | 7 | enteredScreen = false; 8 | 9 | target: Enemy; 10 | 11 | constructor(target: Enemy) { 12 | super('TargetPlayerStartPath', target); 13 | const ship: Ship = this.target.$state.getClosestShip(this.target.position.x, this.target.position.y); 14 | if(ship == null){ 15 | console.log("Error: Ship is null in TargetPlayerStartPath"); 16 | this.target.handleEvent('destroyed'); 17 | return; 18 | } 19 | const dx = this.target.position.x - ship.position.x; 20 | const dy = this.target.position.y - ship.position.y; 21 | this.target.angle = Math.atan2(dy, dx); 22 | } 23 | 24 | onUpdate(deltaTime): void { 25 | this.target.position.x += -Math.cos(this.target.angle) * this.target.speed * deltaTime/1000; 26 | this.target.position.y += -Math.sin(this.target.angle) * this.target.speed * deltaTime/1000; 27 | 28 | if(!this.enteredScreen && CollisionHelper.insideBounds(this.target)){ 29 | this.enteredScreen = true; 30 | } 31 | if(this.enteredScreen && CollisionHelper.outsideBounds(this.target) || CollisionHelper.tooFar(this.target)) { 32 | this.target.handleEvent('destroyed'); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Constants.ts: -------------------------------------------------------------------------------- 1 | import { Bounds } from './helpers/Bounds'; 2 | import { Position } from './models/Position'; 3 | 4 | /* Game constants*/ 5 | export class C { 6 | public static BOUNDS: Bounds = new Bounds(0, 1600, 0, 900); 7 | public static SPAWN_OFFSET = 96; //This may need to be bigger to provide more warning, if warning is provided 8 | public static CIRCLE = 0; 9 | public static RECTANGLE = 1; 10 | public static SHIP_BULLET = 0; 11 | public static ENEMY_BULLET = 1; 12 | 13 | public static get RANDOM_X_ON_SCREEN(): number { 14 | return Math.random() * ( C.BOUNDS.maxX - C.BOUNDS.minX - C.SPAWN_OFFSET*2 ) + C.SPAWN_OFFSET; 15 | } 16 | 17 | public static get RANDOM_Y_ON_SCREEN(): number { 18 | return Math.random() * ( C.BOUNDS.maxY - C.BOUNDS.minY - C.SPAWN_OFFSET*2 ) + C.SPAWN_OFFSET; 19 | } 20 | 21 | public static CENTER_OF_SCREEN: Position = new Position(800, 450); 22 | 23 | public static RANDOM_ON_SCREEN(): Position { 24 | return new Position(C.RANDOM_X_ON_SCREEN, C.RANDOM_Y_ON_SCREEN); 25 | } 26 | } 27 | 28 | /* collision layers used by rbush */ 29 | export class L { 30 | public static SHIP = 0; 31 | public static ENEMIES = 1; 32 | public static ENEMY_BULLETS = 2; 33 | public static SHIP_BULLETS = 3; 34 | } 35 | 36 | export class CT { 37 | public static CIRCLE = 0; 38 | public static ELLIPSE = 1; 39 | public static BOX = 2; 40 | } 41 | 42 | export class S { 43 | public static TOP = 0; 44 | public static LEFT = 1; 45 | public static RIGHT = 2; 46 | } 47 | -------------------------------------------------------------------------------- /src/behaviours/player/SpecialAttackBehaviour.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { SPECIAL } from '../../Special'; 3 | import { SHIP } from '../../Ship'; 4 | import { Ship } from '../../models/Ship'; 5 | import { SpecialSystem } from '../../models/special/SpecialSystem'; 6 | 7 | 8 | export class SpecialAttackBehaviour extends Behaviour { 9 | 10 | private system: SpecialSystem; 11 | 12 | target: Ship; 13 | 14 | constructor(target: Ship) { 15 | super('special_attack', target); 16 | const systemType = SPECIAL.TYPE[this.target.specialWeapon]["systemType"]; 17 | this.system = new systemType(this.target); 18 | this.system.duration = SPECIAL.TYPE[this.target.specialWeapon]["duration"] || 0; 19 | this.system.amount = SPECIAL.TYPE[this.target.specialWeapon]["amount"] || 0; 20 | this.target.specialCooldownMax = SPECIAL.TYPE[this.target.specialWeapon]["fireRate"] * SHIP.TYPE[this.target.shipType]["special"]; 21 | this.target.specialCooldown = this.target.specialCooldownMax; 22 | } 23 | 24 | public onEvent(): void { 25 | if(!this.canFire()) return; 26 | this.target.specialCooldown = 0; 27 | this.system.handleEvent(); 28 | } 29 | 30 | public onUpdate(deltaTime: number): void { 31 | if(this.target.specialCooldown <= this.target.specialCooldownMax) { 32 | this.target.specialCooldown += deltaTime; 33 | } 34 | this.system.handleUpdate(deltaTime); 35 | } 36 | 37 | canFire(): boolean { 38 | return this.target.specialCooldown >= this.target.specialCooldownMax; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/behaviours/enemy/movement/StraightLinePath.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../../behaviour'; 2 | import { C } from '../../../Constants'; 3 | import { CollisionHelper } from '../../../helpers/CollisionHelper'; 4 | import { Enemy } from '../../../models/Enemy'; 5 | 6 | export class StraightLinePath extends Behaviour { 7 | 8 | xDir = 0; 9 | yDir = 0; 10 | 11 | enteredScreen = false; 12 | 13 | target: Enemy; 14 | 15 | constructor(target: Enemy) { 16 | super('StraightLinePath', target); 17 | if(this.target.position.x < C.BOUNDS.minX) { 18 | this.target.angle = 0; 19 | } 20 | if(this.target.position.x > C.BOUNDS.maxX) { 21 | this.target.angle = Math.PI; 22 | } 23 | if(this.target.position.y < C.BOUNDS.minY) { 24 | this.target.angle = Math.PI / 2; 25 | } 26 | if(this.target.position.y > C.BOUNDS.maxY) { 27 | this.target.angle = (3 * Math.PI) / 2; 28 | } 29 | } 30 | 31 | onUpdate(deltaTime): void { 32 | this.target.position.x += Math.cos(this.target.angle) * this.target.speed * deltaTime/1000; 33 | this.target.position.y += Math.sin(this.target.angle) * this.target.speed * deltaTime/1000; 34 | 35 | if(!this.enteredScreen && CollisionHelper.insideBounds(this.target)){ 36 | this.enteredScreen = true; 37 | } 38 | if(this.enteredScreen && CollisionHelper.outsideBounds(this.target) || CollisionHelper.tooFar(this.target)) { 39 | this.target.handleEvent('destroyed'); 40 | } 41 | } 42 | 43 | remove(): void { 44 | this.target.$state.removeEnemy(this.target); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/models/enemies/Blaster.ts: -------------------------------------------------------------------------------- 1 | import { Enemy } from '../Enemy'; 2 | import { StraightLinePath } from '../../behaviours/Enemy/movement/StraightLinePath'; 3 | import { FiresBulletBehaviour } from '../../behaviours/Enemy/FiresBulletBehaviour'; 4 | import { EnemyBullet } from '../../models/primary/EnemyBullet'; 5 | import { Position } from '../Position'; 6 | import { C, CT } from '../../Constants'; 7 | 8 | 9 | export class Blaster extends Enemy { 10 | 11 | constructor(options: any) { 12 | super(options); 13 | this.healthBase = 5; 14 | this.healthGrowth = 0.2; 15 | 16 | this.speedBase = 65; 17 | this.speedGrowth = 5; 18 | 19 | this.collisionDamageBase = 1; 20 | this.collisionDamageGrowth = 0.1; 21 | 22 | this.modelType = "blaster"; 23 | 24 | this.radius = 40; 25 | } 26 | 27 | onInitGame(state: any): void { 28 | super.onInitGame(state); 29 | this.registerBehaviour("path", new StraightLinePath(this)); 30 | 31 | this.bulletOffsets = [ 32 | new Position(32, -18), 33 | new Position(20, -18), 34 | new Position(-20, -18), 35 | new Position(-32, -18) 36 | ]; 37 | 38 | const bulletOptions = { 39 | system: EnemyBullet, 40 | damage: 1, 41 | speed: 300, 42 | range: 500, 43 | collisionType: CT.CIRCLE, 44 | radius: 25, 45 | bulletMesh: "EnemyBeam", 46 | bulletType: C.ENEMY_BULLET, 47 | cooldown: 3000, 48 | behaviour: 'fires' 49 | } 50 | this.registerBehaviour("primary", new FiresBulletBehaviour(this, bulletOptions)); 51 | 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/models/primary/Missile.ts: -------------------------------------------------------------------------------- 1 | import { Bullet } from '../Bullet'; 2 | import { C, CT } from '../../Constants'; 3 | import { Entity } from '../Entity'; 4 | import { MissilePath } from '../../behaviours/bullet/MissilePath'; 5 | import { Primary } from './Primary'; 6 | import { Position } from '../Position'; 7 | 8 | export class Missile extends Primary { 9 | 10 | constructor(entity: Entity, options: any) { 11 | super(entity, options); 12 | } 13 | 14 | getBullets(): Bullet[] { 15 | const bullets: Bullet[] = []; 16 | 17 | let offsetStart = 0; 18 | if(this.bulletOffset != 0) { 19 | offsetStart = -(this.bulletCount * this.bulletOffset) / 2; 20 | } 21 | 22 | for(let i = 0; i < this.bulletCount; i++) { 23 | const options = { 24 | damage: this.damage, 25 | speed: this.speed, 26 | range: this.range, 27 | collisionType: CT.CIRCLE, 28 | radius: this.radius, 29 | bulletMesh: this.bulletMesh, 30 | position: this.entity.getBulletSpawnLocation(), 31 | bulletType: C.SHIP_BULLET 32 | } 33 | 34 | let angle = Math.PI/2; 35 | if( (i == 0 && this.bulletCount == 2) || (i == 1 && this.bulletCount == 3)) { 36 | angle -= this.bulletAngle; 37 | } else if ( ( i == 1 && this.bulletCount == 2 ) || (i == 2 && this.bulletCount == 3)) { 38 | angle += this.bulletAngle; 39 | } 40 | 41 | const bullet = new Bullet(options); 42 | bullet.registerBehaviour("path", new MissilePath(bullet, {angle: Math.PI/2})); 43 | bullets.push( bullet ); 44 | } 45 | 46 | return bullets; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/models/Position.ts: -------------------------------------------------------------------------------- 1 | import { Schema, type} from '@colyseus/schema'; 2 | 3 | export class Position extends Schema { 4 | @type("number") 5 | x: number; 6 | 7 | @type("number") 8 | y: number; 9 | 10 | constructor(x: number, y: number) { 11 | super(); 12 | this.x = x; 13 | this.y = y; 14 | } 15 | 16 | clone(): Position { 17 | return new Position(this.x, this.y); 18 | } 19 | 20 | distanceTo(another: Position): number { 21 | const dx: number = this.x - another.x; 22 | const dy: number = this.y - another.y; 23 | return Math.sqrt(dx*dx + dy*dy); 24 | } 25 | 26 | magnitude(): number { 27 | return Math.sqrt(this.x*this.x + this.y*this.y); 28 | } 29 | 30 | add(another: Position): void { 31 | this.x = this.x + another.x; 32 | this.y = this.y + another.y; 33 | } 34 | 35 | addN(n: number): void { 36 | this.x = this.x + n; 37 | this.y = this.y + n; 38 | } 39 | 40 | sub(another: Position): void { 41 | this.x = this.x - another.x; 42 | this.y = this.y - another.y; 43 | } 44 | 45 | div(another: Position): void { 46 | this.x = this.x / another.x; 47 | this.y = this.y / another.y; 48 | } 49 | 50 | divN(n: number): void { 51 | this.x = this.x / n; 52 | this.y = this.y / n; 53 | } 54 | 55 | mult(another: Position): void { 56 | this.x = this.x * another.x; 57 | this.y = this.y * another.y; 58 | } 59 | 60 | capSpeed(speed: number): void { 61 | if(this.magnitude() > speed) { 62 | const reduction: number = speed / this.magnitude(); 63 | this.x *= reduction; 64 | this.y *= reduction; 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/models/primary/Torpedo.ts: -------------------------------------------------------------------------------- 1 | /* A straight shot that has an explosion on impact */ 2 | 3 | import { Bullet } from '../Bullet'; 4 | import { C, CT } from '../../Constants'; 5 | import { Entity } from '../Entity'; 6 | import { StraightLineUpPath } from '../../behaviours/bullet/StraightLineUpPath'; 7 | import { ExplodeBehaviour } from '../../behaviours/bullet/ExplodeBehaviour'; 8 | import { Primary } from './Primary'; 9 | import { Position } from '../Position'; 10 | 11 | export class Torpedo extends Primary { 12 | 13 | constructor(entity: Entity, options: any) { 14 | super(entity, options); 15 | } 16 | 17 | getBullets(): Bullet[] { 18 | const bullets: Bullet[] = []; 19 | 20 | let offsetStart = 0; 21 | if(this.bulletOffset != 0) { 22 | offsetStart = -(this.bulletCount * this.bulletOffset) / 2; 23 | } 24 | 25 | for(let i = 0; i < this.bulletCount; i++){ 26 | 27 | const options = { 28 | damage: this.damage, 29 | speed: this.speed, 30 | range: this.range, 31 | collisionType: CT.CIRCLE, 32 | radius: this.radius, 33 | bulletMesh: this.bulletMesh, 34 | postion: this.entity.getBulletSpawnLocation(), 35 | bulletType: C.SHIP_BULLET, 36 | explodes: true, 37 | blastRadius: this.blastRadius 38 | } 39 | 40 | const bullet = new Bullet(options); 41 | bullet.position.x = bullet.position.x + offsetStart + (i * this.bulletOffset); 42 | bullet.registerBehaviour("path", new StraightLineUpPath(bullet)); 43 | bullet.registerBehaviour("explode", new ExplodeBehaviour(bullet)); 44 | bullets.push(bullet); 45 | } 46 | 47 | return bullets; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/models/primary/EnemyBullet.ts: -------------------------------------------------------------------------------- 1 | import { Bullet } from '../Bullet'; 2 | import { C, CT } from '../../Constants'; 3 | import { StraightLineDownPath } from '../../behaviours/bullet/StraightLineDownPath'; 4 | import { StraightAnglePath } from '../../behaviours/bullet/StraightAnglePath'; 5 | import { Primary } from './Primary'; 6 | import { Position } from '../Position'; 7 | 8 | export class EnemyBullet extends Primary{ 9 | 10 | getBullets(): Bullet[] { 11 | const bullets: Bullet[] = []; 12 | if(this.entity.bulletOffsets == null || this.entity.bulletOffsets.length <= 0) { 13 | this.entity.bulletOffsets = [ 14 | new Position(0, 0) 15 | ]; 16 | } 17 | 18 | for(let i = 0; i < this.entity.bulletOffsets.length; i++) { 19 | let position = this.entity.position.clone(); 20 | position.x += this.entity.bulletOffsets[i].x; 21 | position.y += this.entity.bulletOffsets[i].y; 22 | 23 | const options = { 24 | damage: this.damage, 25 | speed: this.speed, 26 | range: this.range, 27 | collisionType: CT.CIRCLE, 28 | radius: this.radius, 29 | bulletMesh: this.bulletMesh, 30 | position: position, 31 | bulletType: C.ENEMY_BULLET 32 | } 33 | 34 | const bullet = new Bullet(options); 35 | switch(this.behaviour) { 36 | case 'drops': 37 | bullet.registerBehaviour("path", new StraightLineDownPath(bullet)); 38 | break; 39 | case 'fires': 40 | bullet.registerBehaviour("path", new StraightAnglePath(bullet, {angle: this.entity.angle})); 41 | break; 42 | } 43 | bullets.push(bullet); 44 | } 45 | 46 | return bullets; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/models/primary/Cannon.ts: -------------------------------------------------------------------------------- 1 | /* Basic bullet */ 2 | 3 | /* This is also the mechanism for firing the bullet */ 4 | 5 | import { Bullet } from '../Bullet'; 6 | import { Entity } from '../Entity'; 7 | import { C, CT } from '../../Constants'; 8 | import { StraightLineUpPath } from '../../behaviours/bullet/StraightLineUpPath'; 9 | import { StraightAnglePath } from '../../behaviours/bullet/StraightAnglePath'; 10 | import { Primary } from './Primary'; 11 | import { Position } from '../Position'; 12 | 13 | export class Cannon extends Primary { 14 | 15 | constructor(entity: Entity, options: any) { 16 | super(entity, options); 17 | } 18 | 19 | getBullets(): Bullet[] { 20 | const spawnLocation: Position = this.entity.getBulletSpawnLocation(); 21 | 22 | const bullets: Bullet[] = []; 23 | 24 | for(let i = 0; i < this.bulletCount; i++) { 25 | const options = { 26 | damage: this.damage, 27 | speed: this.speed, 28 | range: this.range, 29 | collisionType: CT.CIRCLE, 30 | radius: this.radius, 31 | bulletMesh: this.bulletMesh, 32 | position: spawnLocation, 33 | bulletType: C.SHIP_BULLET 34 | } 35 | 36 | let angle = Math.PI/2; 37 | if( (i == 0 && this.bulletCount == 2) || (i == 1 && this.bulletCount == 3)) { 38 | angle -= this.bulletAngle; 39 | } else if ( ( i == 1 && this.bulletCount == 2 ) || (i == 2 && this.bulletCount == 3)) { 40 | angle += this.bulletAngle; 41 | } 42 | 43 | const bullet = new Bullet(options); 44 | bullet.registerBehaviour("path", new StraightAnglePath(bullet, {angle: Math.PI/2 - this.bulletAngle})); 45 | bullets.push( bullet ); 46 | } 47 | 48 | return bullets; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/behaviours/drop/MoveToLocationPath.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { C } from '../../Constants'; 3 | import { Drop, Entity, Position } from '../../models'; 4 | import { CollisionHelper } from '../../helpers/CollisionHelper'; 5 | 6 | export class MoveToLocationPath extends Behaviour { 7 | 8 | moveTo: Position; 9 | moveComplete = false; 10 | 11 | enteredScreen = false; 12 | 13 | target: Drop; 14 | 15 | constructor(target: Drop, moveTo?: Position) { 16 | super('MoveToLocation', target); 17 | this.moveTo = moveTo; 18 | if(this.moveTo == null) { 19 | this.moveTo = new Position( C.RANDOM_X_ON_SCREEN, C.RANDOM_Y_ON_SCREEN) 20 | } 21 | 22 | const dx = this.moveTo.x - this.target.position.x; 23 | const dy = this.moveTo.y - this.target.position.y; 24 | 25 | this.target.angle = Math.atan2(dy, dx); 26 | } 27 | 28 | onUpdate(deltaTime): void { 29 | if(this.moveComplete) return; 30 | 31 | if(this.target.position.distanceTo(this.moveTo) <= this.target.speed * deltaTime/1000) { 32 | this.target.position.x = this.moveTo.x; 33 | this.target.position.y = this.moveTo.y; 34 | this.moveComplete = true; 35 | } else { 36 | this.target.position.x += Math.cos(this.target.angle) * this.target.speed * deltaTime/1000; 37 | this.target.position.y += Math.sin(this.target.angle) * this.target.speed * deltaTime/1000; 38 | 39 | if(!this.enteredScreen && CollisionHelper.insideBounds(this.target)){ 40 | this.enteredScreen = true; 41 | } 42 | if(this.enteredScreen && CollisionHelper.outsideBounds(this.target) || CollisionHelper.tooFar(this.target)) { 43 | this.target.handleEvent('destroyed'); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/helpers/UsernameGenerator.ts: -------------------------------------------------------------------------------- 1 | import { sample, capitalize } from 'lodash'; 2 | 3 | export class UsernameGenerator { 4 | 5 | public static names: string[] = [ 6 | 'hunter', 7 | 'killer', 8 | 'bounty', 9 | 'space', 10 | 'pirate', 11 | 'maurader', 12 | 'drifter', 13 | 'star', 14 | 'destroyer', 15 | 'laser', 16 | 'astro', 17 | 'cruiser', 18 | 'pod', 19 | 'racer', 20 | 'planet', 21 | 'eater', 22 | 'ship', 23 | 'avenger', 24 | 'crusher', 25 | 'galaxy', 26 | 'atom', 27 | 'neutron', 28 | 'solar', 29 | 'lunar', 30 | 'discovery', 31 | 'serpent', 32 | 'blade', 33 | 'sword', 34 | 'trident', 35 | 'watcher', 36 | 'assassin', 37 | 'spy', 38 | 'detector', 39 | 'exec', 40 | 'campaign', 41 | 'steering', 42 | 'faith', 43 | 'divider', 44 | 'team', 45 | 'flame', 46 | 'bomb', 47 | 'explosion', 48 | 'electron', 49 | 'radius', 50 | 'titan', 51 | 'rupture', 52 | 'debris', 53 | 'mission', 54 | 'hot', 55 | 'cold', 56 | 'ice', 57 | 'fire', 58 | 'earth', 59 | 'sun', 60 | 'moon', 61 | 'saturn', 62 | 'executor', 63 | 'execusioner' 64 | ]; 65 | 66 | public static getUsername(): string { 67 | const first = capitalize(sample(UsernameGenerator.names)); 68 | 69 | return first + (Math.round(Math.random() * 9999999)); 70 | } 71 | 72 | public static getShipname(): string { 73 | const first = capitalize(sample(UsernameGenerator.names)); 74 | let second = capitalize(sample(UsernameGenerator.names)); 75 | while(first == second) { 76 | second = capitalize(sample(UsernameGenerator.names)); 77 | } 78 | return first + second; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /test/TestExplosionDistance.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { GameState } from "../src/models/GameState"; 3 | import { Enemy } from "../src/models/Enemy"; 4 | import { CollisionHelper } from "../src/helpers/CollisionHelper"; 5 | 6 | describe("Ranges", () => { 7 | it("Should capture enemies in range", () => { 8 | let state:GameState = new GameState(); 9 | let enemy1:Enemy = new Enemy({}); 10 | enemy1.position.x = 0; 11 | enemy1.position.y = 25; 12 | let enemy2:Enemy = new Enemy({}); 13 | enemy2.position.x = 0; 14 | enemy2.position.y = -25; 15 | 16 | state.addEnemy(enemy1); 17 | state.addEnemy(enemy2); 18 | let entities = state.getEnemiesInRange(0, 0, 50, true); 19 | assert.equal(entities.length, 2); 20 | }); 21 | 22 | it("Should exclude enemies out of range", () => { 23 | let state:GameState = new GameState(); 24 | let enemy1:Enemy = new Enemy({}); 25 | enemy1.position.x = 0; 26 | enemy1.position.y = 25; 27 | let enemy2:Enemy = new Enemy({}); 28 | enemy2.position.x = 0; 29 | enemy2.position.y = -25; 30 | 31 | state.addEnemy(enemy1); 32 | state.addEnemy(enemy2); 33 | let entities = state.getEnemiesInRange(0, 0, 15, true); 34 | assert.equal(entities.length, 0); 35 | }); 36 | 37 | it("Should capture invisible enemies in range", () => { 38 | let state:GameState = new GameState(); 39 | let enemy1:Enemy = new Enemy({}); 40 | enemy1.position.x = 0; 41 | enemy1.position.y = 25; 42 | let enemy2:Enemy = new Enemy({}); 43 | enemy2.position.x = 0; 44 | enemy2.position.y = -25; 45 | enemy2.invisible = true; 46 | 47 | state.addEnemy(enemy1); 48 | state.addEnemy(enemy2); 49 | let entities = state.getEnemiesInRange(0, 0, 150, true); 50 | assert.equal(entities.length, 2); 51 | }); 52 | }) 53 | -------------------------------------------------------------------------------- /src/behaviours/enemy/movement/MoveToLocationPath.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../../behaviour'; 2 | import { C } from '../../../Constants'; 3 | import { Enemy, Entity, Position } from '../../../models'; 4 | import { CollisionHelper } from '../../../helpers/CollisionHelper'; 5 | 6 | export class MoveToLocationPath extends Behaviour { 7 | 8 | moveTo: Position; 9 | moveComplete = false; 10 | 11 | enteredScreen = false; 12 | 13 | target: Enemy; 14 | 15 | constructor(target: Enemy, moveTo?: Position) { 16 | super('MoveToLocation', target); 17 | this.moveTo = moveTo; 18 | if(this.moveTo == null) { 19 | this.moveTo = new Position( C.RANDOM_X_ON_SCREEN, C.RANDOM_Y_ON_SCREEN) 20 | } 21 | 22 | const dx = this.moveTo.x - this.target.position.x; 23 | const dy = this.moveTo.y - this.target.position.y; 24 | 25 | this.target.angle = Math.atan2(dy, dx); 26 | this.target.disableBehaviour("primary"); 27 | } 28 | 29 | onUpdate(deltaTime): void { 30 | if(this.moveComplete) return; 31 | 32 | if(this.target.position.distanceTo(this.moveTo) <= this.target.speed * deltaTime/1000) { 33 | this.target.position.x = this.moveTo.x; 34 | this.target.position.y = this.moveTo.y; 35 | this.target.enableBehaviour("primary"); 36 | this.moveComplete = true; 37 | } else { 38 | this.target.position.x += Math.cos(this.target.angle) * this.target.speed * deltaTime/1000; 39 | this.target.position.y += Math.sin(this.target.angle) * this.target.speed * deltaTime/1000; 40 | 41 | if(!this.enteredScreen && CollisionHelper.insideBounds(this.target)){ 42 | this.enteredScreen = true; 43 | } 44 | if(this.enteredScreen && CollisionHelper.outsideBounds(this.target) || CollisionHelper.tooFar(this.target)) { 45 | this.target.handleEvent('destroyed'); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/behaviours/enemy/movement/ClosestPlayerAtStartPath.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../../behaviour'; 2 | import { C } from '../../../Constants'; 3 | import { Ship, Enemy } from '../../../models'; 4 | import { CollisionHelper } from '../../../helpers/CollisionHelper'; 5 | 6 | export class ClosestPlayerAtStartPath extends Behaviour { 7 | 8 | targetPlayer: Ship; 9 | enteredScreen = false; 10 | 11 | target: Enemy; 12 | 13 | constructor(target: Enemy) { 14 | super('ClosestPlayerAtStartPath', target); 15 | this.targetPlayer = this.target.$state.getClosestShip(this.target.position.x, this.target.position.y); 16 | let dx: number, dy: number; 17 | if(this.targetPlayer == null){ 18 | dx = ( C.BOUNDS.maxX / 2 ) - this.target.position.x; 19 | dy = ( C.BOUNDS.maxY / 2 ) - this.target.position.y; 20 | } else { 21 | dx = this.targetPlayer.position.x - this.target.position.x; 22 | dy = this.targetPlayer.position.y - this.target.position.y; 23 | } 24 | this.target.angle = Math.atan2(dy, dx); 25 | } 26 | 27 | onUpdate(deltaTime): void { 28 | if(this.targetPlayer != null && this.targetPlayer.invisible == false) { 29 | const dx = this.targetPlayer.position.x - this.target.position.x; 30 | const dy = this.targetPlayer.position.y - this.target.position.y; 31 | this.target.angle = Math.atan2(dy, dx); 32 | } 33 | 34 | this.target.position.x += Math.cos(this.target.angle) * this.target.speed * deltaTime/1000; 35 | this.target.position.y += Math.sin(this.target.angle) * this.target.speed * deltaTime/1000; 36 | 37 | if(!this.enteredScreen && CollisionHelper.insideBounds(this.target)){ 38 | this.enteredScreen = true; 39 | } 40 | if(this.enteredScreen && CollisionHelper.outsideBounds(this.target) || CollisionHelper.tooFar(this.target)) { 41 | this.target.handleEvent('destroyed'); 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/models/enemies/Tank.ts: -------------------------------------------------------------------------------- 1 | import { Enemy } from '../Enemy'; 2 | import { MoveToLocationThenRotatePath } from '../../behaviours/Enemy/movement/MoveToLocationThenRotatePath'; 3 | import { FiresBulletBehaviour } from '../../behaviours/Enemy/FiresBulletBehaviour'; 4 | import { EnemyBullet } from '../../models/primary/EnemyBullet'; 5 | import { C, CT } from '../../Constants'; 6 | import { Position } from '../../models/Position'; 7 | 8 | export enum TankStates { 9 | MOVE, 10 | ROTATE 11 | } 12 | 13 | 14 | export class Tank extends Enemy { 15 | 16 | moveTo: Position; 17 | 18 | constructor(options: any) { 19 | super(options); 20 | this.healthBase = 5; 21 | this.healthGrowth = 0.2; 22 | 23 | this.speedBase = 75; 24 | this.speedGrowth = 5; 25 | 26 | this.collisionDamageBase = 1; 27 | this.collisionDamageGrowth = 0.1; 28 | 29 | this.modelType = "tank"; 30 | 31 | this.radius = 35; 32 | 33 | this.moveTo = options.moveTo || this.randomMoveToLocation(); 34 | } 35 | 36 | private randomMoveToLocation(): Position { 37 | let x = Math.random() * 6; 38 | if(this.position.x < 0) { 39 | x = x * 100; 40 | } else { 41 | x = (x * 100) + 900; 42 | } 43 | return new Position(x, this.position.y); 44 | } 45 | 46 | onInitGame(state: any): void { 47 | super.onInitGame(state); 48 | 49 | const bulletOptions = { 50 | system: EnemyBullet, 51 | damage: 1, 52 | speed: 300, 53 | range: 500, 54 | collisionType: CT.CIRCLE, 55 | radius: 25, 56 | bulletMesh: "Enemy1", 57 | position: this.position.clone(), 58 | bulletType: C.ENEMY_BULLET, 59 | cooldown: 5000, 60 | behaviour: 'fires' 61 | } 62 | this.registerBehaviour("primary", new FiresBulletBehaviour(this, bulletOptions)); 63 | this.registerBehaviour("path", new MoveToLocationThenRotatePath(this, this.moveTo)); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/behaviours/bullet/ClosestEnemyPath.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { Enemy, Bullet } from '../../models'; 3 | import { CollisionHelper } from '../../helpers/CollisionHelper'; 4 | 5 | export class ClosestEnemyPath extends Behaviour { 6 | 7 | targetEnemy: Enemy; 8 | enteredScreen = false; 9 | distanceTraveled = 0; 10 | 11 | target: Bullet; 12 | 13 | constructor(target: Bullet): void { 14 | super('ClosestPlayerPath', target); 15 | this.targetEnemy = this.target.$state.getClosestEnemy(this.target.x, this.target.y); 16 | if(this.targetEnemy == null){ 17 | console.log("Error: Ship is null in TargetPlayerStartPath"); 18 | this.target.handleEvent('destroyed'); 19 | return; 20 | } 21 | const dx = this.target.position.x - this.targetEnemy.position.x; 22 | const dy = this.target.position.y - this.targetEnemy.position.y; 23 | this.target.angle = Math.atan2(dy, dx); 24 | } 25 | 26 | onUpdate(deltaTime): void { 27 | if(this.targetEnemy != null) { 28 | const dx = this.target.position.x - this.targetEnemy.position.x; 29 | const dy = this.target.position.y - this.targetEnemy.position.y; 30 | this.target.angle = Math.atan2(dy, dx); 31 | } 32 | 33 | this.target.position.x += -Math.cos(this.target.angle) * this.target.speed * deltaTime/1000; 34 | this.target.position.y += -Math.sin(this.target.angle) * this.target.speed * deltaTime/1000; 35 | 36 | this.distanceTraveled += this.target.speed * deltaTime/1000; 37 | 38 | if(this.distanceTraveled >= this.target.range){ 39 | this.target.handleEvent('destroyed'); 40 | } 41 | 42 | if(!this.enteredScreen && CollisionHelper.insideBounds(this.target)){ 43 | this.enteredScreen = true; 44 | } 45 | if(this.enteredScreen && CollisionHelper.outsideBounds(this.target)) { 46 | this.target.handleEvent('destroyed'); 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /ship_stats_raw.txt: -------------------------------------------------------------------------------- 1 | explorer1 explorer2 explorer3 explorer4 explorer5 scout1 scout2 scout3 scout4 scout5 gunship1 gunship2 gunship3 gunship4 gunship5 defender1 defender2 defender3 defender4 defender5 fighter1 fighter2 fighter3 fighter4 fighter5 2 | damage_base 4 5 6 8 10 4 5 6 8 10 6 8 12 16 20 4 5 6 8 10 5 7 9 13 16 3 | damage_growth 1 1 1 2 2 1 1 1 2 2 2 2 3 4 5 1 1 2 2 3 2 2 3 3 4 4 | range_base 300 350 400 450 500 320 370 420 470 520 350 400 450 500 550 300 350 400 450 500 325 375 425 475 525 5 | range_growth 10 10 10 10 10 11 11 11 11 11 12 13 14 15 16 10 10 10 10 10 11 12 13 15 15 6 | fire_rate_base 2000 1950 1900 1850 1800 2000 1950 1900 1850 1800 1600 1550 1500 1450 1400 2000 1950 1900 1850 1800 1800 1750 1700 1650 1600 7 | fire_rate_growth 25 25 25 25 25 25 25 25 25 25 30 30 35 40 45 50 25 25 25 25 30 30 35 35 40 8 | speed_base 250 300 350 400 450 300 350 400 450 500 200 225 250 275 300 200 225 250 275 300 220 270 290 310 330 9 | speed_growth 12 14 18 22 25 14 16 18 22 25 12 13 14 15 18 12 13 14 15 18 12 13 14 15 18 10 | accelleration_base 250 300 350 400 450 300 350 400 450 500 200 225 250 275 300 200 225 250 275 300 220 270 290 310 330 11 | accelleration_growth 12 14 18 22 25 14 14 18 22 25 50 12 14 14 18 12 12 14 14 18 12 13 14 15 18 12 | shields_base 3 4 5 6 8 2 3 4 5 6 2 3 4 5 6 5 7 9 12 15 3 4 5 6 8 13 | shields_growth 1 1 1 1 2 1 1 1 1 2 1 1 1 2 2 2 2 2 2 3 1 1 1 2 2 14 | shield_recharge_base 28000 27500 27000 26500 26000 30000 29500 29000 28500 28000 30000 29500 29000 28500 28000 26000 25000 24000 23000 22000 29000 28000 27000 26000 25000 15 | shield_recharge_growth 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 600 650 700 750 800 500 525 550 575 600 16 | special_base 0.05 0.05 0.05 0.05 0.05 0.01 0.01 0.015 0.02 0.025 0 0 0 0 0 0 0.01 0.01 0.015 0.02 0.01 0.01 0.015 0.015 0.02 17 | special_growth 0.005 0.005 0.01 0.01 0.015 0.01 0.01 0.015 0.015 0.02 0.005 0.005 0.005 0.005 0.005 0.005 0.005 0.01 0.01 0.015 0.005 0.005 0.005 0.005 0.01 18 | -------------------------------------------------------------------------------- /src/Material.ts: -------------------------------------------------------------------------------- 1 | export class MATERIAL { 2 | 3 | public static TYPE = { 4 | "cindertron_recruit1": { 5 | 'description': `Cindertron recruit fight squadron colors.`, 6 | }, 7 | "cindertron_recruit2": { 8 | 'description': `Cindertron recruit omega squadron colors.`, 9 | }, 10 | "cindertron_recruit3": { 11 | 'description': `Cindertron recruit fireborn squadron colors.`, 12 | }, 13 | "bone_brigade1": { 14 | 'description': `The elite bone brigade initiate colors.`, 15 | 'unlockKey': "maxKills", 16 | 'unlockCount': 100 17 | }, 18 | "royal_fleet1": { 19 | 'description': `The royal fleet honor guard initiate colors.`, 20 | 'unlockKey': "maxKills", 21 | 'unlockCount': 200 22 | }, 23 | "camo1": { 24 | 'description': `Cindertron ground army camo pattern.`, 25 | 'unlockKey': "maxKills", 26 | 'unlockCount': 300 27 | }, 28 | "earth_defense1": { 29 | 'description': `Earth defense initate colors.`, 30 | 'unlockKey': "maxKills", 31 | 'unlockCount': 400 32 | }, 33 | "royal_fleet2": { 34 | 'description': `The royal fleet honor guard squad colors.`, 35 | 'unlockKey': "maxKills", 36 | 'unlockCount': 500 37 | }, 38 | "bone_brigade2": { 39 | 'description': `Bone Brigade assault squadron colors.`, 40 | 'unlockKey': "maxKills", 41 | 'unlockCount': 600 42 | }, 43 | "camo2": { 44 | 'description': `Cindertron magma assult camo pattern.`, 45 | 'unlockKey': "maxKills", 46 | 'unlockCount': 700 47 | }, 48 | "royal_fleet3": { 49 | 'description': `Cindertron royal fleet honor guard elite pattern.`, 50 | 'unlockKey': "maxKills", 51 | 'unlockCount': 800 52 | }, 53 | "bone_brigade3": { 54 | 'description': `The elite bone brigade execution squad colors.`, 55 | 'unlockKey': "maxKills", 56 | 'unlockCount': 900 57 | }, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/behaviours/enemy/movement/ClosestPlayerPath.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../../behaviour'; 2 | import { C } from '../../../Constants'; 3 | import { Ship, Enemy } from '../../../models'; 4 | import { CollisionHelper } from '../../../helpers/CollisionHelper'; 5 | 6 | export class ClosestPlayerPath extends Behaviour { 7 | 8 | targetPlayer: Ship; 9 | enteredScreen = false; 10 | 11 | target: Enemy; 12 | 13 | constructor(target: Enemy) { 14 | super('ClosestPlayerPath', target); 15 | this.targetPlayer = this.target.$state.getClosestShip(this.target.position.x, this.target.position.y); 16 | let dx: number, dy: number; 17 | if(this.targetPlayer == null){ 18 | dx = ( C.BOUNDS.maxX / 2 ) - this.target.position.x; 19 | dy = ( C.BOUNDS.maxY / 2 ) - this.target.position.y; 20 | } 21 | else { 22 | dx = this.targetPlayer.position.x - this.target.position.x; 23 | dy = this.targetPlayer.position.y - this.target.position.y; 24 | } 25 | this.target.angle = Math.atan2(dy, dx); 26 | } 27 | 28 | onUpdate(deltaTime): void { 29 | this.targetPlayer = this.target.$state.getClosestShip(this.target.position.x, this.target.position.y); 30 | if(this.targetPlayer != null) { 31 | const dx = this.targetPlayer.position.x - this.target.position.x; 32 | const dy = this.targetPlayer.position.y - this.target.position.y; 33 | this.target.angle = Math.atan2(dy, dx); 34 | } 35 | 36 | this.target.position.x += Math.cos(this.target.angle) * this.target.speed * deltaTime/1000; 37 | this.target.position.y += Math.sin(this.target.angle) * this.target.speed * deltaTime/1000; 38 | 39 | if(!this.enteredScreen && CollisionHelper.insideBounds(this.target)){ 40 | this.enteredScreen = true; 41 | } 42 | if(this.enteredScreen && CollisionHelper.outsideBounds(this.target) || CollisionHelper.tooFar(this.target)) { 43 | this.target.handleEvent('destroyed'); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/behaviours/enemy/movement/StraightAnglePath.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../../behaviour'; 2 | import { C } from '../../../Constants'; 3 | import { Enemy, Position } from '../../../models'; 4 | import { CollisionHelper } from '../../../helpers/CollisionHelper'; 5 | 6 | export class StraightAnglePath extends Behaviour { 7 | 8 | start: Position; 9 | enteredScreen = false; 10 | 11 | target: Enemy; 12 | 13 | constructor(target: Enemy) { 14 | super('StraightAnglePath', target); 15 | this.start = new Position(this.target.position.x, this.target.position.y); 16 | 17 | 18 | let toX = 0; 19 | let toY = 0; 20 | if(this.target.position.x < C.BOUNDS.minX) { 21 | toX = C.BOUNDS.maxX + C.SPAWN_OFFSET; 22 | toY = C.BOUNDS.maxY - this.target.position.y; 23 | } 24 | if(this.target.position.x > C.BOUNDS.maxX) { 25 | toX = -C.SPAWN_OFFSET; 26 | toY = C.BOUNDS.maxY - this.target.position.y; 27 | } 28 | if(this.target.position.y < C.BOUNDS.minY) { 29 | toX = C.BOUNDS.maxX - this.target.position.x; 30 | toY = C.BOUNDS.maxY + C.SPAWN_OFFSET; 31 | } 32 | if(this.target.position.y > C.BOUNDS.maxY){ 33 | toX = C.BOUNDS.maxX - this.target.position.x; 34 | toY = -C.SPAWN_OFFSET; 35 | } 36 | 37 | const dx = toX - this.target.position.x; 38 | const dy = toY - this.target.position.y; 39 | 40 | this.target.angle = Math.atan2(dy, dx); 41 | } 42 | 43 | onUpdate(deltaTime): void { 44 | this.target.position.x += Math.cos(this.target.angle) * this.target.speed * deltaTime/1000; 45 | this.target.position.y += Math.sin(this.target.angle) * this.target.speed * deltaTime/1000; 46 | 47 | if(!this.enteredScreen && CollisionHelper.insideBounds(this.target)){ 48 | this.enteredScreen = true; 49 | } 50 | if(this.enteredScreen && CollisionHelper.outsideBounds(this.target) || CollisionHelper.tooFar(this.target)) { 51 | this.target.handleEvent('destroyed'); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/models/Enemy.ts: -------------------------------------------------------------------------------- 1 | import { type } from "@colyseus/schema"; 2 | import { Position } from './Position'; 3 | 4 | import { CollidesWithShipBullet } from '../behaviours/Enemy/CollidesWithShipBullet'; 5 | import { DestroyedBehaviour } from '../behaviours/Enemy/DestroyedBehaviour'; 6 | import { TakesDamageBehaviour } from '../behaviours/Enemy/TakesDamageBehaviour'; 7 | 8 | import { Entity } from './Entity'; 9 | 10 | export class Enemy extends Entity { 11 | @type("number") 12 | health = 1; 13 | 14 | healthBase = 1; 15 | healthGrowth = 0.1; 16 | 17 | speed = 1; 18 | speedBase = 1; 19 | speedGrowth = 0.1; 20 | 21 | collisionDamage = 1; 22 | collisionDamageBase = 1; 23 | collisionDamageGrowth = 0.1; 24 | 25 | damage = 1; 26 | damageBase = 1; 27 | damageGrowth = 0.2; 28 | 29 | range = 300; 30 | rangeBase = 300; 31 | rangeGrowth = 25; 32 | 33 | flock: Enemy[]; 34 | destination: Position; 35 | velocity: Position; 36 | 37 | @type("string") 38 | modelType = ""; 39 | 40 | wave: number; 41 | 42 | constructor(options: any) { 43 | super(options); 44 | } 45 | 46 | updateStats(wave: number): void { 47 | this.health = Math.floor(this.healthBase + (this.healthGrowth * wave)); 48 | this.speed = Math.min(Math.floor(this.speedBase + (this.speedGrowth * wave)), 350); // Don't let speed go over 350 that might get a bit silly 49 | this.collisionDamage = Math.floor(this.collisionDamageBase + (this.collisionDamageGrowth * wave)); 50 | this.damage = Math.floor(this.damageBase + (this.damageGrowth * wave)); 51 | this.range = Math.floor(this.rangeBase + ( this.rangeGrowth * wave )); 52 | } 53 | 54 | onInitGame(state: any): void { 55 | super.onInitGame(state); 56 | this.registerBehaviour("collides_ship_bullet", new CollidesWithShipBullet(this)); 57 | this.registerBehaviour("destroyed", new DestroyedBehaviour(this)); 58 | this.registerBehaviour("takes_damage", new TakesDamageBehaviour(this)); 59 | this.updateStats(state.currentWave); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/behaviours/enemy/movement/WobblePath.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../../behaviour'; 2 | import { C } from '../../../Constants'; 3 | import { CollisionHelper } from '../../../helpers/CollisionHelper'; 4 | import { Enemy } from '../../../models/Enemy'; 5 | 6 | export class WobblePath extends Behaviour { 7 | 8 | xDir = 0; 9 | yDir = 0; 10 | 11 | wobblePercent = 1; 12 | wobbleDuration = 3; 13 | private wobbleTimer = 0; 14 | 15 | enteredScreen = false; 16 | 17 | target: Enemy; 18 | 19 | constructor(target: Enemy, args: { wobblePercent: number; wobbleDuration: number}) { 20 | super('WobblePath', target); 21 | if(args) { 22 | this.wobblePercent = args.wobblePercent || 1; 23 | this.wobbleDuration = args.wobbleDuration || 3; 24 | } 25 | if(this.target.position.x < C.BOUNDS.minX) this.xDir = 1; 26 | if(this.target.position.x > C.BOUNDS.maxX) this.xDir = -1; 27 | if(this.target.position.y < C.BOUNDS.minY) this.yDir = 1; 28 | if(this.target.position.y > C.BOUNDS.maxY) this.yDir = -1; 29 | } 30 | 31 | onUpdate(deltaTime): void { 32 | if(this.xDir != 0) { 33 | this.target.position.x += this.target.speed * this.xDir * (deltaTime/1000); 34 | this.target.position.y += this.target.speed * this.wobblePercent * (deltaTime/1000); 35 | } 36 | if(this.yDir != 0) { 37 | this.target.position.y += this.target.speed * this.yDir * (deltaTime/1000); 38 | this.target.position.x += this.target.speed * this.wobblePercent * (deltaTime/1000); 39 | } 40 | 41 | this.wobbleTimer += deltaTime / 1000; 42 | if(this.wobbleTimer > this.wobbleDuration) { 43 | this.wobbleTimer = 0; 44 | this.wobblePercent = -this.wobblePercent; 45 | } 46 | 47 | if(!this.enteredScreen && CollisionHelper.insideBounds(this.target)){ 48 | this.enteredScreen = true; 49 | } 50 | 51 | if(this.enteredScreen && CollisionHelper.outsideBounds(this.target) || CollisionHelper.tooFar(this.target)) { 52 | this.target.handleEvent('destroyed'); 53 | } 54 | } 55 | 56 | remove(): void { 57 | this.target.$state.removeEnemy(this.target); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/helpers/AccountHelper.ts: -------------------------------------------------------------------------------- 1 | import { DB } from '../Database'; 2 | import { Account } from '../models/Account'; 3 | import { CommandResult } from 'mongodb'; 4 | 5 | export class AccountHelper { 6 | 7 | static async getAccountByEmail(email: string): Promise { 8 | const account = await DB.$accounts.findOne({ email: email }); 9 | if(account) return new Account(account); 10 | return null; 11 | } 12 | 13 | static async getAccountByUsername(username: string): Promise { 14 | const account = await DB.$accounts.findOne({ username }); 15 | if(account) return new Account(account); 16 | return null; 17 | } 18 | 19 | static async getAccountBySystemID(systemId: string): Promise { 20 | const account = await DB.$accounts.findOne({ systemId }); 21 | if(account){ 22 | return new Account(account); 23 | } else { 24 | const tmpAct = new Account({systemId}); 25 | return await AccountHelper.createAccount(tmpAct); 26 | } 27 | } 28 | 29 | static async createAccount(account: Account): Promise { 30 | const results: CommandResult = await DB.$accounts.insertOne(account.toSaveObject()); 31 | if(results.error) { 32 | console.error('createAccount (error)', results.error); 33 | return null; 34 | } 35 | account = new Account(results.ops[0]); 36 | if(account) { 37 | account.updateUnlocks(); 38 | const success: boolean = await AccountHelper.saveAccount(account); 39 | if(!success) return null; 40 | return account; 41 | } 42 | return null; 43 | } 44 | 45 | static async saveAccount(account: Account): Promise { 46 | const results: CommandResult = DB.$accounts.updateOne({ username: account.username }, { $set: account.toSaveObject() }); 47 | if(results.error) { 48 | console.error('saveAccount (error)', results.error); 49 | return false; 50 | } 51 | return true; 52 | } 53 | 54 | static async clearInGame(username: string): Promise { 55 | const results: CommandResult = DB.$ships.updateMany({username}, {$set: {inGame: -1}}); 56 | if(results.error) return false; 57 | return true; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "space_shooter_server", 3 | "version": "0.1.0", 4 | "description": "Multiplayer server for a colyseus space shooter.", 5 | "repository": "https://github.com/stats/space_shooter_server", 6 | "main": "src/index.js", 7 | "scripts": { 8 | "start": "npm-run-all --parallel start:mongo start:server", 9 | "start:debug": "npm-run-all --parallel start:mongo start:server:debug", 10 | "start:server": "nodemon --ignore \"test/\" --exec node -r ts-node/register src/index.ts", 11 | "start:server:debug": "nodemon --ignore \"test/\" --exec node --inspect -r ts-node/register src/index.ts", 12 | "start:mongo": "mongod --dbpath ./db/development", 13 | "test:server": "mocha --require ts-node/register test/ServerTest.ts --exit --timeout 15000", 14 | "test:explosion": "mocha --require ts-node/register test/TestExplosionDistance.ts --exit --timeout 15000", 15 | "test:username": "ts-node test/UsernameTest.ts", 16 | "codegen": "npx schema-codegen src/models/Account.ts src/models/states/GameState.ts src/models/states/ShipBuilderState src/models/messages/ErrorMessage.ts src/models/messages/ShipList.ts src/models/messages/Statistics.ts src/models/messages/UnlockMessage.ts --output schema-unity --csharp", 17 | "parseStats": "node scripts/stat_parser.js > ship_stats.json", 18 | "lint": "npx eslint \"src/**\"", 19 | "circular": "npx madge --circular --extensions ts ./src" 20 | }, 21 | "author": "Dan Curran", 22 | "license": "MIT", 23 | "dependencies": { 24 | "@colyseus/monitor": "^0.12.1", 25 | "body-parser": "^1.19.0", 26 | "colyseus": "^0.12.3", 27 | "cors": "^2.8.5", 28 | "express": "^4.17.1", 29 | "intersects": "^2.7.1", 30 | "jsonwebtoken": "^8.5.1", 31 | "lodash": "^4.17.19", 32 | "nodemon": "^2.0.2", 33 | "npm-run-all": "^4.1.5", 34 | "uuid": "^7.0.1" 35 | }, 36 | "devDependencies": { 37 | "@types/mocha": "^7.0.2", 38 | "@types/node": "^13.9.1", 39 | "@typescript-eslint/eslint-plugin": "^2.23.0", 40 | "@typescript-eslint/parser": "^2.23.0", 41 | "assert": "^2.0.0", 42 | "eslint": "^6.8.0", 43 | "httpie": "^1.1.2", 44 | "madge": "^3.8.0", 45 | "mocha": "^7.1.0", 46 | "ts-node": "^8.6.2", 47 | "typescript": "^3.8.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/ServerTest.ts: -------------------------------------------------------------------------------- 1 | import * as httpClient from 'httpie'; 2 | import { DB } from '../src/database'; 3 | import assert from "assert"; 4 | 5 | describe("Server", () => { 6 | 7 | const url = "http://localhost:2567"; 8 | const ws = "ws://localhost:2567"; 9 | const dummy_username = "INTEGRATION_TEST"; 10 | const dummy_email = "test@example.com"; 11 | const dummy_password = "test"; 12 | 13 | var access_token = ""; 14 | 15 | describe("signup, login and renew", () => { 16 | after(async () => { 17 | await DB.init(); 18 | await DB.$accounts.deleteMany({username: dummy_username}); 19 | await DB.$ships.deleteMany({username: dummy_username}); 20 | }) 21 | 22 | it("should respond to POST /quick_login to register a user", async() => { 23 | const response = await httpClient.post(`${url}/quick_login`, { 24 | body: { 25 | system_id: 1 26 | } 27 | }); 28 | 29 | assert.ok(response.data.success); 30 | assert.ok(response.data.token); 31 | }); 32 | 33 | it("should respond to POST /signup to register a user", async() => { 34 | const response = await httpClient.post(`${url}/signup`, { 35 | body: { 36 | username: dummy_username, 37 | email: dummy_email, 38 | password: dummy_password 39 | } 40 | }); 41 | 42 | assert.ok(response.data.success); 43 | assert.ok(response.data.token); 44 | 45 | }); 46 | 47 | it("should respond to POST /login to login the registered user", async() => { 48 | const response = await httpClient.post(`${url}/login`, { 49 | body: { 50 | email: dummy_email, 51 | password: dummy_password 52 | } 53 | }); 54 | 55 | assert.ok(response.data.success); 56 | assert.ok(response.data.token); 57 | 58 | access_token = response.data.token; 59 | }); 60 | 61 | it("should respond to POST /renew to renew the users token", async() => { 62 | const response = await httpClient.post(`${url}/renew`, { 63 | body: { 64 | token: access_token 65 | } 66 | }); 67 | 68 | assert.ok(response.data.success); 69 | assert.ok(response.data.token); 70 | 71 | access_token = response.data.token; 72 | }); 73 | }); 74 | }) 75 | -------------------------------------------------------------------------------- /src/behaviours/enemy/movement/MoveToLocationThenRotatePath.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../../behaviour'; 2 | import { C } from '../../../Constants'; 3 | import { Enemy, Position } from '../../../models'; 4 | import { CollisionHelper } from '../../../helpers/CollisionHelper'; 5 | 6 | export class MoveToLocationThenRotatePath extends Behaviour { 7 | 8 | moveTo: Position; 9 | moveComplete = false; 10 | 11 | rotationDirection = 1; 12 | 13 | enteredScreen = false; 14 | 15 | target: Enemy; 16 | 17 | constructor(target: Enemy, moveTo: Position ) { 18 | super('MoveToLocationThenRotate', target); 19 | this.moveTo = moveTo || C.RANDOM_ON_SCREEN(); 20 | if(this.moveTo == null) { 21 | this.moveTo = new Position( C.RANDOM_X_ON_SCREEN, C.RANDOM_Y_ON_SCREEN) 22 | } 23 | 24 | const dx = this.moveTo.x - this.target.position.x; 25 | const dy = this.moveTo.y - this.target.position.y; 26 | 27 | this.target.angle = Math.atan2(dy, dx); 28 | this.rotationDirection = Math.random() > 0.5 ? 1 : -1; 29 | this.target.disableBehaviour("primary"); 30 | } 31 | 32 | onUpdate(deltaTime): void { 33 | if(this.moveComplete) { 34 | this.handleRotation(deltaTime); 35 | } else { 36 | this.handleMovement(deltaTime); 37 | } 38 | } 39 | 40 | private handleRotation(deltaTime): void { 41 | this.target.angle += this.rotationDirection * 0.5 * deltaTime/1000; 42 | } 43 | 44 | private handleMovement(deltaTime): void { 45 | if(this.target.position.distanceTo(this.moveTo) <= this.target.speed * deltaTime/1000) { 46 | this.target.position.x = this.moveTo.x; 47 | this.target.position.y = this.moveTo.y; 48 | this.moveComplete = true; 49 | this.target.enableBehaviour("primary"); 50 | } else { 51 | this.target.position.x += Math.cos(this.target.angle) * this.target.speed * deltaTime/1000; 52 | this.target.position.y += Math.sin(this.target.angle) * this.target.speed * deltaTime/1000; 53 | 54 | if(!this.enteredScreen && CollisionHelper.insideBounds(this.target)){ 55 | this.enteredScreen = true; 56 | } 57 | if(this.enteredScreen && CollisionHelper.outsideBounds(this.target) || CollisionHelper.tooFar(this.target)) { 58 | this.target.handleEvent('destroyed'); 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /schema-unity/Ship.cs: -------------------------------------------------------------------------------- 1 | // 2 | // THIS FILE HAS BEEN GENERATED AUTOMATICALLY 3 | // DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING 4 | // 5 | // GENERATED USING @colyseus/schema 0.5.33 6 | // 7 | 8 | using Colyseus.Schema; 9 | 10 | public class Ship : Entity { 11 | [Type(6, "string")] 12 | public string name = ""; 13 | 14 | [Type(7, "boolean")] 15 | public bool connected = false; 16 | 17 | [Type(8, "boolean")] 18 | public bool justDamaged = false; 19 | 20 | [Type(9, "string")] 21 | public string shipType = ""; 22 | 23 | [Type(10, "string")] 24 | public string shipMaterial = ""; 25 | 26 | [Type(11, "string")] 27 | public string primaryWeapon = ""; 28 | 29 | [Type(12, "string")] 30 | public string specialWeapon = ""; 31 | 32 | [Type(13, "number")] 33 | public float primaryCooldownMax = 0; 34 | 35 | [Type(14, "number")] 36 | public float primaryCooldown = 0; 37 | 38 | [Type(15, "number")] 39 | public float specialCooldownMax = 0; 40 | 41 | [Type(16, "number")] 42 | public float specialCooldown = 0; 43 | 44 | [Type(17, "number")] 45 | public float kills = 0; 46 | 47 | [Type(18, "number")] 48 | public float killScore = 0; 49 | 50 | [Type(19, "number")] 51 | public float currentKills = 0; 52 | 53 | [Type(20, "int32")] 54 | public int shield = 0; 55 | 56 | [Type(21, "number")] 57 | public float damage = 0; 58 | 59 | [Type(22, "number")] 60 | public float fireRate = 0; 61 | 62 | [Type(23, "number")] 63 | public float range = 0; 64 | 65 | [Type(24, "int32")] 66 | public int maxShield = 0; 67 | 68 | [Type(25, "number")] 69 | public float shieldRechargeCooldown = 0; 70 | 71 | [Type(26, "number")] 72 | public float shieldRechargeTime = 0; 73 | 74 | [Type(27, "number")] 75 | public float speed = 0; 76 | 77 | [Type(28, "number")] 78 | public float rank = 0; 79 | 80 | [Type(29, "number")] 81 | public float highestWave = 0; 82 | 83 | [Type(30, "number")] 84 | public float level = 0; 85 | 86 | [Type(31, "number")] 87 | public float previousLevel = 0; 88 | 89 | [Type(32, "number")] 90 | public float nextLevel = 0; 91 | 92 | [Type(33, "int32")] 93 | public int upgradePoints = 0; 94 | 95 | [Type(34, "int32")] 96 | public int upgradeDamage = 0; 97 | 98 | [Type(35, "int32")] 99 | public int upgradeRange = 0; 100 | 101 | [Type(36, "int32")] 102 | public int upgradeFireRate = 0; 103 | 104 | [Type(37, "int32")] 105 | public int upgradeSpeed = 0; 106 | 107 | [Type(38, "int32")] 108 | public int upgradeShield = 0; 109 | 110 | [Type(39, "int32")] 111 | public int upgradeShieldRecharge = 0; 112 | 113 | [Type(40, "array", typeof(ArraySchema))] 114 | public ArraySchema tempUpgrades = new ArraySchema(); 115 | 116 | [Type(41, "number")] 117 | public float tempUpgradeTimer = 0; 118 | } 119 | 120 | -------------------------------------------------------------------------------- /src/behaviours/bullet/MissilePath.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../behaviour'; 2 | import { C } from '../../Constants'; 3 | import { Bullet, Entity } from '../../models'; 4 | 5 | export class MissilePath extends Behaviour { 6 | 7 | private entity: Entity; 8 | 9 | private xLoc = -999; 10 | private yLoc = -999; 11 | 12 | private acc_x = 0; 13 | private acc_y = 0; 14 | 15 | private speed_x = 0; 16 | private speed_y = 0; 17 | 18 | private traveled = 0; 19 | 20 | private gotGameState = false; 21 | 22 | private t1 = 0.01; 23 | private t2 = 0.001; 24 | 25 | target: Bullet; 26 | 27 | constructor(target: Bullet, args: { angle: number }) { 28 | super('MissilePath', target); 29 | this.target.angle = args.angle || Math.PI/2; 30 | } 31 | 32 | handleGameState(): void { 33 | if(this.target.bulletType == C.SHIP_BULLET) { 34 | this.entity = this.target.$state.getClosestEnemy(this.target.position.x, this.target.position.y); 35 | } else { 36 | this.entity = this.target.$state.getClosestShip(this.target.position.x, this.target.position.y); 37 | } 38 | if(this.entity == null) { 39 | this.xLoc = this.target.position.x; 40 | this.yLoc = this.target.position.y + this.target.range; 41 | } 42 | this.gotGameState = true; 43 | } 44 | 45 | onUpdate(deltaTime): void { 46 | if(this.gotGameState == false && this.target.$state == null) { 47 | return; 48 | } else if (this.gotGameState == false && this.target.$state != null) { 49 | this.handleGameState(); 50 | } 51 | if(this.getEntityX() == null) { 52 | this.target.handleEvent('destroyed'); 53 | return; 54 | } 55 | if(this.entity && this.entity.invisible) { 56 | this.xLoc = this.entity.position.x; 57 | this.yLoc = this.entity.position.y; 58 | this.entity = null; 59 | } 60 | 61 | const dx = this.getEntityX() - this.target.position.x; 62 | const dy = this.getEntityY() - this.target.position.y; 63 | const ct = Math.atan2(dy, dx); 64 | if(ct - this.target.angle > this.t1) { 65 | this.target.angle += this.t1; 66 | } else if(ct - this.target.angle < this.t1) { 67 | this.target.angle -= this.t1; 68 | } else { 69 | this.target.angle = ct; 70 | } 71 | 72 | this.t1 += this.t2; 73 | 74 | this.target.position.x += Math.cos(this.target.angle) * this.target.speed * deltaTime / 1000; 75 | this.target.position.y += Math.sin(this.target.angle) * this.target.speed * deltaTime / 1000; 76 | 77 | this.traveled += this.target.speed * deltaTime / 1000; 78 | 79 | if(this.traveled > this.target.range) this.target.handleEvent('destroyed'); 80 | } 81 | 82 | engine(): number { 83 | return 5 * Math.random() * 0.1 + 0.05; 84 | } 85 | 86 | drag(): number { 87 | return 5 * Math.random() * 0.05 + 0.94; 88 | } 89 | 90 | sgn(n: number): number { 91 | return n < 0 ? -1 : 1; 92 | } 93 | 94 | getEntityX(): number { 95 | if( this.entity == null && this.xLoc == -999) return null; 96 | if( this.entity != null ) this.xLoc = this.entity.position.x; 97 | return this.xLoc; 98 | } 99 | 100 | getEntityY(): number { 101 | if( this.entity == null && this.yLoc == -999) return null; 102 | if( this.entity != null ) this.yLoc = this.entity.position.y; 103 | return this.yLoc; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/models/Entity.ts: -------------------------------------------------------------------------------- 1 | import { Schema, type } from "@colyseus/schema"; 2 | import { merge } from 'lodash'; 3 | import { Position } from './Position'; 4 | import { CT } from '../Constants'; 5 | 6 | import { v4 as uuid } from 'uuid'; 7 | 8 | export class Entity extends Schema { 9 | 10 | @type('string') 11 | uuid: string; 12 | 13 | @type(Position) 14 | position: Position = new Position(0, 0); 15 | 16 | lastPosition: Position = new Position(0, 0); 17 | 18 | @type('number') 19 | angle = 0; 20 | 21 | state: number; 22 | 23 | bulletOffsetX = 0; 24 | bulletOffsetY = 0; 25 | bulletOffsets: Position[]; 26 | 27 | collisionType: number = CT.CIRCLE; 28 | radius = 25; 29 | width: number; 30 | height: number; 31 | radiusX: number; 32 | radiusY: number; 33 | 34 | @type('boolean') 35 | bulletInvulnerable = false; 36 | 37 | @type('boolean') 38 | collisionInvulnerable = false; 39 | 40 | @type('boolean') 41 | invisible = false; 42 | 43 | public $state: any; 44 | 45 | protected $behaviours: any = {}; 46 | 47 | constructor(options: any) { 48 | super(); 49 | merge(this, options); 50 | if(!this.position) this.position = new Position(0, 0); 51 | if(!this.uuid) this.uuid = uuid(); 52 | } 53 | 54 | public registerBehaviour(key: string, behaviour: any): void { 55 | this.$behaviours[key] = behaviour; 56 | behaviour.onRegistered(); 57 | } 58 | 59 | public removeBehaviour(key: string): void { 60 | this.$behaviours[key].onRemoved(); 61 | delete(this.$behaviours[key]); 62 | } 63 | 64 | public removeAllBehaviours(): void { 65 | for(const key in this.$behaviours) { 66 | this.removeBehaviour(key); 67 | } 68 | } 69 | 70 | public enableBehaviour(key: string): void { 71 | const behaviour: any = this.$behaviours[key]; 72 | if(behaviour) behaviour.enable(); 73 | } 74 | 75 | public disableBehaviour(key: string): void { 76 | const behaviour: any = this.$behaviours[key]; 77 | if(behaviour) behaviour.disable(); 78 | } 79 | 80 | public isBehavourEnabled(key: string): boolean { 81 | const behaviour: any = this.$behaviours[key]; 82 | if(behaviour) behaviour.isEnabled(); 83 | return false; 84 | } 85 | 86 | public handleEvent(eventType: string, args?: any): void { 87 | let handledEvent = false; 88 | for(const key in this.$behaviours) { 89 | const behaviour = this.$behaviours[key]; 90 | if(behaviour.eventType == eventType && behaviour.isEnabled()) { 91 | behaviour.onEvent(args); 92 | handledEvent = true; 93 | } 94 | } 95 | if(!handledEvent) { 96 | console.warn('[Entity]', eventType, 'not handled in', this.uuid); 97 | } 98 | } 99 | 100 | /** 101 | * Iterates through the behaviours and fires their onUpdate method 102 | **/ 103 | onUpdate(deltaTime: number): void { 104 | for(const key in this.$behaviours) { 105 | const behaviour = this.$behaviours[key]; 106 | if(behaviour.isEnabled()) { 107 | behaviour.onUpdate(deltaTime); 108 | } 109 | } 110 | } 111 | 112 | public getBulletSpawnLocation(): Position { 113 | return new Position(this.position.x + this.bulletOffsetX, this.position.y + this.bulletOffsetY); 114 | } 115 | 116 | onInitGame(state: any): void { 117 | this.$state = state; 118 | } 119 | 120 | addKill(currentWave: number, modelType: string): void { 121 | //do nothing. 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/helpers/CollisionHelper.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../models/Entity'; 2 | import { C, CT } from '../Constants'; 3 | import * as Intersects from 'intersects'; 4 | 5 | export class CollisionHelper { 6 | 7 | static collisionBetween(e1: Entity, e2: Entity): boolean { 8 | if(e1.collisionType == CT.CIRCLE && e2.collisionType == CT.CIRCLE) 9 | return Intersects.circleCircle(e1.position.x, e1.position.y, e1.radius, e2.position.x, e2.position.y, e2.radius); 10 | if(e1.collisionType == CT.CIRCLE && e2.collisionType == CT.ELLIPSE) 11 | return Intersects.circleEllipse(e1.position.x, e1.position.y, e1.radius, e2.position.x, e2.position.y, e2.radiusX, e2.radiusY); 12 | if(e1.collisionType == CT.CIRCLE && e2.collisionType == CT.BOX) 13 | return Intersects.circleBox(e1.position.x, e1.position.y, e1.radius, e2.position.x, e2.position.y, e2.width, e2.height); 14 | if(e1.collisionType == CT.ELLIPSE && e2.collisionType == CT.CIRCLE) 15 | return Intersects.ellipseCircle(e1.position.x, e1.position.y, e1.radiusX, e1.radiusY, e2.position.x, e2.position.y, e2.radius); 16 | if(e1.collisionType == CT.ELLIPSE && e2.collisionType == CT.ELLIPSE) 17 | return Intersects.ellipseEllipse(e1.position.x, e1.position.y, e1.radiusX, e1.radiusY, e2.position.x, e2.position.y, e2.radiusX, e2.radiusY); 18 | if(e1.collisionType == CT.ELLIPSE && e2.collisionType == CT.BOX) 19 | return Intersects.ellipseBox(e1.position.x, e1.position.y, e1.radiusX, e1.radiusY, e2.position.x, e2.position.y,e2.width, e2.height); 20 | if(e1.collisionType == CT.BOX && e2.collisionType == CT.CIRCLE) 21 | return Intersects.boxCircle(e1.position.x, e1.position.y, e1.width, e1.height, e2.position.x, e2.position.y, e2.radius); 22 | if(e1.collisionType == CT.BOX && e2.collisionType == CT.ELLIPSE) 23 | return Intersects.boxEllipse(e1.position.x, e1.position.y, e1.width, e1.height, e2.position.x, e2.position.y, e2.radiusX, e2.radiusY); 24 | if(e1.collisionType == CT.BOX && e2.collisionType == CT.BOX) 25 | return Intersects.boxBox(e1.position.x, e1.position.y, e1.width, e1.height, e2.position.x, e2.position.y, e2.width, e2.height); 26 | console.log("[CollisionHelper] Could not resolve collision.", e1, e2); 27 | } 28 | 29 | static distance(x1: number, y1: number, x2: number, y2: number): number { 30 | const dx: number = x1 - x2; 31 | const dy: number = y1 - y2; 32 | return Math.sqrt(dx*dx + dy*dy); 33 | } 34 | 35 | static insideBounds(e: Entity): boolean { 36 | if( e.position.x > C.BOUNDS.minX - (C.SPAWN_OFFSET * 2) && 37 | e.position.x < C.BOUNDS.maxX + (C.SPAWN_OFFSET * 2) && 38 | e.position.y > C.BOUNDS.minY - (C.SPAWN_OFFSET * 2) && 39 | e.position.y < C.BOUNDS.maxY + (C.SPAWN_OFFSET * 2)) { 40 | return true; 41 | } 42 | return false; 43 | } 44 | 45 | static tooFar(e: Entity): boolean { 46 | if(e.position.x > C.BOUNDS.maxX + (C.SPAWN_OFFSET * 10) || 47 | e.position.x < C.BOUNDS.minX - (C.SPAWN_OFFSET * 10) || 48 | e.position.y > C.BOUNDS.maxY + (C.SPAWN_OFFSET * 10) || 49 | e.position.y < C.BOUNDS.minY - (C.SPAWN_OFFSET * 10) ) { 50 | return true; 51 | } 52 | return false; 53 | } 54 | 55 | static outsideBounds(e: Entity): boolean { 56 | if( e.position.x < C.BOUNDS.minX - (C.SPAWN_OFFSET * 2) || 57 | e.position.x > C.BOUNDS.maxX + (C.SPAWN_OFFSET * 2) || 58 | e.position.y < C.BOUNDS.minY - (C.SPAWN_OFFSET * 2) || 59 | e.position.y > C.BOUNDS.maxY + (C.SPAWN_OFFSET * 2)) { 60 | return true; 61 | } 62 | return false; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Special.ts: -------------------------------------------------------------------------------- 1 | import { WeaponCharge, Thrusters, 2 | RammingShield, Shotgun, ScatterShot, HyperSpeed, Invisibility, 3 | Bomb, MissileBarage, MegaBomb, ShieldRecharge, ForceShield } from './models/special'; 4 | 5 | export class SPECIAL { 6 | public static TYPE = { 7 | "Weapon Charge": { 8 | 'systemType': WeaponCharge, 9 | 'description': `Increate the damage, radius and explosive radius of your next attack.`, 10 | 'amount': 2, 11 | 'fireRate': 3000 12 | }, 13 | "Shotgun": { 14 | 'systemType': Shotgun, 15 | 'description': `Fires a blast of short range molten slugs in front of your ship.`, 16 | 'fireRate': 5000, 17 | 'unlockKey': "maxKills_Weapon Charge", 18 | 'unlockCount': 200 19 | }, 20 | "Bomb": { 21 | 'systemType': Bomb, 22 | 'description': `Fires a slow moving bomb in front of your ship that explodes on impact.`, 23 | 'fireRate': 5000, 24 | 'unlockKey': "maxKills_Shotgun", 25 | 'unlockCount': 200 26 | }, 27 | "Mega Bomb": { 28 | 'systemType': MegaBomb, 29 | 'description': `Fires a slow moving bomb in front of your ship, has a large explosion on impact.`, 30 | 'fireRate': 10000, 31 | 'unlockKey': "maxKills_Bomb", 32 | 'unlockCount': 200 33 | }, 34 | "Scatter Shot": { 35 | 'systemType': ScatterShot, 36 | 'description': `Fires a blast of short range molten slugs around your ship.`, 37 | 'fireRate': 15000, 38 | 'unlockKey': "maxKills_Mega Bomb", 39 | 'unlockCount': 200 40 | }, 41 | "Missile Barrage": { 42 | 'systemType': MissileBarage, 43 | 'description': `Fires a mass off tracking missiles from the front of your ship.`, 44 | 'fireRate': 5000, 45 | 'unlockKey': "maxKills_Scatter Shot", 46 | 'unlockCount': 200 47 | }, 48 | "Shield Recharge": { 49 | 'systemType': ShieldRecharge, 50 | 'description': `Immediately recharge your shields. When used at full charge causes an explosion around your ship.`, 51 | 'amount': 1, 52 | 'fireRate': 15000 53 | }, 54 | "Ramming Shield": { 55 | 'systemType': RammingShield, 56 | 'description': `Protect your ship from collision damage allowing you to ram enemies.`, 57 | 'duration': 4000, 58 | 'fireRate': 10000, 59 | 'unlockKey': "maxKills_Shield Recharge", 60 | 'unlockCount': 200 61 | }, 62 | "Force Shield": { 63 | 'systemType': ForceShield, 64 | 'description': `Protect your ship from all forms of damage.`, 65 | 'duration': 3000, 66 | 'fireRate': 15000, 67 | 'unlockKey': "maxKills_Ramming Shield", 68 | 'unlockCount': 200 69 | }, 70 | "Thrusters": { 71 | 'systemType': Thrusters, 72 | 'description': `Doubles your speed and accelleration causing damage to all enemies in your wake.`, 73 | 'amount': 2, 74 | 'duration': 2000, 75 | 'fireRate': 4000 76 | }, 77 | "Hyper Speed": { 78 | 'systemType': HyperSpeed, 79 | 'description': `Create a wormhole and travel a short distance thought spacetime damaging all enemies in your path.`, 80 | 'amount': 250, 81 | 'fireRate': 5000, 82 | 'unlockKey': "maxKills_Thrusters", 83 | 'unlockCount': 200 84 | }, 85 | "Invisibility": { 86 | 'systemType': Invisibility, 87 | 'description': `Turn invisible and untargettable by enemies and missile. Become invulnerable for a brief moment when activated and creating an explosion when reappearing.`, 88 | 'duration': 5000, 89 | 'fireRate': 12000, 90 | 'unlockKey': "maxKills_Hyper Speed", 91 | 'unlockCount': 200 92 | }, 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /src/behaviours/enemy/movement/SimpleFlockingPath.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '../../behaviour'; 2 | import { Enemy, Position } from '../../../models'; 3 | import { CollisionHelper } from '../../../helpers/CollisionHelper'; 4 | 5 | export class SimpleFlockingPath extends Behaviour { 6 | 7 | destination: Position; 8 | flock: Enemy[]; 9 | 10 | enteredScreen = false; 11 | 12 | target: Enemy; 13 | 14 | constructor(target: Enemy, args: { destination: Position; flock: Enemy[] }) { 15 | super('SimpleFlockingPath', target); 16 | this.flock = args.flock || []; 17 | this.destination = args.destination || new Position(0, 0); 18 | this.target.velocity = new Position(0,0); 19 | } 20 | 21 | onUpdate(deltaTime: number): void { 22 | if(this.flock.length > 1) { 23 | this.flockBehaviour(deltaTime); 24 | } else { 25 | this.moveTowardsTarget(deltaTime); 26 | } 27 | 28 | if(!this.enteredScreen && CollisionHelper.insideBounds(this.target)){ 29 | this.enteredScreen = true; 30 | } 31 | if(this.enteredScreen && CollisionHelper.outsideBounds(this.target) || CollisionHelper.tooFar(this.target)) { 32 | this.target.handleEvent('destroyed'); 33 | } 34 | if(this.target.position.distanceTo(this.destination) < 50 ) { 35 | this.target.handleEvent('destroyed'); 36 | } 37 | } 38 | 39 | private moveTowardsTarget(deltaTime: number): void { 40 | this.target.velocity.x = this.destination.x - this.target.position.x; 41 | this.target.velocity.y = this.destination.y - this.target.position.y; 42 | this.target.velocity.capSpeed(this.target.speed); 43 | this.target.position.x += this.target.velocity.x * deltaTime / 1000; 44 | this.target.position.y += this.target.velocity.y * deltaTime / 1000; 45 | 46 | } 47 | 48 | private flockBehaviour(deltaTime: number): void { 49 | const v1: Position = this.towardsCenter(); 50 | const v2: Position = this.keepDistance(); 51 | const v3: Position = this.matchVelocity(); 52 | const v4: Position = this.towardsDestination(); 53 | v4.capSpeed(1); 54 | 55 | this.target.velocity.x = this.target.velocity.x + v1.x + v2.x + v3.x + v4.x; 56 | this.target.velocity.y = this.target.velocity.y + v1.y + v2.y + v3.y + v4.y; 57 | 58 | /** 59 | * Maintain max speed 60 | **/ 61 | this.target.velocity.capSpeed(this.target.speed); 62 | 63 | this.target.position.x += this.target.velocity.x * deltaTime / 1000; 64 | this.target.position.y += this.target.velocity.y * deltaTime / 1000; 65 | } 66 | 67 | private towardsCenter(): Position { 68 | const pc = new Position(0,0); 69 | for(const e of this.flock) { 70 | if(e != this.target) { 71 | pc.add(e.position); 72 | } 73 | } 74 | 75 | pc.divN(this.flock.length - 1); 76 | pc.sub(this.target.position); 77 | pc.divN(100); 78 | 79 | return pc; 80 | } 81 | 82 | private keepDistance(): Position { 83 | const c = new Position(0, 0); 84 | for(const e of this.flock) { 85 | if( e != this.target ) { 86 | if(e.position.distanceTo(this.target.position) < this.target.radius * 4) { 87 | c.x = c.x - (e.position.x - this.target.position.x); 88 | c.y = c.y - (e.position.y - this.target.position.y); 89 | } 90 | } 91 | } 92 | return c; 93 | } 94 | 95 | private matchVelocity(): Position { 96 | const pv: Position = new Position(0,0); 97 | 98 | for(const e of this.flock) { 99 | if(e != this.target) { 100 | pv.add(e.velocity); 101 | } 102 | } 103 | 104 | pv.divN(this.flock.length - 1); 105 | 106 | pv.sub(this.target.velocity); 107 | pv.divN(8); 108 | 109 | return pv; 110 | } 111 | 112 | private towardsDestination(): Position { 113 | const x: number = (this.destination.x - this.target.position.x) / 100; 114 | const y: number = (this.destination.y - this.target.position.y) / 100; 115 | return new Position(x, y); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/helpers/ShipHelper.ts: -------------------------------------------------------------------------------- 1 | import { DB } from '../Database'; 2 | import { Ship } from '../models/Ship'; 3 | import { ShipList } from '../models/messages'; 4 | import { SHIP } from '../Ship'; 5 | import { AccountHelper } from './AccountHelper'; 6 | 7 | export class ShipHelper { 8 | 9 | static async getShip(username: string, shipUuid: string): Promise { 10 | const ship = await DB.$ships.findOne({username, uuid: shipUuid}); 11 | if(ship) return new Ship(ship); 12 | return null; 13 | } 14 | 15 | static async getShips(username: string): Promise { 16 | return await DB.$ships.find({username}).toArray(); 17 | } 18 | 19 | static async getShipList(username: string): Promise { 20 | const ships = await ShipHelper.getShips(username); 21 | const shipList: ShipList = new ShipList(); 22 | for(let i = 0, l = ships.length; i < l; i++) { 23 | shipList.ships[ships[i].uuid] = new Ship(ships[i]); 24 | } 25 | return shipList; 26 | } 27 | 28 | static async validateShipParameters(username: string, data: { name: string; shipType: string; shipMaterial: string; primaryWeapon: string; specialWeapon: string }): Promise { 29 | const account = await AccountHelper.getAccountByUsername(username); 30 | if (data['name'].length > 3 && account.isUnlocked(data['shipType']) && account.isUnlocked(data['shipMaterial']) && account.isUnlocked(data['primaryWeapon']) && account.isUnlocked(data['specialWeapon'])) { 31 | return true; 32 | } 33 | return false; 34 | } 35 | 36 | static async createShip(username: string, data: { name: string; shipType: string; shipMaterial: string; primaryWeapon: string; specialWeapon: string }): Promise { 37 | const canCreate = ShipHelper.validateShipParameters(username, data); 38 | if(!canCreate) return false; 39 | 40 | let shipData = {}; 41 | shipData['username'] = username; 42 | shipData['name'] = data.name; 43 | shipData['shipType'] = data.shipType; 44 | shipData['shipMaterial'] = data.shipMaterial; 45 | shipData['primaryWeapon'] = data.primaryWeapon; 46 | shipData['specialWeapon'] = data.specialWeapon; 47 | shipData['level'] = 1; 48 | shipData['createdAt'] = Date.now(); 49 | 50 | const account = await AccountHelper.getAccountByUsername(username); 51 | account.increaseStat("shipsCreated", 1); 52 | AccountHelper.saveAccount(account); 53 | 54 | const ship = new Ship(shipData); 55 | return await DB.$ships.insertOne(ship.toSaveObject()); 56 | } 57 | 58 | static async saveShip(ship: Ship): Promise { 59 | return await DB.$ships.updateOne({uuid: ship.uuid}, { $set: ship.toSaveObject() }); 60 | } 61 | 62 | static async deleteShip(username: string, uuid: string): Promise { 63 | const account = await AccountHelper.getAccountByUsername(username); 64 | account.increaseStat("shipsDestroyed", 1); 65 | AccountHelper.saveAccount(account); 66 | return await DB.$ships.deleteOne({username: username, uuid: uuid}); 67 | } 68 | 69 | static async upgradeShip(ship: Ship, upgrades: any): Promise { 70 | let spentPoints = 0; 71 | for (const key in upgrades) { 72 | spentPoints += upgrades[key]; 73 | } 74 | if(spentPoints > ship.upgradePoints) { 75 | return false; 76 | } else { 77 | ship.upgradePoints -= spentPoints; 78 | for(const key in upgrades) { 79 | ship["upgrade" + key] += upgrades[key]; 80 | } 81 | return await DB.$ships.updateOne({uuid: ship.uuid}, {$set: ship.toSaveObject() }); 82 | } 83 | } 84 | 85 | static async addInGame(uuid: string): Promise { 86 | return await DB.$ships.updateOne({uuid: uuid}, { $set: { inGame: 1 }}); 87 | } 88 | 89 | static async removeInGame(uuid: string): Promise { 90 | return await DB.$ships.updateOne({uuid: uuid}, { $set: { inGame: -1 }}); 91 | } 92 | 93 | static async getShipInGame(username: string): Promise { 94 | const data = await DB.$ships.findOne({ username, inGame: 1}); 95 | if(!data) return; 96 | return new Ship(data); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import * as bodyParser from 'body-parser'; 3 | import cors from 'cors'; 4 | import { createServer } from 'http'; 5 | import { Server } from 'colyseus'; 6 | import { monitor } from '@colyseus/monitor'; 7 | 8 | import { ShipBuilderRoom } from "./rooms/ShipBuilderRoom"; 9 | import { MatchMakerRoom } from "./rooms/MatchMakerRoom"; 10 | import { GameRoom } from "./rooms/GameRoom"; 11 | 12 | import { Account } from "./models/Account"; 13 | 14 | import { AccountHelper } from './helpers/AccountHelper'; 15 | import { JWTHelper } from './helpers/JWTHelper'; 16 | 17 | import { DB } from './Database'; 18 | 19 | const asyncMiddleware = fn => 20 | (req, res, next) => { 21 | Promise.resolve(fn(req, res, next)) 22 | .catch(next); 23 | }; 24 | 25 | DB.init().then(() => { 26 | /** Mark all ships out of game when the server restarts **/ 27 | DB.$ships.updateMany({}, { $set: { inGame: -1} }); 28 | 29 | const port = Number(process.env.PORT || 2567 ) + Number(process.env.NODE_APP_INSTANCE || 0); 30 | const app = express(); 31 | 32 | const server = createServer(app) 33 | 34 | const gameServer = new Server({ 35 | server: server 36 | }); 37 | 38 | gameServer.define("ShipBuilderRoom", ShipBuilderRoom); 39 | gameServer.define("MatchMakerRoom", MatchMakerRoom); 40 | gameServer.define("GameRoom", GameRoom); 41 | 42 | app.use(cors()); 43 | app.use(bodyParser.urlencoded({ extended: false })); 44 | app.use(bodyParser.json()); 45 | 46 | 47 | app.use('/colyseus', monitor()); 48 | 49 | app.post('/quick_login', asyncMiddleware( async function(req, res, ) { 50 | const { systemId } = req.body; 51 | console.log("System_ID", systemId) 52 | let account = null; 53 | try { 54 | account = await AccountHelper.getAccountBySystemID(systemId); 55 | } catch (err) { 56 | console.log("[QuickLogin] (error)", err); 57 | res.status(401).json({ 58 | message: "Unable to sign in.", 59 | error: err 60 | }); 61 | return; 62 | } 63 | 64 | if(account) { 65 | res.status(200).json(JWTHelper.getSuccessJSON(account.username)); 66 | } else { 67 | res.status(401).json({ 68 | message: "Could not create your account." 69 | }); 70 | } 71 | 72 | })); 73 | 74 | app.post('/login', asyncMiddleware( async function(req, res, ) { 75 | const { email, password } = req.body; 76 | let account = null; 77 | try { 78 | account = await AccountHelper.getAccountByEmail(email); 79 | } catch (err) { 80 | res.status(401).json({ 81 | message: "Unable to sign in.", 82 | error: err 83 | }); 84 | return; 85 | } 86 | 87 | if(account && account.password == password){ //TODO: NEED TO SALT THIS PASSWORD CHECK 88 | res.status(200).json(JWTHelper.getSuccessJSON(account.username)); 89 | } else { 90 | res.status(401).json({ 91 | message: "Validation failed. Check your email and password and try again." 92 | }); 93 | } 94 | })); 95 | 96 | app.post('/signup', asyncMiddleware( async function(req, res, ) { 97 | const { username, email, password } = req.body; 98 | if(username && email && password) { 99 | let account = null; 100 | try { 101 | account = await AccountHelper.createAccount(new Account({username: username, email: email, password: password })); //TODO: NEED TO SALT THIS PASSWORD CHECK 102 | } catch (err) { 103 | res.status(401).json({ 104 | message: "Unable to create account.", 105 | error: err 106 | }); 107 | console.log(err); 108 | return; 109 | } 110 | 111 | if(account) { 112 | res.status(200).json(JWTHelper.getSuccessJSON(account.username)); 113 | } else { 114 | res.status(401).json({ 115 | message: "Unable to create account. Null account." 116 | }); 117 | } 118 | } else { 119 | res.status(401).json({ 120 | message: "Unable to create account. Missing data." 121 | }) 122 | } 123 | })); 124 | 125 | app.post('/renew', asyncMiddleware( async function(req, res, ) { 126 | const { token } = req.body; 127 | if(token) { 128 | if(JWTHelper.verifyToken(token)) { 129 | const username = JWTHelper.extractUsernameFromToken(token); 130 | console.log("Sending new token"); 131 | res.status(200).json(JWTHelper.getSuccessJSON(username)); 132 | } else { 133 | res.status(401).json({ 134 | message: "Invalid token." 135 | }); 136 | } 137 | } else { 138 | res.status(401).json({ 139 | message: "Must supply access_token for renewal." 140 | }); 141 | return; 142 | } 143 | })); 144 | 145 | 146 | console.log('[SERVER] Starting on port: ' + port); 147 | server.listen(port); 148 | }); 149 | -------------------------------------------------------------------------------- /src/rooms/GameRoom.ts: -------------------------------------------------------------------------------- 1 | import { Room, Delayed , Client} from 'colyseus'; 2 | 3 | import { JWTHelper } from '../helpers/JWTHelper'; 4 | import { ShipHelper } from '../helpers/ShipHelper'; 5 | 6 | import { Bullet, Enemy, Ship, Drop } from '../models'; 7 | import { GameState } from '../models/states/GameState'; 8 | 9 | import { Spawner } from '../spawner/Spawner'; 10 | 11 | export class GameRoom extends Room { 12 | 13 | maxClients = 4; 14 | 15 | spawnCompleteFrequency = 1000; 16 | 17 | clientShipHash: any = {}; 18 | 19 | spawner; 20 | 21 | private spawnCompleteInterval: Delayed; 22 | 23 | onCreate(options: any): void { 24 | console.log('[GameRoom]', this.roomId, 'Created'); 25 | this.setSimulationInterval((deltaTime) => this.onUpdate(deltaTime)); 26 | this.setState(new GameState()); 27 | 28 | this.state.startWave = Math.max(options.waveRank, 1) || 1; 29 | this.state.currentWave = this.state.startWave; 30 | 31 | this.spawner = new Spawner(this); 32 | 33 | // const gameStartTimeout = this.clock.setInterval(() => { 34 | // this.state.startGame -= 1; 35 | // this.broadcast(`Battle Starts In ${this.state.startGame} Seconds`); 36 | // if(this.state.startGame <= 0) { 37 | // this.startWave(); 38 | // gameStartTimeout.clear(); 39 | // } 40 | // }, 1000); 41 | 42 | } 43 | 44 | async onAuth(client: Client, options: any): Promise { 45 | const isValidToken = await JWTHelper.verifyToken(options.token); 46 | 47 | if(!isValidToken) { 48 | this.send(client, { error: 'error_invalid_token' }); 49 | return false; 50 | } 51 | 52 | const username = JWTHelper.extractUsernameFromToken(options.token) 53 | 54 | return username; 55 | } 56 | 57 | async onJoin(client: Client, options: any, username: string): Promise { 58 | console.log('[GameRoom]', this.roomId, 'Client Join', username); 59 | const ship = await ShipHelper.getShipInGame(username); 60 | ship.position.x = 700 + (Math.random() * 200); 61 | ship.position.y = 350 + (Math.random() * 200); 62 | if(!ship) { 63 | this.send(client, { error: 'no_ship_in_game'}); 64 | return; 65 | } 66 | ship.connected = true; 67 | this.clientShipHash[client.id] = ship; 68 | this.state.addShip(ship) 69 | } 70 | 71 | onMessage(client: Client, data: any): void { 72 | if(data.action === "input") this.handleClientInput(client, data.input); 73 | } 74 | 75 | async onLeave(client: Client, consented: boolean): Promise { 76 | console.log('[GameRoom] On Leave (consented):', consented) 77 | const ship = this.clientShipHash[client.id]; 78 | ship.connected = false; 79 | try { 80 | if(consented) { 81 | /** This error allows the room to be cleaned up **/ 82 | throw new Error("consented leave"); 83 | } 84 | // allow a disconnected client up to 5 seconds to reconnect. 85 | await this.allowReconnection(client, 5); 86 | ship.connected = true; 87 | console.log('[GameRoom] Client has Reconnected.'); 88 | } catch(e) { 89 | console.log('[GameRoom] Client Failed to Reconnect.'); 90 | ship.checkLevelUp(); 91 | ship.updateWaveRank(this.state.currentWave); 92 | ShipHelper.saveShip(ship); 93 | this.state.removeShip(ship); 94 | ShipHelper.removeInGame(ship.uuid); 95 | delete this.clientShipHash[client.id]; 96 | } 97 | } 98 | 99 | onDispose(): void { 100 | console.log("[GameRoom]", this.roomId, "Disposed"); 101 | } 102 | 103 | onUpdate( deltaTime: number ): void { 104 | let uuid; 105 | for(uuid in this.state.ships) { 106 | const ship: Ship = this.state.ships[uuid]; 107 | ship.onUpdate(deltaTime); 108 | } 109 | for(uuid in this.state.enemies) { 110 | const enemy: Enemy = this.state.enemies[uuid]; 111 | enemy.onUpdate(deltaTime); 112 | } 113 | for(uuid in this.state.bullets) { 114 | const bullet: Bullet = this.state.bullets[uuid]; 115 | bullet.onUpdate(deltaTime); 116 | } 117 | for(uuid in this.state.drops) { 118 | const drop: Drop = this.state.drops[uuid]; 119 | drop.onUpdate(deltaTime); 120 | } 121 | 122 | if(this.state.hasStarted() && !this.state.hasShips()) { 123 | console.log(`[GameRoom (${this.roomId})] Battle Lost`); 124 | this.broadcast('The Battle Has Been Lost'); 125 | this.state.battleLost(); 126 | this.disconnect(); 127 | } 128 | 129 | this.spawner.onUpdate(deltaTime); 130 | } 131 | 132 | handleClientInput(client: Client, input: any): void { 133 | const ship: Ship = this.clientShipHash[client.id]; 134 | ship.handleEvent('input', input); 135 | } 136 | 137 | announceNextWave() { 138 | this.broadcast(`Wave ${this.state.currentWave} Incoming`); 139 | console.log(`[GameRoom (${this.roomId})] Wave ${this.state.currentWave} Incoming`); 140 | } 141 | 142 | announceBossWave() { 143 | this.broadcast(`Boss Incoming`); 144 | console.log(`[GameRoom (${this.roomId})] Boss Incoming`) 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/models/states/GameState.ts: -------------------------------------------------------------------------------- 1 | import { Schema, type, MapSchema } from '@colyseus/schema'; 2 | 3 | import { Ship } from '../Ship'; 4 | import { Enemy } from '../Enemy'; 5 | import { Bullet } from '../Bullet'; 6 | import { Drop } from '../Drop'; 7 | 8 | import { CollisionHelper } from '../../helpers/CollisionHelper'; 9 | 10 | export class GameState extends Schema { 11 | 12 | @type({map:Ship}) 13 | ships = new MapSchema(); 14 | 15 | @type({map:Enemy}) 16 | enemies = new MapSchema(); 17 | 18 | @type({map:Bullet}) 19 | bullets = new MapSchema(); 20 | 21 | @type({map:Drop}) 22 | drops = new MapSchema(); 23 | 24 | @type("number") 25 | startGame = 5; //number of seconds until the game starts 26 | 27 | @type("int32") 28 | startWave = 0; 29 | 30 | @type("int32") 31 | currentWave = 0; 32 | 33 | @type("int32") 34 | enemiesSpawned = 0; 35 | 36 | @type("int32") 37 | enemiesKilled = 0; 38 | 39 | addShip(ship: Ship): void { 40 | this.ships[ship.uuid] = ship; 41 | ship.onInitGame(this); 42 | } 43 | 44 | removeShip(ship: Ship): void { 45 | delete this.ships[ship.uuid]; 46 | } 47 | 48 | addDrop(drop: Drop): void { 49 | this.drops[drop.uuid] = drop; 50 | drop.onInitGame(this); 51 | } 52 | 53 | removeDrop(drop: Drop): void { 54 | delete this.drops[drop.uuid]; 55 | } 56 | 57 | addEnemy(enemy: Enemy): void { 58 | this.enemies[enemy.uuid] = enemy; 59 | this.enemiesSpawned++; 60 | enemy.onInitGame(this); 61 | } 62 | 63 | removeEnemy(enemy: Enemy): void { 64 | delete this.enemies[enemy.uuid]; 65 | 66 | /** Cleanup flocking **/ 67 | if(enemy.flock !== undefined) { 68 | for(let i = 0, l = enemy.flock.length; i < l; i++) { 69 | if(enemy.flock[i].uuid == enemy.uuid) { 70 | enemy.flock.splice(i,1); 71 | break; 72 | } 73 | } 74 | } 75 | } 76 | 77 | numberEnemies(): number { 78 | return Object.keys(this.enemies).length; 79 | } 80 | 81 | addBullet(bullet: Bullet): void { 82 | this.bullets[bullet.uuid] = bullet; 83 | bullet.onInitGame(this); 84 | } 85 | 86 | addBullets(bullets: Bullet[]): void { 87 | for(let i = 0; i < bullets.length; i++){ 88 | this.addBullet(bullets[i]); 89 | } 90 | } 91 | 92 | removeBullet(bullet: Bullet): void { 93 | delete this.bullets[bullet.uuid]; 94 | } 95 | 96 | removeAllShips(): void { 97 | for(const uuid in this.ships) { 98 | this.removeEnemy(this.ships[uuid]); 99 | } 100 | } 101 | 102 | removeAllBullets(): void { 103 | for(const uuid in this.bullets) { 104 | this.removeEnemy(this.bullets[uuid]); 105 | } 106 | } 107 | 108 | removeAllEnemies(): void { 109 | for(const uuid in this.enemies) { 110 | this.removeEnemy(this.enemies[uuid]); 111 | } 112 | } 113 | 114 | battleLost(): void { 115 | this.removeAllShips(); 116 | this.removeAllBullets(); 117 | this.removeAllEnemies(); 118 | } 119 | 120 | hasStarted(): boolean { 121 | return this.startGame <= 0; 122 | } 123 | 124 | hasEnemies(): boolean { 125 | return Object.keys(this.enemies).length > 0; 126 | } 127 | 128 | hasShips(): boolean { 129 | return Object.keys(this.ships).length > 0; 130 | } 131 | 132 | getClosestEnemy(x: number, y: number, ignoreInvisible= false): Enemy { 133 | let returnEnemy: Enemy = null; 134 | let distance = 99999; 135 | for(const key in this.enemies) { 136 | const enemy = this.enemies[key]; 137 | if(enemy.invisible && ignoreInvisible == false) continue; 138 | const d: number = CollisionHelper.distance(x, y, enemy.position.x, enemy.position.y); 139 | if(d < distance) { 140 | returnEnemy = enemy; 141 | distance = d; 142 | } 143 | } 144 | return returnEnemy; 145 | } 146 | 147 | getEnemiesInRange(x: number, y: number, radius: number, ignoreInvisible = false): Enemy[] { 148 | const enemies: Enemy[] = []; 149 | for(const key in this.enemies) { 150 | const enemy = this.enemies[key]; 151 | if(enemy.invisible && ignoreInvisible == false) continue; 152 | const d: number = CollisionHelper.distance(x, y, enemy.position.x, enemy.position.y); 153 | if(d <= radius){ 154 | enemies.push(enemy); 155 | } 156 | } 157 | return enemies; 158 | } 159 | 160 | getShipsInRange(x: number, y: number, radius: number, ignoreInvisible = false): Ship[] { 161 | const ships: Ship[] = []; 162 | for(const key in this.ships) { 163 | const ship = this.ships[key]; 164 | if(ship.invisible && ignoreInvisible == false) continue; 165 | const d: number = CollisionHelper.distance(x, y, ship.position.x, ship.position.y); 166 | if(d <= radius){ 167 | ships.push(ship); 168 | } 169 | } 170 | return ships; 171 | } 172 | 173 | getClosestShip(x: number, y: number, ignoreInvisible = false): Ship { 174 | let returnShip: Ship = null; 175 | let distance = 99999; 176 | for(const key in this.ships) { 177 | const ship = this.ships[key]; 178 | if(ship.invisible && ignoreInvisible == false) continue; 179 | const d: number = CollisionHelper.distance(x, y, ship.position.x, ship.position.y); 180 | if(d < distance) { 181 | returnShip = ship; 182 | distance = d; 183 | } 184 | } 185 | return returnShip; 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /src/rooms/ShipBuilderRoom.ts: -------------------------------------------------------------------------------- 1 | import { Room, Client } from 'colyseus'; 2 | 3 | import { JWTHelper } from '../helpers/JWTHelper'; 4 | import { AccountHelper } from '../helpers/AccountHelper'; 5 | import { ShipHelper } from '../helpers/ShipHelper'; 6 | 7 | import { ErrorMessage, ShipList } from '../models/messages'; 8 | import { ShipBuilderState } from '../models/states/ShipBuilderState'; 9 | 10 | export class ShipBuilderRoom extends Room { 11 | 12 | private clientUsernameHash: any = {}; 13 | 14 | onCreate(options: any): void { 15 | this.setState(new ShipBuilderState()); 16 | } 17 | 18 | async onAuth(client: Client, options: any): Promise { 19 | console.log("[ShipBuilderRoom] Client auth attempt"); 20 | const isValidToken = await JWTHelper.verifyToken(options.token); 21 | 22 | if(!isValidToken) { 23 | this.send(client, { 24 | error: 'error_invalid_token' 25 | }); 26 | return false; 27 | } 28 | 29 | const username = JWTHelper.extractUsernameFromToken(options.token); 30 | 31 | return username; 32 | } 33 | 34 | onJoin(client: Client, options: any, username: string): void { 35 | console.log("[ShipBuilderRoom] Client joined: ", username); 36 | this.clientUsernameHash[client.id] = username; 37 | AccountHelper.clearInGame(username); 38 | this.sendShips(client); 39 | this.unlockedData(client); 40 | } 41 | 42 | onLeave(client: Client) { 43 | delete this.clientUsernameHash[client.id]; 44 | } 45 | 46 | onMessage(client: Client, data: any): void { 47 | switch(data.action) { 48 | case 'play': this.playShip(client, data.uuid); return; 49 | case 'create': this.createShip(client, data.ship); return; 50 | case 'upgrade': this.upgradeShip(client, data.uuid, data.upgrades); return; 51 | case 'delete': this.deleteShip(client, data.uuid); return; 52 | case 'unlocked': this.unlockedData(client); return; 53 | case 'stats': this.statsData(client); return; 54 | } 55 | } 56 | 57 | private async unlockedData(client: Client): Promise { 58 | const username = this.clientUsernameHash[client.id]; 59 | console.log('[ShipBuilderRoom] Starting unlockedData for', username); 60 | const account = await AccountHelper.getAccountByUsername(username); 61 | if(!account) { 62 | this.send(client, new ErrorMessage('Your account could not be found', 'invalid_account')); 63 | return; 64 | } 65 | console.log("[ShipBuilderRoom] Sending UnlockMessage"); 66 | this.send(client, account.getUnlockMessage()); 67 | } 68 | 69 | private async statsData(client: Client): Promise { 70 | const account = await AccountHelper.getAccountByUsername(this.clientUsernameHash[client.id]); 71 | if(!account) { 72 | this.send(client, new ErrorMessage('Your account could not be found', 'invalid_account')); 73 | return; 74 | } 75 | 76 | this.send(client, account.getStatistics()); 77 | 78 | //this.send(client, { action: "stats", message: new MapSchema(account.stats) }); 79 | } 80 | 81 | private async upgradeShip(client: Client, uuid: string, upgrades: any): Promise { 82 | const ship = await ShipHelper.getShip(this.clientUsernameHash[client.id], uuid); 83 | if(!ship) { 84 | this.send(client, new ErrorMessage('The ship requested could not be found', 'invalid_ship')); 85 | return; 86 | } 87 | const returnShip = await ShipHelper.upgradeShip(ship, upgrades); 88 | //console.log("Return Ship", returnShip); 89 | if(returnShip) { 90 | this.send(client, { action: 'ship_upgrade_success'}); 91 | this.sendShips(client); 92 | return; 93 | } else { 94 | this.send(client, new ErrorMessage('The ship could not be upgraded', 'error_could_not_upgrade')); 95 | return; 96 | } 97 | } 98 | 99 | private async playShip(client: Client, uuid: string): Promise { 100 | console.log('[ShipBuilderRoom] playShip', uuid); 101 | const ship = await ShipHelper.getShip(this.clientUsernameHash[client.id], uuid); 102 | 103 | if(!ship) { 104 | this.send(client, new ErrorMessage('The ship requested could not be found', 'invalid_ship')); 105 | return; 106 | } 107 | ShipHelper.addInGame(uuid); 108 | this.send(client, { action: 'enter_match_making', ship}); 109 | } 110 | 111 | private async sendShips(client: Client): Promise { 112 | console.log('[ShipBuilderRoom] sending ships'); 113 | const ships: ShipList = await ShipHelper.getShipList(this.clientUsernameHash[client.id]); 114 | this.send(client, ships); 115 | } 116 | 117 | private async createShip(client: Client, ship: any): Promise { 118 | //console.log('[ShipBuilderRoom] creating a ship', ship); 119 | const success = await ShipHelper.createShip(this.clientUsernameHash[client.id], ship); 120 | if(success) { 121 | this.send(client, { action: 'message', message: 'Ship successfully created.'}); 122 | } else { 123 | this.send(client, new ErrorMessage('The ship could not be created', 'create_ship_failure')); 124 | return; 125 | } 126 | this.sendShips(client); 127 | } 128 | 129 | 130 | private async deleteShip(client: Client, uuid: string): Promise { 131 | const success = await ShipHelper.deleteShip(this.clientUsernameHash[client.id], uuid); 132 | if(success) { 133 | this.send(client, { action: 'message', message: 'Ship successfully destroyed.'}); 134 | } else { 135 | this.send(client, new ErrorMessage('The ship requested could not be deleted', 'delete_ship_failure')); 136 | } 137 | this.sendShips(client); 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/models/Account.ts: -------------------------------------------------------------------------------- 1 | import { Schema, type } from "@colyseus/schema"; 2 | import { CommandResult } from 'mongodb'; 3 | import { pick, merge } from 'lodash'; 4 | import { UsernameGenerator } from '../helpers/UsernameGenerator'; 5 | import { Ship } from './Ship'; 6 | import { UnlockMessage, Statistics } from './messages'; 7 | import { UnlockItem } from './UnlockItem'; 8 | import { SHIP } from '../Ship'; 9 | import { PRIMARY } from '../Primary'; 10 | import { SPECIAL } from '../Special'; 11 | import { MATERIAL } from '../Material'; 12 | 13 | export class Account extends Schema { 14 | 15 | createdAt: number; 16 | email: string; 17 | password: string; 18 | 19 | systemId: string; 20 | 21 | @type("string") 22 | username: string; 23 | 24 | unlocked: any; 25 | stats: any; 26 | 27 | constructor(options: any) { 28 | super(); 29 | merge(this, options); 30 | if(!this.unlocked) this.unlocked = options.unlocked || {}; 31 | if(!this.stats) this.stats = options.stats || {}; 32 | if(!this.createdAt) this.createdAt = Date.now(); 33 | if(!this.username) this.username = UsernameGenerator.getUsername(); 34 | } 35 | 36 | getStatistics(): Statistics { 37 | return new Statistics(this.stats); 38 | } 39 | 40 | getUnlockMessage(): UnlockMessage { 41 | let item: UnlockItem; 42 | const message: UnlockMessage = new UnlockMessage({}); 43 | 44 | for(const key in SHIP.TYPE) { 45 | const t = SHIP.TYPE[key]; 46 | if( !("unlockKey" in t) ) { 47 | item = new UnlockItem("", true, 0, "ship", key, t.description) 48 | } else { 49 | item = new UnlockItem(t.unlockKey, (key in this.unlocked), t.unlockCount, "ship", key, t.description) 50 | } 51 | message.unlocks[key] = item; 52 | } 53 | 54 | for(const key in PRIMARY.TYPE) { 55 | const t = PRIMARY.TYPE[key]; 56 | if( !("unlockKey" in t) ) { 57 | item = new UnlockItem("", true, 0, "primary", key, t.description) 58 | } else { 59 | item = new UnlockItem(t.unlockKey, (key in this.unlocked), t.unlockCount, "primary", key, t.description) 60 | } 61 | message.unlocks[key] = item; 62 | } 63 | 64 | for(const key in SPECIAL.TYPE) { 65 | const t = SPECIAL.TYPE[key]; 66 | if( !("unlockKey" in t) ) { 67 | item = new UnlockItem("", true, 0, "special", key, t.description) 68 | } else { 69 | item = new UnlockItem(t.unlockKey, (key in this.unlocked), t.unlockCount, "special", key, t.description) 70 | } 71 | message.unlocks[key] = item; 72 | } 73 | 74 | for(const key in MATERIAL.TYPE) { 75 | const t = MATERIAL.TYPE[key]; 76 | if( !("unlockKey" in t) ) { 77 | item = new UnlockItem("", true, 0, "material", key, t.description) 78 | } else { 79 | item = new UnlockItem(t.unlockKey, (key in this.unlocked), t.unlockCount, "material", key, t.description) 80 | } 81 | message.unlocks[key] = item; 82 | } 83 | 84 | return message; 85 | } 86 | 87 | updateUnlocks(): void { 88 | console.log("Running update unlocks"); 89 | for(const key in SHIP.TYPE) { 90 | /** We need do no tests if already unlocked. **/ 91 | if(this.isUnlocked(key)) continue; 92 | const t = SHIP.TYPE[key]; 93 | if( !("unlockKey" in t) ) { 94 | console.log("Setting a default unlock for", key); 95 | this.unlock(key); 96 | } else { 97 | if(this.getStat(t["unlockKey"]) >= t["unlockCount"]) this.unlock(key); 98 | } 99 | } 100 | 101 | for(const key in SPECIAL.TYPE) { 102 | /** We need do no tests if already unlocked. **/ 103 | if(this.isUnlocked(key)) continue; 104 | const t = SPECIAL.TYPE[key]; 105 | if( !("unlockKey" in t) ) { 106 | this.unlock(key); 107 | } else { 108 | if(this.getStat(t["unlockKey"]) >= t["unlockCount"]) this.unlock(key); 109 | } 110 | } 111 | 112 | for(const key in PRIMARY.TYPE) { 113 | /** We need do no tests if already unlocked. **/ 114 | if(this.isUnlocked(key)) continue; 115 | const t = PRIMARY.TYPE[key]; 116 | if( !("unlockKey" in t) ) { 117 | this.unlock(key); 118 | } else { 119 | if(this.getStat(t["unlockKey"]) >= t["unlockCount"]) this.unlock(key); 120 | } 121 | } 122 | 123 | for(const key in MATERIAL.TYPE) { 124 | /** We need do no tests if already unlocked. **/ 125 | if(this.isUnlocked(key)) continue; 126 | const t = MATERIAL.TYPE[key]; 127 | if( !("unlockKey" in t) ) { 128 | this.unlock(key); 129 | } else { 130 | if(this.getStat(t["unlockKey"]) >= t["unlockCount"]) this.unlock(key); 131 | } 132 | } 133 | 134 | } 135 | 136 | updateStatsWithShip(ship: Ship): void { 137 | this.updateStat("maxLevel", ship.level); 138 | this.updateStat(`maxLevel_${ship.shipType}`, ship.level); 139 | 140 | this.updateStat("maxKills", ship.kills); 141 | this.updateStat(`maxKills_${ship.shipType}`, ship.kills); 142 | this.updateStat(`maxKills_${ship.primaryWeapon}`, ship.kills); 143 | this.updateStat(`maxKills_${ship.specialWeapon}`, ship.kills); 144 | 145 | for(const key in ship.tracker) { 146 | this.updateStat(`maxKills_${key}`, ship.tracker[key]); 147 | } 148 | this.updateUnlocks(); 149 | } 150 | 151 | getStat(type): number { 152 | if(type in this.stats) return this.stats[type]; 153 | return 0; 154 | } 155 | 156 | updateStat(type, count): void { 157 | if(!(type in this.stats) || count > this.stats[type]) this.stats[type] = count; 158 | } 159 | 160 | increaseStat(type, amount): void { 161 | if(!(type in this.stats)) this.stats[type] = amount; 162 | else this.stats[type] += amount; 163 | } 164 | 165 | isUnlocked(type: string): boolean { 166 | if(type in this.unlocked && this.unlocked[type] == true) return true; 167 | return false; 168 | } 169 | 170 | unlock(type: string): void { 171 | this.unlocked[type] = true; 172 | } 173 | 174 | public toSaveObject(): any { 175 | const baseObj: any = pick(this, [ 176 | 'createdAt', 'email', 'password', 'systemId', 'username', 'unlocked', 'stats' 177 | ]); 178 | return baseObj; 179 | } 180 | 181 | 182 | } 183 | -------------------------------------------------------------------------------- /src/rooms/MatchMakerRoom.ts: -------------------------------------------------------------------------------- 1 | import { Room, Client, matchMaker } from 'colyseus'; 2 | import { JWTHelper } from '../helpers/JWTHelper'; 3 | import { ShipHelper } from '../helpers/ShipHelper'; 4 | import { ShipList } from '../models/messages/ShipList'; 5 | import { ShipBuilderState } from '../models/states/ShipBuilderState'; 6 | 7 | interface MatchmakingGroup { 8 | averageRank: number; 9 | clients: ClientStat[]; 10 | priority?: boolean; 11 | 12 | ready?: boolean; 13 | confirmed?: number; 14 | } 15 | 16 | interface ClientStat { 17 | client: Client; 18 | waitingTime: number; 19 | options?: any; 20 | group?: MatchmakingGroup; 21 | rank: number; 22 | confirmed?: boolean; 23 | } 24 | 25 | export class MatchMakerRoom extends Room { 26 | allowUnmatchedGroups = true; 27 | 28 | evaluateGroupInterval = 2000; 29 | 30 | groups: MatchmakingGroup[] = []; 31 | 32 | roomToCreate = 'GameRoom'; 33 | 34 | maxWaitingTime: number = 60 * 1000; 35 | 36 | maxWaitingTimeForPriority?: number = 30 * 1000; 37 | 38 | numClientsToMatch = 4; 39 | 40 | stats: ClientStat[] = []; 41 | 42 | clientShipHash: any = {}; 43 | 44 | onCreate(options: any): void { 45 | 46 | this.setState(new ShipBuilderState()); 47 | 48 | if(options.maxWaitingTime) { 49 | this.maxWaitingTime = options.maxWaitingTime; 50 | } 51 | 52 | if(options.numClientsToMatch) { 53 | this.numClientsToMatch = options.numClientsToMatch; 54 | } 55 | 56 | this.setSimulationInterval(() => this.redistributeGroups(), this.evaluateGroupInterval); 57 | } 58 | 59 | async onAuth(client, options): Promise { 60 | const isValidToken = await JWTHelper.verifyToken(options.token); 61 | 62 | if(!isValidToken) { 63 | this.send(client, { 64 | error: 'error_invalid_token' 65 | }); 66 | return false; 67 | } 68 | 69 | const username = JWTHelper.extractUsernameFromToken(options.token); 70 | 71 | return username; 72 | } 73 | 74 | async onJoin(client: Client, options: any, username: string): Promise { 75 | console.log('[MatchMakerRoom] (onJoin)', username, "rank:", options.rank); 76 | this.stats.push({ 77 | client: client, 78 | rank: options.rank, 79 | waitingTime: 0, 80 | options 81 | }); 82 | 83 | const ship = await ShipHelper.getShipInGame(username); 84 | if(!ship) { 85 | this.send(client, { error: 'no_ship_in_game'}); 86 | return; 87 | } 88 | this.clientShipHash[client.id] = ship; 89 | 90 | this.send(client, 1); 91 | } 92 | 93 | onMessage(client: Client, message: any): void{ 94 | if(message === 1) { 95 | const stat = this.stats.find(stat => stat.client === client); 96 | 97 | if(stat && stat.group && typeof(stat.group.confirmed) === "number") { 98 | stat.confirmed = true; 99 | stat.group.confirmed++; 100 | 101 | if(stat.group.confirmed === stat.group.clients.length) { 102 | stat.group.clients.forEach(client => client.client.close()); 103 | } 104 | } 105 | } 106 | } 107 | 108 | createGroup(): MatchmakingGroup { 109 | const group: MatchmakingGroup = { clients: [], averageRank: 0}; 110 | this.groups.push(group); 111 | return group; 112 | } 113 | 114 | redistributeGroups(): void { 115 | this.groups = []; 116 | 117 | const stats = this.stats.sort((a, b) => a.rank - b.rank); 118 | 119 | let currentGroup: MatchmakingGroup = this.createGroup(); 120 | let totalRank = 0; 121 | 122 | for(let i = 0, l = stats.length; i < l; i++) { 123 | const stat = stats[i]; 124 | stat.waitingTime += this.clock.deltaTime; 125 | 126 | if (stat.group && stat.group.ready) { 127 | continue; 128 | } 129 | 130 | if(currentGroup.clients.length === this.numClientsToMatch) { 131 | currentGroup = this.createGroup(); 132 | totalRank = 0; 133 | } 134 | 135 | if( stat.waitingTime >= this.maxWaitingTime && this.allowUnmatchedGroups) { 136 | currentGroup.ready = true; 137 | } else if ( 138 | this.maxWaitingTimeForPriority !== undefined && 139 | stat.waitingTime >= this.maxWaitingTimeForPriority 140 | ) { 141 | currentGroup.priority = true; 142 | } 143 | 144 | if( 145 | currentGroup.averageRank > 0 && 146 | !currentGroup.priority 147 | ) { 148 | const diff = Math.abs(stat.rank - currentGroup.averageRank); 149 | const diffRatio = (diff / currentGroup.averageRank); 150 | 151 | if(diffRatio > 2) { 152 | currentGroup = this.createGroup(); 153 | totalRank = 0; 154 | } 155 | } 156 | 157 | stat.group = currentGroup; 158 | currentGroup.clients.push(stat); 159 | 160 | totalRank += stat.rank; 161 | 162 | currentGroup.averageRank = totalRank / currentGroup.clients.length; 163 | } 164 | this.checkGroupsReady(); 165 | } 166 | 167 | async checkGroupsReady(): Promise { 168 | await Promise.all( 169 | this.groups 170 | .map(async (group) => { 171 | if(group.ready || group.clients.length === this.numClientsToMatch) { 172 | group.ready = true; 173 | group.confirmed = 0; 174 | 175 | let count = 0; 176 | let rankings = 0; 177 | for(const client of group.clients) { 178 | count += 1; 179 | rankings += client.rank; 180 | } 181 | 182 | const waveRank = Math.max(Math.round(rankings/count) - 5, 1); 183 | console.log("[MatchMakerRoom] Creating Room with Rank:", waveRank); 184 | const room = await matchMaker.createRoom(this.roomToCreate, {waveRank: waveRank}); //TODO: Set the waveRank to be the correct wave rank 185 | 186 | await Promise.all(group.clients.map(async (client) => { 187 | const matchData = await matchMaker.reserveSeatFor(room, client.options); 188 | this.send(client.client, matchData); 189 | })); 190 | } else { 191 | const shipList: ShipList = new ShipList(); 192 | for(let i = 0; i < group.clients.length; i++) { 193 | const ship = this.clientShipHash[group.clients[i].client.id]; 194 | shipList.ships[ship.uuid] = ship; 195 | } 196 | group.clients.forEach(client => this.send(client.client, shipList)); 197 | } 198 | }) 199 | ); 200 | } 201 | 202 | onLeave(client: Client, ): void { 203 | const index = this.stats.findIndex(stat => stat.client === client); 204 | this.stats.splice(index, 1); 205 | delete this.clientShipHash[client.id]; 206 | } 207 | } 208 | --------------------------------------------------------------------------------