├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .husky
└── pre-commit
├── README.md
├── docs
└── screenshot.png
├── eslint.config.js
├── index.html
├── package-lock.json
├── package.json
├── public
├── assets
│ ├── characters
│ │ ├── CommandoAttack1.png
│ │ ├── CommandoAttack2.png
│ │ ├── CommandoDamage1.png
│ │ ├── CommandoDamage2.png
│ │ ├── CommandoDeath1.png
│ │ ├── CommandoDeath2.png
│ │ ├── CommandoDeath3.png
│ │ ├── CommandoDeath4.png
│ │ ├── CommandoIdle.png
│ │ ├── CommandoWalk1.png
│ │ ├── CommandoWalk2.png
│ │ ├── CommandoWalk3.png
│ │ ├── CommandoWalk4.png
│ │ ├── FlyguyAttack1.png
│ │ ├── FlyguyAttack2.png
│ │ ├── FlyguyDamage1.png
│ │ ├── FlyguyDamage2.png
│ │ ├── FlyguyDeath1.png
│ │ ├── FlyguyDeath2.png
│ │ ├── FlyguyDeath3.png
│ │ ├── FlyguyDeath4.png
│ │ ├── FlyguyIdle.png
│ │ ├── FlyguyWalk1.png
│ │ ├── FlyguyWalk2.png
│ │ ├── FlyguyWalk3.png
│ │ ├── FlyguyWalk4.png
│ │ ├── SlayerAttack1.png
│ │ ├── SlayerAttack2.png
│ │ ├── SlayerDamage1.png
│ │ ├── SlayerDamage2.png
│ │ ├── SlayerDeath1.png
│ │ ├── SlayerDeath2.png
│ │ ├── SlayerDeath3.png
│ │ ├── SlayerDeath4.png
│ │ ├── SlayerIdle.png
│ │ ├── SlayerWalk1.png
│ │ ├── SlayerWalk2.png
│ │ ├── SlayerWalk3.png
│ │ ├── SlayerWalk4.png
│ │ ├── SoldierAttack1.png
│ │ ├── SoldierAttack2.png
│ │ ├── SoldierDamage1.png
│ │ ├── SoldierDamage2.png
│ │ ├── SoldierDeath1.png
│ │ ├── SoldierDeath2.png
│ │ ├── SoldierDeath3.png
│ │ ├── SoldierDeath4.png
│ │ ├── SoldierIdle.png
│ │ ├── SoldierWalk1.png
│ │ ├── SoldierWalk2.png
│ │ ├── SoldierWalk3.png
│ │ ├── SoldierWalk4.png
│ │ ├── TankAttack1.png
│ │ ├── TankAttack2.png
│ │ ├── TankDamage1.png
│ │ ├── TankDamage2.png
│ │ ├── TankDeath1.png
│ │ ├── TankDeath2.png
│ │ ├── TankDeath3.png
│ │ ├── TankDeath4.png
│ │ ├── TankIdle.png
│ │ ├── TankWalk1.png
│ │ ├── TankWalk2.png
│ │ ├── TankWalk3.png
│ │ ├── TankWalk4.png
│ │ ├── ZombieAttack1.png
│ │ ├── ZombieAttack2.png
│ │ ├── ZombieDamage1.png
│ │ ├── ZombieDamage2.png
│ │ ├── ZombieDeath1.png
│ │ ├── ZombieDeath2.png
│ │ ├── ZombieDeath3.png
│ │ ├── ZombieDeath4.png
│ │ ├── ZombieIdle.png
│ │ ├── ZombieWalk1.png
│ │ ├── ZombieWalk2.png
│ │ ├── ZombieWalk3.png
│ │ └── ZombieWalk4.png
│ ├── icons
│ │ ├── bullets.png
│ │ ├── health.png
│ │ └── timer.png
│ ├── items
│ │ ├── health_pack.png
│ │ ├── pistol_ammo.png
│ │ └── pistol_weapon.png
│ ├── music
│ │ ├── dead-lift-yeti.mp3
│ │ ├── heavy-duty-zoo.mp3
│ │ ├── on-the-edge-reakt.mp3
│ │ ├── scorcher-abbynoise.mp3
│ │ ├── shocking-red-abbynoise.mp3
│ │ └── zombie-world-alex-besss.mp3
│ ├── sounds
│ │ ├── attack-knife.mp3
│ │ ├── attack-zombie.mp3
│ │ ├── gun-shot.mp3
│ │ ├── hurt.mp3
│ │ ├── lazer-shot.mp3
│ │ └── pick.mp3
│ ├── textures
│ │ ├── BRICK_1A.PNG
│ │ ├── BRICK_2B.PNG
│ │ ├── BRICK_3B.PNG
│ │ ├── BRICK_3D.PNG
│ │ ├── BRICK_4A.PNG
│ │ ├── BRICK_6D.PNG
│ │ ├── CONCRETE_3C.PNG
│ │ ├── CONCRETE_4A.PNG
│ │ ├── CONSOLE_1B.PNG
│ │ ├── CRATE_1D.PNG
│ │ ├── CRATE_1E.PNG
│ │ ├── CRATE_2C.PNG
│ │ ├── CRATE_2M.PNG
│ │ ├── DIRT_1A.PNG
│ │ ├── DOORTRIM_1A.PNG
│ │ ├── DOOR_1A.PNG
│ │ ├── DOOR_1C.PNG
│ │ ├── DOOR_1E.PNG
│ │ ├── DOOR_4A.PNG
│ │ ├── FLOOR_1A.PNG
│ │ ├── FLOOR_3A.PNG
│ │ ├── FLOOR_4A.PNG
│ │ ├── GRASS_1A.PNG
│ │ ├── GRID_1A.PNG
│ │ ├── GRID_2B.PNG
│ │ ├── HEDGE_1A.PNG
│ │ ├── LAB_2B.PNG
│ │ ├── LIGHT_1B.PNG
│ │ ├── PAPER_1B.PNG
│ │ ├── PAPER_1E.PNG
│ │ ├── PAPER_1F.PNG
│ │ ├── PIPES_1A.PNG
│ │ ├── PIPES_2B.PNG
│ │ ├── RIVET_1A.PNG
│ │ ├── SAND_1A.PNG
│ │ ├── SLIME_1A.PNG
│ │ ├── SLUDGE_1A.PNG
│ │ ├── STEEL_1A.PNG
│ │ ├── SUPPORT_3A.PNG
│ │ ├── SUPPORT_4A.PNG
│ │ ├── TECH_1C.PNG
│ │ ├── TECH_1E.PNG
│ │ ├── TECH_2F.PNG
│ │ ├── TECH_3B.PNG
│ │ ├── TECH_4E.PNG
│ │ ├── TECH_4F.PNG
│ │ ├── TILE_1A.PNG
│ │ ├── TILE_2C.PNG
│ │ ├── WARN_1A.PNG
│ │ └── WOOD_1C.PNG
│ └── weapons
│ │ ├── knife_1.png
│ │ ├── knife_2.png
│ │ ├── knife_3.png
│ │ ├── knife_4.png
│ │ ├── knife_5.png
│ │ ├── pistol_1.png
│ │ ├── pistol_2.png
│ │ ├── pistol_3.png
│ │ ├── pistol_4.png
│ │ ├── pistol_5.png
│ │ ├── pistol_bullet.png
│ │ └── shotgun_bullet.gif
└── maze.svg
├── src
├── global.d.ts
├── levels
│ ├── generators
│ │ ├── characters.ts
│ │ ├── components.ts
│ │ └── items.ts
│ ├── index.ts
│ ├── level_1.ts
│ ├── level_2.ts
│ ├── level_3.ts
│ └── level_final.ts
├── lib
│ ├── Canvas
│ │ ├── BufferCanvas.ts
│ │ ├── DefaultCanvas.ts
│ │ ├── WebglCanvas.ts
│ │ └── lib
│ │ │ └── webgl.ts
│ ├── ecs
│ │ ├── Component.ts
│ │ ├── Entity.ts
│ │ ├── System.ts
│ │ ├── components
│ │ │ ├── AIComponent.ts
│ │ │ ├── AngleComponent.ts
│ │ │ ├── AnimatedSpriteComponent.ts
│ │ │ ├── BoxComponent.ts
│ │ │ ├── BulletComponent.ts
│ │ │ ├── CameraComponent.ts
│ │ │ ├── CircleComponent.ts
│ │ │ ├── CollisionComponent.ts
│ │ │ ├── ControlComponent.ts
│ │ │ ├── DoorComponent.ts
│ │ │ ├── EnemyComponent.ts
│ │ │ ├── HealthComponent.ts
│ │ │ ├── HighlightComponent.ts
│ │ │ ├── ItemComponent.ts
│ │ │ ├── LightComponent.ts
│ │ │ ├── MinimapComponent.ts
│ │ │ ├── MoveComponent.ts
│ │ │ ├── PlayerComponent.ts
│ │ │ ├── PositionComponent.ts
│ │ │ ├── RotateComponent.ts
│ │ │ ├── SpriteComponent.ts
│ │ │ ├── TextureComponent.ts
│ │ │ ├── WeaponComponent.ts
│ │ │ ├── WeaponMeleeComponent.ts
│ │ │ └── WeaponRangeComponent.ts
│ │ ├── index.ts
│ │ ├── lib
│ │ │ ├── PolarMap.ts
│ │ │ ├── PositionMap.ts
│ │ │ └── ScaledMap.ts
│ │ └── systems
│ │ │ ├── AISystem.ts
│ │ │ ├── AnimationSystem.ts
│ │ │ ├── ControlSystem.ts
│ │ │ ├── DoorsSystem.ts
│ │ │ ├── LightSystem
│ │ │ ├── LightCasting2D.ts
│ │ │ └── index.ts
│ │ │ ├── MapItemSystem.ts
│ │ │ ├── MapPolarSystem.ts
│ │ │ ├── MapTextureSystem.ts
│ │ │ ├── MinimapSystem.ts
│ │ │ ├── MoveSystem.ts
│ │ │ ├── RenderSystem
│ │ │ ├── EntityRenders
│ │ │ │ ├── DoorRender.ts
│ │ │ │ ├── IEntityRender.ts
│ │ │ │ └── WallRender.ts
│ │ │ └── index.ts
│ │ │ ├── RotateSystem.ts
│ │ │ └── WeaponSystem.ts
│ ├── image.ts
│ ├── loop.ts
│ ├── scenario.ts
│ └── utils
│ │ ├── angle.ts
│ │ ├── color.ts
│ │ ├── geometry.ts
│ │ └── math.ts
├── main.ts
├── managers
│ ├── AnimationManager.ts
│ ├── SoundManager.ts
│ └── TextureManager.ts
├── presets.ts
├── scenario.ts
├── scenes
│ ├── BaseScene.ts
│ ├── LevelScene.ts
│ └── TitleScene.ts
├── views
│ ├── ContentView.ts
│ └── LevelPlayerView.ts
└── vite-env.d.ts
├── tsconfig.json
└── vite.config.mts
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Deploy project
2 |
3 | on:
4 | push:
5 | branches: ['main']
6 |
7 | permissions:
8 | contents: read
9 | pages: write
10 | id-token: write
11 |
12 | concurrency:
13 | group: 'pages'
14 | cancel-in-progress: true
15 |
16 | jobs:
17 | deploy:
18 | environment:
19 | name: github-pages
20 | url: ${{ steps.deployment.outputs.page_url }}
21 | runs-on: ubuntu-latest
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v3
25 |
26 | - name: Set up Node
27 | uses: actions/setup-node@v3
28 | with:
29 | node-version: 18
30 | cache: 'npm'
31 |
32 | - name: Install dependencies
33 | run: npm install
34 |
35 | - name: Build
36 | run: npm run build
37 |
38 | - name: Setup Pages
39 | uses: actions/configure-pages@v3
40 |
41 | - name: Upload artifact
42 | uses: actions/upload-pages-artifact@v1
43 | with:
44 | path: './dist'
45 |
46 | - name: Deploy to GitHub Pages
47 | id: deployment
48 | uses: actions/deploy-pages@v1
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | .gitconfig
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npm run lint
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Browser First Person Shooter v0.3.0
2 | ===================================
3 |
4 | This is a [wolfenstein](https://en.wikipedia.org/wiki/Wolfenstein)-like single player game. It uses old approaches from the 1990s, raycasting algorithm. All the graphics calculations take place on the CPU, so the game has performance limitations, but it's enough for playing.
5 |
6 | The game has no dependencies; it was built from scratch with TypeScript.
7 |
8 | 
9 |
10 | ## Features
11 |
12 | - Primitive AI
13 | - Weapons: knife, pistol
14 | - Items: pistol ammo, heath pack
15 | - Doors openning
16 |
17 | ## Assets
18 |
19 | - Textures, [retro texture pack](https://little-martian.itch.io/retro-texture-pack)
20 | - Characters:
21 | - zombie (by [Semyon Yushkevich](https://github.com/jussiemion))
22 | - [soldier, commando, slayer, flyguy, tank](https://fredrichi.itch.io/free-characters-with-animations-for-fps-game) (by [FredRichi](https://fredrichi.itch.io/))
23 | - Weapon: knife, pistol (by [Semyon Yushkevich](https://github.com/jussiemion))
24 |
--------------------------------------------------------------------------------
/docs/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/docs/screenshot.png
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import globals from "globals";
2 | import pluginJs from "@eslint/js";
3 | import tseslint from "typescript-eslint";
4 |
5 | export default [
6 | {
7 | languageOptions: {
8 | globals: globals.browser
9 | }
10 | },
11 | pluginJs.configs.recommended,
12 | ...tseslint.configs.recommended,
13 | {
14 | rules: {
15 | "@typescript-eslint/no-explicit-any": "off",
16 | "@typescript-eslint/ban-types": "off",
17 | "@typescript-eslint/no-unsafe-function-type": "off",
18 | "@typescript-eslint/no-empty-object-type": "off"
19 | },
20 | }
21 | ];
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Shoot or run
8 |
48 |
49 |
50 | Loading..
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fsp-ts",
3 | "private": true,
4 | "version": "0.1.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "preview": "vite preview",
10 | "lint": "eslint ./src",
11 | "prepare": "husky"
12 | },
13 | "devDependencies": {
14 | "@eslint/js": "^9.12.0",
15 | "eslint": "^9.12.0",
16 | "globals": "^16.1.0",
17 | "husky": "^9.1.6",
18 | "prettier": "^3.4.2",
19 | "typescript": "^5.6.3",
20 | "typescript-eslint": "^8.8.1",
21 | "vite": "^6.3.5",
22 | "vite-plugin-eslint": "^1.8.1"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/public/assets/characters/CommandoAttack1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/CommandoAttack1.png
--------------------------------------------------------------------------------
/public/assets/characters/CommandoAttack2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/CommandoAttack2.png
--------------------------------------------------------------------------------
/public/assets/characters/CommandoDamage1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/CommandoDamage1.png
--------------------------------------------------------------------------------
/public/assets/characters/CommandoDamage2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/CommandoDamage2.png
--------------------------------------------------------------------------------
/public/assets/characters/CommandoDeath1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/CommandoDeath1.png
--------------------------------------------------------------------------------
/public/assets/characters/CommandoDeath2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/CommandoDeath2.png
--------------------------------------------------------------------------------
/public/assets/characters/CommandoDeath3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/CommandoDeath3.png
--------------------------------------------------------------------------------
/public/assets/characters/CommandoDeath4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/CommandoDeath4.png
--------------------------------------------------------------------------------
/public/assets/characters/CommandoIdle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/CommandoIdle.png
--------------------------------------------------------------------------------
/public/assets/characters/CommandoWalk1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/CommandoWalk1.png
--------------------------------------------------------------------------------
/public/assets/characters/CommandoWalk2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/CommandoWalk2.png
--------------------------------------------------------------------------------
/public/assets/characters/CommandoWalk3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/CommandoWalk3.png
--------------------------------------------------------------------------------
/public/assets/characters/CommandoWalk4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/CommandoWalk4.png
--------------------------------------------------------------------------------
/public/assets/characters/FlyguyAttack1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/FlyguyAttack1.png
--------------------------------------------------------------------------------
/public/assets/characters/FlyguyAttack2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/FlyguyAttack2.png
--------------------------------------------------------------------------------
/public/assets/characters/FlyguyDamage1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/FlyguyDamage1.png
--------------------------------------------------------------------------------
/public/assets/characters/FlyguyDamage2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/FlyguyDamage2.png
--------------------------------------------------------------------------------
/public/assets/characters/FlyguyDeath1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/FlyguyDeath1.png
--------------------------------------------------------------------------------
/public/assets/characters/FlyguyDeath2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/FlyguyDeath2.png
--------------------------------------------------------------------------------
/public/assets/characters/FlyguyDeath3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/FlyguyDeath3.png
--------------------------------------------------------------------------------
/public/assets/characters/FlyguyDeath4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/FlyguyDeath4.png
--------------------------------------------------------------------------------
/public/assets/characters/FlyguyIdle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/FlyguyIdle.png
--------------------------------------------------------------------------------
/public/assets/characters/FlyguyWalk1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/FlyguyWalk1.png
--------------------------------------------------------------------------------
/public/assets/characters/FlyguyWalk2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/FlyguyWalk2.png
--------------------------------------------------------------------------------
/public/assets/characters/FlyguyWalk3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/FlyguyWalk3.png
--------------------------------------------------------------------------------
/public/assets/characters/FlyguyWalk4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/FlyguyWalk4.png
--------------------------------------------------------------------------------
/public/assets/characters/SlayerAttack1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SlayerAttack1.png
--------------------------------------------------------------------------------
/public/assets/characters/SlayerAttack2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SlayerAttack2.png
--------------------------------------------------------------------------------
/public/assets/characters/SlayerDamage1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SlayerDamage1.png
--------------------------------------------------------------------------------
/public/assets/characters/SlayerDamage2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SlayerDamage2.png
--------------------------------------------------------------------------------
/public/assets/characters/SlayerDeath1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SlayerDeath1.png
--------------------------------------------------------------------------------
/public/assets/characters/SlayerDeath2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SlayerDeath2.png
--------------------------------------------------------------------------------
/public/assets/characters/SlayerDeath3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SlayerDeath3.png
--------------------------------------------------------------------------------
/public/assets/characters/SlayerDeath4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SlayerDeath4.png
--------------------------------------------------------------------------------
/public/assets/characters/SlayerIdle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SlayerIdle.png
--------------------------------------------------------------------------------
/public/assets/characters/SlayerWalk1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SlayerWalk1.png
--------------------------------------------------------------------------------
/public/assets/characters/SlayerWalk2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SlayerWalk2.png
--------------------------------------------------------------------------------
/public/assets/characters/SlayerWalk3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SlayerWalk3.png
--------------------------------------------------------------------------------
/public/assets/characters/SlayerWalk4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SlayerWalk4.png
--------------------------------------------------------------------------------
/public/assets/characters/SoldierAttack1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SoldierAttack1.png
--------------------------------------------------------------------------------
/public/assets/characters/SoldierAttack2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SoldierAttack2.png
--------------------------------------------------------------------------------
/public/assets/characters/SoldierDamage1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SoldierDamage1.png
--------------------------------------------------------------------------------
/public/assets/characters/SoldierDamage2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SoldierDamage2.png
--------------------------------------------------------------------------------
/public/assets/characters/SoldierDeath1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SoldierDeath1.png
--------------------------------------------------------------------------------
/public/assets/characters/SoldierDeath2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SoldierDeath2.png
--------------------------------------------------------------------------------
/public/assets/characters/SoldierDeath3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SoldierDeath3.png
--------------------------------------------------------------------------------
/public/assets/characters/SoldierDeath4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SoldierDeath4.png
--------------------------------------------------------------------------------
/public/assets/characters/SoldierIdle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SoldierIdle.png
--------------------------------------------------------------------------------
/public/assets/characters/SoldierWalk1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SoldierWalk1.png
--------------------------------------------------------------------------------
/public/assets/characters/SoldierWalk2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SoldierWalk2.png
--------------------------------------------------------------------------------
/public/assets/characters/SoldierWalk3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SoldierWalk3.png
--------------------------------------------------------------------------------
/public/assets/characters/SoldierWalk4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/SoldierWalk4.png
--------------------------------------------------------------------------------
/public/assets/characters/TankAttack1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/TankAttack1.png
--------------------------------------------------------------------------------
/public/assets/characters/TankAttack2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/TankAttack2.png
--------------------------------------------------------------------------------
/public/assets/characters/TankDamage1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/TankDamage1.png
--------------------------------------------------------------------------------
/public/assets/characters/TankDamage2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/TankDamage2.png
--------------------------------------------------------------------------------
/public/assets/characters/TankDeath1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/TankDeath1.png
--------------------------------------------------------------------------------
/public/assets/characters/TankDeath2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/TankDeath2.png
--------------------------------------------------------------------------------
/public/assets/characters/TankDeath3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/TankDeath3.png
--------------------------------------------------------------------------------
/public/assets/characters/TankDeath4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/TankDeath4.png
--------------------------------------------------------------------------------
/public/assets/characters/TankIdle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/TankIdle.png
--------------------------------------------------------------------------------
/public/assets/characters/TankWalk1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/TankWalk1.png
--------------------------------------------------------------------------------
/public/assets/characters/TankWalk2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/TankWalk2.png
--------------------------------------------------------------------------------
/public/assets/characters/TankWalk3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/TankWalk3.png
--------------------------------------------------------------------------------
/public/assets/characters/TankWalk4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/TankWalk4.png
--------------------------------------------------------------------------------
/public/assets/characters/ZombieAttack1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/ZombieAttack1.png
--------------------------------------------------------------------------------
/public/assets/characters/ZombieAttack2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/ZombieAttack2.png
--------------------------------------------------------------------------------
/public/assets/characters/ZombieDamage1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/ZombieDamage1.png
--------------------------------------------------------------------------------
/public/assets/characters/ZombieDamage2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/ZombieDamage2.png
--------------------------------------------------------------------------------
/public/assets/characters/ZombieDeath1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/ZombieDeath1.png
--------------------------------------------------------------------------------
/public/assets/characters/ZombieDeath2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/ZombieDeath2.png
--------------------------------------------------------------------------------
/public/assets/characters/ZombieDeath3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/ZombieDeath3.png
--------------------------------------------------------------------------------
/public/assets/characters/ZombieDeath4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/ZombieDeath4.png
--------------------------------------------------------------------------------
/public/assets/characters/ZombieIdle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/ZombieIdle.png
--------------------------------------------------------------------------------
/public/assets/characters/ZombieWalk1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/ZombieWalk1.png
--------------------------------------------------------------------------------
/public/assets/characters/ZombieWalk2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/ZombieWalk2.png
--------------------------------------------------------------------------------
/public/assets/characters/ZombieWalk3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/ZombieWalk3.png
--------------------------------------------------------------------------------
/public/assets/characters/ZombieWalk4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/characters/ZombieWalk4.png
--------------------------------------------------------------------------------
/public/assets/icons/bullets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/icons/bullets.png
--------------------------------------------------------------------------------
/public/assets/icons/health.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/icons/health.png
--------------------------------------------------------------------------------
/public/assets/icons/timer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/icons/timer.png
--------------------------------------------------------------------------------
/public/assets/items/health_pack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/items/health_pack.png
--------------------------------------------------------------------------------
/public/assets/items/pistol_ammo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/items/pistol_ammo.png
--------------------------------------------------------------------------------
/public/assets/items/pistol_weapon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/items/pistol_weapon.png
--------------------------------------------------------------------------------
/public/assets/music/dead-lift-yeti.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/music/dead-lift-yeti.mp3
--------------------------------------------------------------------------------
/public/assets/music/heavy-duty-zoo.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/music/heavy-duty-zoo.mp3
--------------------------------------------------------------------------------
/public/assets/music/on-the-edge-reakt.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/music/on-the-edge-reakt.mp3
--------------------------------------------------------------------------------
/public/assets/music/scorcher-abbynoise.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/music/scorcher-abbynoise.mp3
--------------------------------------------------------------------------------
/public/assets/music/shocking-red-abbynoise.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/music/shocking-red-abbynoise.mp3
--------------------------------------------------------------------------------
/public/assets/music/zombie-world-alex-besss.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/music/zombie-world-alex-besss.mp3
--------------------------------------------------------------------------------
/public/assets/sounds/attack-knife.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/sounds/attack-knife.mp3
--------------------------------------------------------------------------------
/public/assets/sounds/attack-zombie.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/sounds/attack-zombie.mp3
--------------------------------------------------------------------------------
/public/assets/sounds/gun-shot.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/sounds/gun-shot.mp3
--------------------------------------------------------------------------------
/public/assets/sounds/hurt.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/sounds/hurt.mp3
--------------------------------------------------------------------------------
/public/assets/sounds/lazer-shot.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/sounds/lazer-shot.mp3
--------------------------------------------------------------------------------
/public/assets/sounds/pick.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/sounds/pick.mp3
--------------------------------------------------------------------------------
/public/assets/textures/BRICK_1A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/BRICK_1A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/BRICK_2B.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/BRICK_2B.PNG
--------------------------------------------------------------------------------
/public/assets/textures/BRICK_3B.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/BRICK_3B.PNG
--------------------------------------------------------------------------------
/public/assets/textures/BRICK_3D.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/BRICK_3D.PNG
--------------------------------------------------------------------------------
/public/assets/textures/BRICK_4A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/BRICK_4A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/BRICK_6D.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/BRICK_6D.PNG
--------------------------------------------------------------------------------
/public/assets/textures/CONCRETE_3C.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/CONCRETE_3C.PNG
--------------------------------------------------------------------------------
/public/assets/textures/CONCRETE_4A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/CONCRETE_4A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/CONSOLE_1B.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/CONSOLE_1B.PNG
--------------------------------------------------------------------------------
/public/assets/textures/CRATE_1D.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/CRATE_1D.PNG
--------------------------------------------------------------------------------
/public/assets/textures/CRATE_1E.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/CRATE_1E.PNG
--------------------------------------------------------------------------------
/public/assets/textures/CRATE_2C.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/CRATE_2C.PNG
--------------------------------------------------------------------------------
/public/assets/textures/CRATE_2M.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/CRATE_2M.PNG
--------------------------------------------------------------------------------
/public/assets/textures/DIRT_1A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/DIRT_1A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/DOORTRIM_1A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/DOORTRIM_1A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/DOOR_1A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/DOOR_1A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/DOOR_1C.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/DOOR_1C.PNG
--------------------------------------------------------------------------------
/public/assets/textures/DOOR_1E.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/DOOR_1E.PNG
--------------------------------------------------------------------------------
/public/assets/textures/DOOR_4A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/DOOR_4A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/FLOOR_1A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/FLOOR_1A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/FLOOR_3A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/FLOOR_3A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/FLOOR_4A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/FLOOR_4A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/GRASS_1A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/GRASS_1A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/GRID_1A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/GRID_1A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/GRID_2B.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/GRID_2B.PNG
--------------------------------------------------------------------------------
/public/assets/textures/HEDGE_1A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/HEDGE_1A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/LAB_2B.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/LAB_2B.PNG
--------------------------------------------------------------------------------
/public/assets/textures/LIGHT_1B.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/LIGHT_1B.PNG
--------------------------------------------------------------------------------
/public/assets/textures/PAPER_1B.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/PAPER_1B.PNG
--------------------------------------------------------------------------------
/public/assets/textures/PAPER_1E.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/PAPER_1E.PNG
--------------------------------------------------------------------------------
/public/assets/textures/PAPER_1F.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/PAPER_1F.PNG
--------------------------------------------------------------------------------
/public/assets/textures/PIPES_1A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/PIPES_1A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/PIPES_2B.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/PIPES_2B.PNG
--------------------------------------------------------------------------------
/public/assets/textures/RIVET_1A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/RIVET_1A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/SAND_1A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/SAND_1A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/SLIME_1A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/SLIME_1A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/SLUDGE_1A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/SLUDGE_1A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/STEEL_1A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/STEEL_1A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/SUPPORT_3A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/SUPPORT_3A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/SUPPORT_4A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/SUPPORT_4A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/TECH_1C.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/TECH_1C.PNG
--------------------------------------------------------------------------------
/public/assets/textures/TECH_1E.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/TECH_1E.PNG
--------------------------------------------------------------------------------
/public/assets/textures/TECH_2F.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/TECH_2F.PNG
--------------------------------------------------------------------------------
/public/assets/textures/TECH_3B.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/TECH_3B.PNG
--------------------------------------------------------------------------------
/public/assets/textures/TECH_4E.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/TECH_4E.PNG
--------------------------------------------------------------------------------
/public/assets/textures/TECH_4F.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/TECH_4F.PNG
--------------------------------------------------------------------------------
/public/assets/textures/TILE_1A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/TILE_1A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/TILE_2C.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/TILE_2C.PNG
--------------------------------------------------------------------------------
/public/assets/textures/WARN_1A.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/WARN_1A.PNG
--------------------------------------------------------------------------------
/public/assets/textures/WOOD_1C.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/textures/WOOD_1C.PNG
--------------------------------------------------------------------------------
/public/assets/weapons/knife_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/weapons/knife_1.png
--------------------------------------------------------------------------------
/public/assets/weapons/knife_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/weapons/knife_2.png
--------------------------------------------------------------------------------
/public/assets/weapons/knife_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/weapons/knife_3.png
--------------------------------------------------------------------------------
/public/assets/weapons/knife_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/weapons/knife_4.png
--------------------------------------------------------------------------------
/public/assets/weapons/knife_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/weapons/knife_5.png
--------------------------------------------------------------------------------
/public/assets/weapons/pistol_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/weapons/pistol_1.png
--------------------------------------------------------------------------------
/public/assets/weapons/pistol_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/weapons/pistol_2.png
--------------------------------------------------------------------------------
/public/assets/weapons/pistol_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/weapons/pistol_3.png
--------------------------------------------------------------------------------
/public/assets/weapons/pistol_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/weapons/pistol_4.png
--------------------------------------------------------------------------------
/public/assets/weapons/pistol_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/weapons/pistol_5.png
--------------------------------------------------------------------------------
/public/assets/weapons/pistol_bullet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/weapons/pistol_bullet.png
--------------------------------------------------------------------------------
/public/assets/weapons/shotgun_bullet.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ufocoder/fps/68c15f074e4d51803425c2e999eabe83db0ecb2c/public/assets/weapons/shotgun_bullet.gif
--------------------------------------------------------------------------------
/public/maze.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | interface Color {
2 | r: number;
3 | g: number;
4 | b: number;
5 | a: number;
6 | }
7 |
8 | type ItemType = 'health_pack' | 'pistol_weapon' | 'pistol_ammo'
9 |
10 | interface Item {
11 | x: number;
12 | y: number;
13 | type: ItemType;
14 | radius: number;
15 | value: number;
16 | }
17 |
18 | interface Character {
19 | x: number;
20 | y: number;
21 | angle: number;
22 | health: number;
23 | }
24 |
25 | interface Enemy extends Character {
26 | type: 'zombie' | 'flyguy' | 'soldier' | 'commando' | 'tank' | 'slayer';
27 | sprite: string;
28 | radius: number;
29 | aiDistance?: number;
30 | meleeWeapon?: {
31 | damage: number;
32 | frequency: number;
33 | }
34 | rangeWeapon?: {
35 | bulletSprite: string;
36 | bulletTotal: number;
37 | bulletSpeed: number;
38 | bulletDamage: number;
39 | attackDistance: number;
40 | attackFrequency: number;
41 | }
42 | }
43 |
44 | type LevelMap = (number | string)[][];
45 |
46 | type MapEntityEmpty = {
47 | type: 'empty';
48 | }
49 |
50 | type MapEntityWall = {
51 | type: 'wall',
52 | texture: string
53 | }
54 |
55 | type MapEntityDoor = {
56 | type: 'door',
57 | texture: string
58 | }
59 | type MapEntityLight = {
60 | type: 'light',
61 | }
62 |
63 | type MapEntity = MapEntityEmpty | MapEntityWall | MapEntityDoor | MapEntityLight;
64 |
65 | interface ExitEndingScenario {
66 | name: 'exit';
67 | position: {
68 | x: number;
69 | y: number;
70 | }
71 | }
72 |
73 | interface EnemyEndingScenario {
74 | name: 'enemy';
75 | }
76 |
77 | interface TimerEndingScenario {
78 | name: 'survive';
79 | timer: number;
80 | }
81 |
82 | interface Level {
83 | map: LevelMap;
84 | mapEntities: Record;
85 | player: Character;
86 | music?: string;
87 | enemies?: Enemy[];
88 | items?: Item[];
89 | world: {
90 | colors: {
91 | top: Color;
92 | bottom: Color;
93 | }
94 | }
95 | endingScenario: ExitEndingScenario | EnemyEndingScenario | TimerEndingScenario;
96 | }
97 |
98 | interface TexturePreset {
99 | id: string;
100 | url: string;
101 | }
102 |
103 | interface SpritePreset {
104 | id: string;
105 | url: string;
106 | }
107 |
108 | interface SoundPreset {
109 | id: string;
110 | url: string;
111 | volume?: number;
112 | }
113 |
114 | interface AnimationSpritePreset {
115 | id: string;
116 | frames: string[]
117 | }
118 |
119 | interface TextureBitmap {
120 | width: number;
121 | height: number;
122 | colors: Color[][];
123 | }
124 |
125 | interface Color {
126 | r: number;
127 | g: number;
128 | b: number;
129 | a: number;
130 | }
131 |
132 | type Sprite = Texture
133 |
134 | type Vector2D = {
135 | x: number;
136 | y: number;
137 | }
138 |
139 | type PlayerState = {
140 | ammo?: number;
141 | health: number;
142 | }
143 |
144 |
--------------------------------------------------------------------------------
/src/levels/generators/characters.ts:
--------------------------------------------------------------------------------
1 | import { degreeToRadians } from "src/lib/utils/angle";
2 |
3 | const random = (from: number, to: number) => {
4 | return from + Math.random() * (to - from);
5 | };
6 |
7 | export const generateEntities =
8 | (generator: (x: number, y: number, ai: number) => T) =>
9 | (
10 | limit: number,
11 | x: number,
12 | y: number,
13 | dx: number,
14 | dy: number,
15 | ai: number = 0,
16 | ) => {
17 | return new Array(limit)
18 | .fill(0)
19 | .map(() => generator(random(x - dx, x + dx), random(y - dy, y + dy), ai));
20 | };
21 |
22 | export const generateCircle = (
23 | x: number,
24 | y: number,
25 | radius: number,
26 | total: number,
27 | ): number[][] => {
28 | const step = 360 / total;
29 | const coords = [];
30 |
31 | for (let angle = 0; angle < 360; angle += step) {
32 | coords.push([
33 | x + radius * Math.cos(degreeToRadians(angle)),
34 | y + radius * Math.sin(degreeToRadians(angle)),
35 | ]);
36 | }
37 |
38 | return coords;
39 | };
40 |
41 | export const generateZombie = (x: number, y: number, aiDistance: number = 0) =>
42 | ({
43 | x,
44 | y,
45 | aiDistance,
46 | type: "zombie",
47 | health: 100,
48 | radius: 0.4,
49 | meleeWeapon: {
50 | damage: 5,
51 | frequency: 1_000,
52 | },
53 | }) as Enemy;
54 |
55 | export const generateFlyguy = (x: number, y: number, aiDistance: number = 0) =>
56 | ({
57 | x,
58 | y,
59 | aiDistance,
60 | type: "flyguy",
61 | health: 150,
62 | radius: 0.4,
63 | rangeWeapon: {
64 | bulletSprite: "pistol_bullet",
65 | bulletDamage: 5,
66 | bulletSpeed: 8,
67 | attackDistance: 2,
68 | attackFrequency: 1_000,
69 | },
70 | }) as Enemy;
71 |
72 | export const generateSoldier = (x: number, y: number, aiDistance: number = 0) =>
73 | ({
74 | x,
75 | y,
76 | aiDistance,
77 | type: "soldier",
78 | health: 200,
79 | radius: 0.4,
80 | rangeWeapon: {
81 | bulletSprite: "shotgun_bullet",
82 | bulletDamage: 10,
83 | bulletSpeed: 6,
84 | attackDistance: 2,
85 | attackFrequency: 1_500,
86 | },
87 | }) as Enemy;
88 |
89 | export const generateCommando = (
90 | x: number,
91 | y: number,
92 | aiDistance: number = 0,
93 | ) =>
94 | ({
95 | x,
96 | y,
97 | aiDistance,
98 | type: "commando",
99 | health: 500,
100 | radius: 0.6,
101 | rangeWeapon: {
102 | bulletSprite: "shotgun_bullet",
103 | bulletDamage: 15,
104 | bulletSpeed: 7,
105 | attackDistance: 3,
106 | attackFrequency: 1_500,
107 | },
108 | }) as Enemy;
109 |
110 | export const generateTank = (x: number, y: number, aiDistance: number = 0) =>
111 | ({
112 | x,
113 | y,
114 | aiDistance,
115 | type: "tank",
116 | health: 2000,
117 | radius: 0.4,
118 | rangeWeapon: {
119 | bulletSprite: "shotgun_bullet",
120 | bulletDamage: 25,
121 | bulletSpeed: 5,
122 | attackDistance: 3,
123 | attackFrequency: 750,
124 | },
125 | }) as Enemy;
126 |
127 | export const generateZombies = generateEntities(generateZombie);
128 | export const generateSoldiers = generateEntities(generateSoldier);
129 | export const generateCommandos = generateEntities(generateCommando);
130 | export const generateFlygies = generateEntities(generateFlyguy);
131 | export const generateTanks = generateEntities(generateTank);
132 |
--------------------------------------------------------------------------------
/src/levels/generators/components.ts:
--------------------------------------------------------------------------------
1 | import AnimatedSpriteComponent from "src/lib/ecs/components/AnimatedSpriteComponent";
2 | import WeaponMeleeComponent from "src/lib/ecs/components/WeaponMeleeComponent";
3 | import WeaponRangeComponent from "src/lib/ecs/components/WeaponRangeComponent";
4 | import AnimationManager from "src/managers/AnimationManager";
5 |
6 | export const generateKnifeWeapon = (animationManager: AnimationManager) => new WeaponMeleeComponent({
7 | sprite: new AnimatedSpriteComponent('idle', {
8 | attack: animationManager.get("knifeAttack"),
9 | idle: animationManager.get("knifeIdle"),
10 | }),
11 | attackDamage: 30,
12 | attackFrequency: 500,
13 | });
14 |
15 | export const generatePistolWeapon = (animationManager: AnimationManager, bulletTotal: number = 30) => new WeaponRangeComponent({
16 | bulletTotal,
17 | bulletSprite: "pistol_bullet",
18 | bulletDamage: 100,
19 | bulletSpeed: 35,
20 | attackDistance: 15,
21 | attackFrequency: 500,
22 | sprite: new AnimatedSpriteComponent("idle", {
23 | attack: animationManager.get("pistolAttack"),
24 | idle: animationManager.get("pistolIdle"),
25 | }),
26 | });
--------------------------------------------------------------------------------
/src/levels/generators/items.ts:
--------------------------------------------------------------------------------
1 | export const generatePistol = (x: number, y: number, value: number) =>
2 | ({
3 | type: "pistol_weapon",
4 | radius: 0.3,
5 | x,
6 | y,
7 | value,
8 | }) as Item;
9 |
10 | export const generatePistolAmmo = (x: number, y: number, value: number) =>
11 | ({
12 | type: "pistol_ammo",
13 | radius: 0.3,
14 | x,
15 | y,
16 | value,
17 | }) as Item;
18 |
19 | export const generateHealthPack = (x: number, y: number, value: number) =>
20 | ({
21 | type: "health_pack",
22 | radius: 0.3,
23 | x,
24 | y,
25 | value,
26 | }) as Item;
27 |
--------------------------------------------------------------------------------
/src/levels/index.ts:
--------------------------------------------------------------------------------
1 | import level_1 from "./level_1";
2 | import level_2 from "./level_2";
3 | import level_final from "./level_final";
4 |
5 | // prettier-ignore
6 | export default [
7 | level_1,
8 | level_2,
9 | level_final,
10 |
11 | ];
12 |
--------------------------------------------------------------------------------
/src/levels/level_1.ts:
--------------------------------------------------------------------------------
1 | import { generateSoldier, generateZombies } from "./generators/characters";
2 | import { generatePistolAmmo, generatePistol } from "./generators/items";
3 |
4 | const level: Level = {
5 | world: {
6 | colors: {
7 | top: { r: 0, g: 0, b: 0, a: 255 },
8 | bottom: { r: 84, g: 98, b: 92, a: 255 },
9 | },
10 | },
11 | music: "shocking-red-abbynoise",
12 | // prettier-ignore
13 | map: [
14 | ['#', '#', '#', '#', '&', '#', '#', '#', '#', '#', '#', '&', '#', '#', '#', '#', '#', '#', '#', '#'],
15 | ['#', ' ', ' ', ' ', '&', ' ', '*', ' ', ' ', ' ', ' ', '&', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '#'],
16 | ['5', ' ', ' ', ' ', '|', ' ', ' ', '&', ' ', ' ', ' ', '&', ' ', ' ', '&', ' ', ' ', ' ', ' ', '4'],
17 | ['#', ' ', ' ', ' ', '&', ' ', '&', '&', ' ', ' ', ' ', '&', ' ', ' ', '&', ' ', ' ', ' ', ' ', '#'],
18 | ['#', ' ', ' ', ' ', ' ', ' ', ' ', '&', ' ', ' ', ' ', ' ', ' ', ' ', '&', ' ', ' ', ' ', ' ', '#'],
19 | ['#', ' ', ' ', ' ', ' ', ' ', ' ', '&', ' ', ' ', ' ', ' ', ' ', ' ', '&', ' ', ' ', ' ', '*', '#'],
20 | ['#', '#', '#', '#', '#', '#', '#', '&', '#', '#', '#', '#', '#', '#', '&', '#', '#', '#', '#', '#'],
21 | ],
22 | mapEntities: {
23 | " ": { type: "empty" },
24 | "#": { type: "wall", texture: "TECH_1C" },
25 | "&": { type: "wall", texture: "TECH_1E" },
26 | "4": { type: "wall", texture: "DOOR_1A" },
27 | "5": { type: "wall", texture: "DOOR_1E" },
28 | "|": { type: "door", texture: "DOOR_1A" },
29 | "*": { type: "light" },
30 | },
31 | player: {
32 | x: 1.5,
33 | y: 2.5,
34 | angle: 0,
35 | health: 100,
36 | },
37 | items: [
38 | generatePistol(3.5, 1.6, 15),
39 | generatePistolAmmo(3.5, 1.8, 15),
40 | generatePistolAmmo(3.5, 2, 15),
41 | generatePistolAmmo(3.5, 2.2, 15),
42 | generatePistolAmmo(3.5, 2.4, 15),
43 | generatePistolAmmo(16, 5, 15),
44 | ],
45 | enemies: [
46 | generateSoldier(18, 1.75, 4),
47 | generateSoldier(18, 3.25, 4),
48 | ...generateZombies(10, 6, 2.5, 0.75, 0.75, 2),
49 | ...generateZombies(10, 9.5, 4.5, 1, 0.75, 2),
50 | ...generateZombies(10, 13, 2.5, 0.75, 0.75, 2),
51 | ],
52 | endingScenario: {
53 | name: "exit",
54 | position: { x: 18, y: 2 },
55 | },
56 | };
57 |
58 | export default level;
59 |
--------------------------------------------------------------------------------
/src/levels/level_2.ts:
--------------------------------------------------------------------------------
1 | import {
2 | generateSoldier,
3 | generateZombie,
4 | generateZombies,
5 | } from "./generators/characters";
6 | import { generatePistolAmmo, generateHealthPack } from "./generators/items";
7 |
8 | const level: Level = {
9 | world: {
10 | colors: {
11 | top: { r: 0, g: 0, b: 0, a: 255 },
12 | bottom: { r: 84, g: 98, b: 92, a: 255 },
13 | },
14 | },
15 | music: "heavy-duty-zoo",
16 | // prettier-ignore
17 | map: [
18 | [1, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
19 | [1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4],
20 | [1, 0, 1, 0, 0, 2, 1, 0, 0, 1, 0, 1, 0, 2, 1, 1, 0, 0, 0, 1, 0, 2, 2, 1],
21 | [1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
22 | [1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 2, 0, 1],
23 | [1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
24 | [1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 2, 1],
25 | [1, 0, 1, 0, 0, 1, 0, 0, 0, 2, 0, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
26 | [1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 1, 1, 2, 2, 0, 1],
27 | [1, 0, 2, 0, 0, 1, 0, 0, 0, 2, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
28 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1],
29 | ],
30 | mapEntities: {
31 | 0: { type: "empty" },
32 | 1: { type: "wall", texture: "TECH_1C" },
33 | 2: { type: "wall", texture: "TECH_1E" },
34 | 3: { type: "wall", texture: "TECH_2F" },
35 | 4: { type: "wall", texture: "DOOR_1A" },
36 | 5: { type: "wall", texture: "DOOR_1E" },
37 | },
38 | player: {
39 | x: 1.5,
40 | y: 1.5,
41 | angle: 90,
42 | health: 100,
43 | },
44 | items: [
45 | generatePistolAmmo(1.5, 9.5, 15),
46 | generatePistolAmmo(6.5, 3.5, 15),
47 | generatePistolAmmo(6.5, 5.5, 15),
48 | generatePistolAmmo(6.5, 7.5, 15),
49 | generatePistolAmmo(6.5, 9.5, 15),
50 | generatePistolAmmo(10.5, 1, 20),
51 | generatePistolAmmo(10.5, 2, 20),
52 | generatePistolAmmo(10.5, 2.5, 20),
53 | generatePistolAmmo(10.5, 3, 20),
54 | generatePistolAmmo(10.5, 3.5, 20),
55 | generatePistolAmmo(10.5, 4, 20),
56 | generatePistolAmmo(10.5, 4.5, 20),
57 | generatePistolAmmo(10.5, 5, 20),
58 | generatePistolAmmo(10.5, 5.5, 20),
59 | generateHealthPack(10.5, 6, 100),
60 | ],
61 | enemies: [
62 | generateZombie(1.5, 9.5, 2),
63 | generateSoldier(6, 1, 3),
64 | generateSoldier(4.5, 6.5, 4),
65 | generateSoldier(4.5, 7.5, 4),
66 | generateSoldier(4.5, 8.5, 4),
67 | generateSoldier(4.5, 9.5, 4),
68 | generateSoldier(4.5, 10.5, 4),
69 |
70 | ...generateZombies(15, 4.5, 4.5, 1, 1, 1),
71 | generateZombie(7, 3.5, 2),
72 | generateZombie(7, 5.5, 2),
73 | generateZombie(7, 7.5, 2),
74 | generateZombie(7, 9.5, 2),
75 | generateZombie(7, 9.5, 2),
76 | generateZombie(10.5, 7.5, 2),
77 | generateZombie(10.5, 9.5, 2),
78 | generateZombie(12.5, 7.5, 2),
79 | generateZombie(12.5, 9.5, 2),
80 | generateZombie(14.5, 7.5, 2),
81 | generateZombie(14.5, 8.5, 2),
82 | generateZombie(14.5, 9.5, 2),
83 | generateZombie(18.5, 1.5, 2),
84 | generateZombie(18.5, 2.5, 2),
85 | generateZombie(18.5, 3.5, 2),
86 | generateZombie(18.5, 4.5, 2),
87 | ...generateZombies(15, 14.5, 4.5, 1, 1, 2),
88 | ...generateZombies(25, 18, 5, 1, 1, 2),
89 | ...generateZombies(25, 17, 7, 1, 1, 2),
90 | ...generateZombies(15, 16, 8, 1, 1, 2),
91 | ...generateZombies(50, 17, 2, 1, 1, 2),
92 | ],
93 | endingScenario: {
94 | name: "exit",
95 | position: {
96 | x: 22,
97 | y: 1,
98 | },
99 | },
100 | };
101 |
102 | export default level;
103 |
--------------------------------------------------------------------------------
/src/levels/level_3.ts:
--------------------------------------------------------------------------------
1 | import {
2 | generateFlygies,
3 | generateSoldiers,
4 | generateZombies,
5 | } from "./generators/characters";
6 | import { generatePistolAmmo } from "./generators/items";
7 |
8 | const level: Level = {
9 | world: {
10 | colors: {
11 | top: { r: 0, g: 0, b: 0, a: 255 },
12 | bottom: { r: 84, g: 98, b: 92, a: 255 },
13 | },
14 | },
15 | music: "heavy-duty-zoo",
16 | // prettier-ignore
17 | map: [
18 | [1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1],
19 | [1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1],
20 | [1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1],
21 | [1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 2, 0, 0, 0, 0, 4],
22 | [1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 2, 0, 0, 0, 0, 1],
23 | [1, 0, 0, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1],
24 | [1, 0, 0, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1],
25 | [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1],
26 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1],
27 | ],
28 | mapEntities: {
29 | 0: { type: "empty" },
30 | 1: { type: "wall", texture: "TECH_1C" },
31 | 2: { type: "wall", texture: "TECH_1E" },
32 | 3: { type: "wall", texture: "TECH_2F" },
33 | 4: { type: "wall", texture: "DOOR_1A" },
34 | 5: { type: "wall", texture: "DOOR_1E" },
35 | },
36 | player: {
37 | x: 2,
38 | y: 2.5,
39 | angle: 90,
40 | health: 100,
41 | },
42 | items: [
43 | generatePistolAmmo(1.75, 7.5, 15),
44 | generatePistolAmmo(2, 7.5, 15),
45 | generatePistolAmmo(2.25, 7.5, 15),
46 | ],
47 | enemies: [
48 | ...generateFlygies(10, 7, 2, 1, 1, 3),
49 | ...generateZombies(20, 8, 3, 1, 1, 5),
50 | ...generateSoldiers(20, 8, 8, 1, 1, 5),
51 | ],
52 | endingScenario: {
53 | name: "exit",
54 | position: {
55 | x: 18,
56 | y: 2,
57 | },
58 | },
59 | };
60 |
61 | export default level;
62 |
--------------------------------------------------------------------------------
/src/levels/level_final.ts:
--------------------------------------------------------------------------------
1 | import {
2 | generateCircle,
3 | generateTank,
4 | generateZombie,
5 | generateZombies,
6 | } from "./generators/characters";
7 | import { generatePistolAmmo } from "./generators/items";
8 |
9 | const level: Level = {
10 | world: {
11 | colors: {
12 | top: { r: 0, g: 0, b: 0, a: 255 },
13 | bottom: { r: 84, g: 98, b: 92, a: 255 },
14 | },
15 | },
16 | music: "zombie-world-alex-besss",
17 | // prettier-ignore
18 | map: [
19 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
20 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
21 | [1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1],
22 | [1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1],
23 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
24 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
25 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
26 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
27 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
28 | [1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1],
29 | [1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1],
30 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
31 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
32 | ],
33 | mapEntities: {
34 | 0: { type: "empty" },
35 | 1: { type: "wall", texture: "TECH_1C" },
36 | 2: { type: "wall", texture: "TECH_1E" },
37 | 3: { type: "wall", texture: "TECH_2F" },
38 | 4: { type: "wall", texture: "DOOR_1A" },
39 | 5: { type: "wall", texture: "DOOR_1E" },
40 | },
41 | player: {
42 | x: 5.5,
43 | y: 5.5,
44 | angle: 90,
45 | health: 100,
46 | },
47 | items: [
48 | generatePistolAmmo(3.5, 3.5, 15),
49 | generatePistolAmmo(3.5, 9.5, 15),
50 | generatePistolAmmo(9.5, 3.5, 15),
51 | generatePistolAmmo(9.5, 9.5, 15),
52 | ...generateCircle(6.5, 6.5, 3.5, 4).map(([x, y]) =>
53 | generatePistolAmmo(x, y, 15),
54 | ),
55 | ],
56 | enemies: [
57 | ...generateZombies(3, 4, 4, 0.5, 0.5, 8),
58 | ...generateZombies(3, 4, 8, 0.5, 0.5, 8),
59 | ...generateZombies(3, 8, 4, 0.5, 0.5, 8),
60 | ...generateZombies(3, 8, 8, 0.5, 0.5, 8),
61 | ...generateCircle(6.5, 6.5, 3.5, 10).map(([x, y]) =>
62 | generateZombie(x, y, 10),
63 | ),
64 | generateTank(2, 2, 6),
65 | generateTank(2, 8, 6),
66 | generateTank(8, 2, 6),
67 | generateTank(8, 8, 6),
68 | ],
69 | endingScenario: {
70 | name: "survive",
71 | timer: 30,
72 | },
73 | };
74 |
75 | export default level;
76 |
--------------------------------------------------------------------------------
/src/lib/Canvas/BufferCanvas.ts:
--------------------------------------------------------------------------------
1 | import { minmax } from "src/lib/utils/math";
2 |
3 | interface CanvasProps {
4 | id?: string;
5 | width: number;
6 | height: number;
7 | scale?: number;
8 | style?: string;
9 | }
10 |
11 | interface DrawVerticalLineProps {
12 | x: number;
13 | y1: number;
14 | y2: number;
15 | color: Color;
16 | }
17 |
18 | interface DrawRectProps {
19 | x: number;
20 | y: number;
21 | width: number;
22 | height: number;
23 | color: Color;
24 | }
25 |
26 | interface DrawPixelProps {
27 | x: number;
28 | y: number;
29 | color: Color;
30 | }
31 |
32 | interface DrawImageProps {
33 | x: number;
34 | y: number;
35 | texture: TextureBitmap;
36 | }
37 |
38 | export default class BufferCanvas {
39 | readonly width: number;
40 | readonly height: number;
41 |
42 | element: HTMLCanvasElement;
43 | context: CanvasRenderingContext2D;
44 |
45 | protected buffer: ImageData;
46 |
47 | constructor({ id, width, height, style, scale }: CanvasProps) {
48 | this.width = width;
49 | this.height = height;
50 |
51 | this.element = document.createElement("canvas");
52 | if (id) {
53 | this.element.id = id;
54 | }
55 | this.element.width = width;
56 | this.element.height = height;
57 |
58 | if (style) {
59 | this.element.setAttribute("style", style);
60 | }
61 |
62 | this.context = this.element.getContext("2d")!;
63 |
64 | if (scale) {
65 | this.context.scale(scale, scale);
66 | }
67 |
68 | this.buffer = this.context.createImageData(this.width, this.height);
69 | }
70 |
71 | clear() {
72 | this.context.clearRect(0, 0, this.width, this.height);
73 | }
74 |
75 | createBufferSnapshot() {
76 | this.buffer = this.context.createImageData(this.width, this.height);
77 | }
78 |
79 | commitBufferSnapshot() {
80 | this.context.putImageData(this.buffer!, 0, 0);
81 | }
82 |
83 | drawPixel({ x, y, color }: DrawPixelProps) {
84 | if (color.a === 0) {
85 | return;
86 | }
87 | const offset = 4 * (Math.floor(x) + Math.floor(y) * this.width);
88 |
89 | this.buffer.data[offset] = color.r;
90 | this.buffer.data[offset + 1] = color.g;
91 | this.buffer.data[offset + 2] = color.b;
92 | this.buffer.data[offset + 3] = color.a;
93 | }
94 |
95 | drawImage({ x, y, texture }: DrawImageProps) {
96 | for (let i = 0; i < texture.height; i++) {
97 | for (let j = 0; j < texture.width; j++) {
98 | const color = texture.colors[i][j];
99 | if (color.a !== 0) {
100 | this.drawPixel({
101 | x: x + j,
102 | y: y + i,
103 | color,
104 | });
105 | }
106 | }
107 | }
108 | }
109 |
110 | drawVerticalLine({ x, y1, y2, color }: DrawVerticalLineProps) {
111 | for (let y = y1; y < y2; y++) {
112 | this.drawPixel({ x, y, color });
113 | }
114 | }
115 |
116 | drawRect({ x, y, width, height, color }: DrawRectProps) {
117 | const startX = minmax(x, 0, this.width);
118 | const startY = minmax(y, 0, this.width);
119 | const limitX = Math.min(this.width, x + width);
120 | const limitY = Math.min(this.height, y + height);
121 |
122 | for (let i = startX; i < limitX; i++) {
123 | for (let j = startY; j < limitY; j++) {
124 | this.drawPixel({
125 | x: i,
126 | y: j,
127 | color,
128 | });
129 | }
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/lib/Canvas/DefaultCanvas.ts:
--------------------------------------------------------------------------------
1 | interface CanvasProps {
2 | id?: string;
3 | width: number;
4 | height: number;
5 | scale?: number;
6 | style?: string;
7 | }
8 |
9 | interface DrawLineProps {
10 | x1: number;
11 | y1: number;
12 | x2: number;
13 | y2: number;
14 | color: string | CanvasGradient | CanvasPattern;
15 | }
16 |
17 | interface DrawVerticalLineProps {
18 | x: number;
19 | y1: number;
20 | y2: number;
21 | color: string | CanvasGradient | CanvasPattern;
22 | }
23 |
24 |
25 | interface DrawRectProps {
26 | x: number;
27 | y: number;
28 | width: number;
29 | height: number;
30 | color: string | CanvasGradient | CanvasPattern;
31 | }
32 |
33 | interface DrawPolygonProps {
34 | paths: number[];
35 | color: string | CanvasGradient | CanvasPattern;
36 | }
37 |
38 | interface DrawCircleProps {
39 | x: number;
40 | y: number;
41 | radius: number;
42 | color: string;
43 | }
44 |
45 | interface DrawTextProps {
46 | x: number;
47 | y: number;
48 | align?: CanvasTextAlign;
49 | text: string;
50 | font: string;
51 | color?: string | CanvasGradient | CanvasPattern;
52 | }
53 |
54 | export default class Canvas {
55 | readonly width: number;
56 | readonly height: number;
57 |
58 | element: HTMLCanvasElement;
59 | context: CanvasRenderingContext2D;
60 |
61 | constructor({ id, width, height, style, scale }: CanvasProps) {
62 | this.width = width;
63 | this.height = height;
64 |
65 | this.element = document.createElement('canvas');
66 | if (id) {
67 | this.element.id = id;
68 | }
69 | this.element.width = width;
70 | this.element.height = height;
71 |
72 | if (style) {
73 | this.element.setAttribute('style', style);
74 | }
75 |
76 | this.context = this.element.getContext('2d')!;
77 |
78 | if (scale) {
79 | this.context.scale(scale, scale);
80 | }
81 | }
82 |
83 | drawBackground(color: string) {
84 | this.context.fillStyle = color;
85 | this.context.fillRect(0, 0, this.width, this.height);
86 | }
87 |
88 | drawVerticalLine({ x, y1, y2, color }: DrawVerticalLineProps) {
89 | this.context.fillStyle = color;
90 | this.context.fillRect(
91 | x,
92 | y1,
93 | 1,
94 | y2 - y1,
95 | );
96 | }
97 |
98 | drawLine({ x1, y1, x2, y2, color }: DrawLineProps) {
99 | this.context.strokeStyle = color;
100 | this.context.beginPath();
101 | this.context.moveTo(x1, y1);
102 | this.context.lineTo(x2, y2);
103 | this.context.stroke();
104 | }
105 |
106 | drawRect({ x, y, width, height, color }: DrawRectProps) {
107 | this.context.fillStyle = color;
108 | this.context.fillRect(
109 | x,
110 | y,
111 | width,
112 | height
113 | );
114 | }
115 |
116 | drawPolygon({ paths, color }: DrawPolygonProps) {
117 | if (paths.length < 8) return;
118 | this.context.fillStyle = color;
119 | this.context.beginPath();
120 |
121 | this.context.moveTo(paths[0], paths[1]);
122 |
123 | for (let i = 2; i < paths.length - 1; i += 2) {
124 | this.context.lineTo(paths[i], paths[i + 1]);
125 | }
126 | this.context.closePath();
127 | this.context.fill();
128 | }
129 |
130 | drawCircle({ x, y, radius, color }: DrawCircleProps) {
131 | this.context.beginPath();
132 | this.context.arc(x, y, radius, 0, 2 * Math.PI);
133 | this.context.fillStyle = color;
134 | this.context.fill();
135 | }
136 |
137 | drawText({ x, y, color, font, text, align }: DrawTextProps) {
138 | if (align) {
139 | this.context.textAlign = align;
140 | }
141 | if (color) {
142 | this.context.fillStyle = color;
143 | }
144 | this.context.font = font;
145 | this.context.fillText(text, x, y);
146 | }
147 |
148 | clear() {
149 | this.context.clearRect(0, 0, this.width, this.height);
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/lib/Canvas/WebglCanvas.ts:
--------------------------------------------------------------------------------
1 | import { minmax } from "src/lib/utils/math";
2 | import { createTextureFromBuffer, setupWebGL, WebGLContext } from "./lib/webgl";
3 |
4 | interface CanvasProps {
5 | id?: string;
6 | width: number;
7 | height: number;
8 | style?: string;
9 | }
10 |
11 | interface DrawVerticalLineProps {
12 | x: number;
13 | y1: number;
14 | y2: number;
15 | color: Color;
16 | }
17 |
18 | interface DrawRectProps {
19 | x: number;
20 | y: number;
21 | width: number;
22 | height: number;
23 | color: Color;
24 | }
25 |
26 | interface DrawPixelProps {
27 | x: number;
28 | y: number;
29 | color: Color;
30 | }
31 |
32 | interface DrawImageProps {
33 | x: number;
34 | y: number;
35 | texture: TextureBitmap;
36 | }
37 |
38 | export default class WebglCanvas {
39 | width: number;
40 | height: number;
41 |
42 | element: HTMLCanvasElement;
43 |
44 | protected buffer: Uint8Array;
45 | protected setup: WebGLContext;
46 |
47 | constructor({ id, width, height, style }: CanvasProps) {
48 | const canvas = document.createElement("canvas");
49 | const setup = setupWebGL(canvas, width, height);
50 |
51 | if (!setup) {
52 | throw new Error("bad setup");
53 | }
54 |
55 | this.setup = setup;
56 |
57 | this.width = width;
58 | this.height = height;
59 | this.element = canvas;
60 | this.element.width = width;
61 | this.element.height = height;
62 |
63 | if (id) {
64 | this.element.id = id;
65 | }
66 |
67 | if (style) {
68 | this.element.setAttribute("style", style);
69 | }
70 |
71 | this.buffer = new Uint8Array(width * height * 4);
72 | }
73 |
74 | clear() {
75 | this.setup.gl.clear(0);
76 | this.buffer.fill(0);
77 | }
78 |
79 | createBufferSnapshot() {}
80 |
81 | commitBufferSnapshot() {
82 | const { gl, program, buffers } = this.setup;
83 | const texture = createTextureFromBuffer(
84 | gl,
85 | this.width,
86 | this.height,
87 | this.buffer,
88 | );
89 |
90 | const vertexPosition = gl.getAttribLocation(program, "aVertexPosition");
91 | const textureCoord = gl.getAttribLocation(program, "aTextureCoord");
92 | const uSampler = gl.getUniformLocation(program, "uSampler");
93 |
94 | gl.clearColor(0.0, 0.0, 0.0, 1.0);
95 | gl.clear(gl.COLOR_BUFFER_BIT);
96 |
97 | gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
98 | gl.vertexAttribPointer(vertexPosition, 2, gl.FLOAT, false, 0, 0);
99 | gl.enableVertexAttribArray(vertexPosition);
100 |
101 | gl.bindBuffer(gl.ARRAY_BUFFER, buffers.textureCoord);
102 | gl.vertexAttribPointer(textureCoord, 2, gl.FLOAT, false, 0, 0);
103 | gl.enableVertexAttribArray(textureCoord);
104 |
105 | gl.useProgram(program);
106 |
107 | gl.activeTexture(gl.TEXTURE0);
108 | gl.bindTexture(gl.TEXTURE_2D, texture);
109 | gl.uniform1i(uSampler, 0);
110 |
111 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
112 | }
113 |
114 | drawPixel({ x, y, color }: DrawPixelProps) {
115 | if (color.a === 0) {
116 | return;
117 | }
118 | const offset = 4 * (Math.floor(x) + Math.floor(y) * this.width);
119 |
120 | this.buffer[offset] = color.r;
121 | this.buffer[offset + 1] = color.g;
122 | this.buffer[offset + 2] = color.b;
123 | this.buffer[offset + 3] = color.a;
124 | }
125 |
126 | drawImage({ x, y, texture }: DrawImageProps) {
127 | for (let i = 0; i < texture.height; i++) {
128 | for (let j = 0; j < texture.width; j++) {
129 | const color = texture.colors[i][j];
130 | if (color.a !== 0) {
131 | this.drawPixel({
132 | x: x + j,
133 | y: y + i,
134 | color,
135 | });
136 | }
137 | }
138 | }
139 | }
140 |
141 | drawVerticalLine({ x, y1, y2, color }: DrawVerticalLineProps) {
142 | for (let y = y1; y < y2; y++) {
143 | this.drawPixel({ x, y, color });
144 | }
145 | }
146 |
147 | drawRect({ x, y, width, height, color }: DrawRectProps) {
148 | const startX = minmax(x, 0, this.width);
149 | const startY = minmax(y, 0, this.width);
150 | const limitX = Math.min(this.width, x + width);
151 | const limitY = Math.min(this.height, y + height);
152 |
153 | for (let i = startX; i < limitX; i++) {
154 | for (let j = startY; j < limitY; j++) {
155 | this.drawPixel({
156 | x: i,
157 | y: j,
158 | color,
159 | });
160 | }
161 | }
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/lib/Canvas/lib/webgl.ts:
--------------------------------------------------------------------------------
1 | export interface WebGLContext {
2 | gl: WebGLRenderingContext;
3 | program: WebGLProgram;
4 | buffers: {
5 | position: WebGLBuffer,
6 | textureCoord: WebGLBuffer
7 | }
8 | }
9 |
10 | const vertexShaderSource = `
11 | attribute vec4 aVertexPosition;
12 | attribute vec2 aTextureCoord;
13 | varying highp vec2 vTextureCoord;
14 | void main(void) {
15 | gl_Position = aVertexPosition;
16 | vTextureCoord = aTextureCoord;
17 | }
18 | `;
19 |
20 | const fragmentShaderSource = `
21 | varying highp vec2 vTextureCoord;
22 | uniform sampler2D uSampler;
23 | void main(void) {
24 | gl_FragColor = texture2D(uSampler, vTextureCoord);
25 | }
26 | `;
27 |
28 | function createShaderProgram(gl: WebGLRenderingContext): WebGLProgram {
29 | const vertexShader = gl.createShader(gl.VERTEX_SHADER)!;
30 | gl.shaderSource(vertexShader, vertexShaderSource);
31 | gl.compileShader(vertexShader);
32 |
33 | if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
34 | throw new Error(`ERROR compiling vertex shader: ${gl.getShaderInfoLog(vertexShader)}`);
35 | }
36 |
37 | const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)!;
38 | gl.shaderSource(fragmentShader, fragmentShaderSource);
39 | gl.compileShader(fragmentShader);
40 |
41 | if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
42 | throw new Error(`ERROR compiling fragment shader: ${gl.getShaderInfoLog(vertexShader)}`);
43 | }
44 |
45 | const program = gl.createProgram()!;
46 |
47 | gl.attachShader(program, vertexShader);
48 | gl.attachShader(program, fragmentShader);
49 | gl.linkProgram(program);
50 |
51 | return program;
52 | }
53 |
54 | function createBuffers(gl: WebGLRenderingContext) {
55 | const positionBuffer = gl.createBuffer();
56 | gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
57 | const positions = [
58 | -1.0, 1.0,
59 | 1.0, 1.0,
60 | -1.0, -1.0,
61 | 1.0, -1.0,
62 | ];
63 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
64 |
65 | const textureCoordBuffer = gl.createBuffer();
66 | gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
67 | const textureCoordinates = [
68 | 0.0, 0.0,
69 | 1.0, 0.0,
70 | 0.0, 1.0,
71 | 1.0, 1.0,
72 | ];
73 |
74 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gl.STATIC_DRAW);
75 |
76 | return {
77 | position: positionBuffer!,
78 | textureCoord: textureCoordBuffer!,
79 | };
80 | }
81 |
82 | export function createTextureFromBuffer(gl: WebGLRenderingContext, width: number, height: number, data: Uint8Array): WebGLTexture {
83 | const texture = gl.createTexture()!;
84 | gl.bindTexture(gl.TEXTURE_2D, texture);
85 |
86 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
87 |
88 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
89 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
90 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
91 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
92 |
93 | return texture;
94 | }
95 |
96 |
97 | export function setupWebGL(canvas: HTMLCanvasElement, width: number, height: number): WebGLContext {
98 | const gl = canvas.getContext('webgl') as WebGLRenderingContext;
99 |
100 | if (!gl) {
101 | throw new Error('WebGL not supported');
102 | }
103 |
104 | gl.viewport(0, 0, width, height);
105 |
106 | const program = createShaderProgram(gl);
107 | const buffers = createBuffers(gl);
108 |
109 | return { gl, program, buffers };
110 | }
111 |
--------------------------------------------------------------------------------
/src/lib/ecs/Component.ts:
--------------------------------------------------------------------------------
1 | export interface Component {}
2 |
3 | export type ComponentClass = new (...args: any[]) => T
4 |
5 | export class ComponentContainer {
6 | private map = new Map()
7 |
8 | public add(component: Component): void {
9 | this.map.set(component.constructor, component);
10 | }
11 |
12 | public get(componentClass: ComponentClass): T {
13 | return this.map.get(componentClass) as T;
14 | }
15 |
16 | public has(componentClass: Function): boolean {
17 | return this.map.has(componentClass);
18 | }
19 |
20 | public all(componentClasses: Iterable): boolean {
21 | for (const componentClass of componentClasses) {
22 | if (!this.map.has(componentClass)) {
23 | return false;
24 | }
25 | }
26 | return true;
27 | }
28 |
29 | public delete(componentClass: Function): void {
30 | this.map.delete(componentClass);
31 | }
32 | }
--------------------------------------------------------------------------------
/src/lib/ecs/Entity.ts:
--------------------------------------------------------------------------------
1 | export type Entity = number;
2 |
--------------------------------------------------------------------------------
/src/lib/ecs/System.ts:
--------------------------------------------------------------------------------
1 | import ECS from "src/lib/ecs";
2 | import { Entity } from "src/lib/ecs/Entity";
3 |
4 | export default abstract class System {
5 | public ecs: ECS;
6 |
7 | public readonly abstract componentsRequired: Set;
8 |
9 | constructor(ecs: ECS) {
10 | this.ecs = ecs;
11 | }
12 |
13 | abstract start(): void;
14 | abstract update(dt: number, entities: Set): void;
15 | abstract destroy(): void;
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/AIComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class SimpleAIComponent implements Component {
4 | activateDistance: number;
5 | actionPassedTime: number = Infinity;
6 |
7 | constructor(activateDistance: number) {
8 | this.activateDistance = activateDistance;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/AngleComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class AngleComponent implements Component {
4 | angle: number;
5 |
6 | constructor(angle = 0) {
7 | this.angle = angle;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/AnimatedSpriteComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class AnimatedSpriteComponent implements Component {
4 |
5 | states: Record = {};
6 | currentState: string;
7 | currentFrame: number;
8 | sprite: TextureBitmap;
9 | animationSpeed: number = 0.2;
10 | timeSinceLastFrame: number = 0;
11 | loop: boolean = false;
12 |
13 | constructor(initialState: string, states: Record) {
14 | this.states = states;
15 | this.currentFrame = 0;
16 | this.currentState = initialState;
17 | this.sprite = states[initialState][0];
18 | }
19 |
20 | update(dt: number) {
21 | const frames = this.states[this.currentState];
22 | this.timeSinceLastFrame += dt;
23 |
24 | if (!this.loop && this.currentFrame === frames.length -1) {
25 | return
26 | }
27 |
28 | if (this.timeSinceLastFrame > this.animationSpeed) {
29 | this.currentFrame = (this.currentFrame + 1) % frames.length;
30 | this.sprite = frames[this.currentFrame];
31 | this.timeSinceLastFrame = 0;
32 | }
33 | }
34 |
35 | switchState(stateName: string, loop: boolean) {
36 | if (this.currentState === stateName && loop == true) {
37 | return
38 | }
39 |
40 | if (stateName in this.states) {
41 | this.currentFrame = 0;
42 | this.currentState = stateName;
43 | this.loop = loop;
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/BoxComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class BoxComponent implements Component {
4 | size: number;
5 |
6 | constructor(size = 0) {
7 | this.size = size;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/BulletComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class BulletComponent implements Component {
4 | fromEntity: number;
5 | damage: number = 5;
6 | createdAt: number = +new Date();
7 |
8 | constructor(fromEntity: number, damage: number) {
9 | this.fromEntity = fromEntity;
10 | this.damage = damage;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/CameraComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class CameraComponent implements Component {
4 | fov: number; // FieldOfVisionComponent
5 |
6 | constructor(fov: number = 0) {
7 | this.fov = fov;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/CircleComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class CircleComponent implements Component {
4 | radius: number;
5 |
6 | constructor(radius = 0) {
7 | this.radius = radius;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/CollisionComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component, ComponentContainer } from "src/lib/ecs/Component";
2 |
3 | export default class CollisionComponent implements Component {
4 | collidedEntity?: ComponentContainer;
5 | isCollided: boolean = false;
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/ControlComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class ControlComponent implements Component {}
4 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/DoorComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class DoorComponent implements Component {
4 | isOpened: boolean;
5 | animationTime: number;
6 | offset: number = 0;
7 | isVertical: boolean;
8 |
9 | constructor(isOpened: boolean, isVerticalDoor: boolean) {
10 | this.isOpened = isOpened;
11 | this.isVertical = isVerticalDoor;
12 | this.animationTime = 1;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/EnemyComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class EnemyComponent implements Component {}
4 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/HealthComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class HealthComponent implements Component {
4 | public maximum: number;
5 | public current: number;
6 |
7 | constructor(maximum: number, current: number) {
8 | this.maximum = maximum;
9 | this.current = current;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/HighlightComponent.ts:
--------------------------------------------------------------------------------
1 |
2 | import { Component } from "src/lib/ecs/Component";
3 |
4 | export default class HighlightComponent implements Component {
5 |
6 | color: Color;
7 | duration: number
8 | startedAt: number = 0;
9 |
10 | constructor(color: Color, duration: number = 500) {
11 | this.startedAt = Date.now();
12 | this.color = color;
13 | this.duration = duration;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/ItemComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class ItemComponent implements Component {
4 | type: ItemType;
5 | value: number;
6 |
7 | constructor(type: ItemType, value: number) {
8 | this.type = type;
9 | this.value = value;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/LightComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | const lightApplyFn = {
4 | linear(x: number) {
5 | return x;
6 | },
7 | easeInOutCubic(x: number) {
8 | return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
9 | },
10 | easeInQuart(x: number) {
11 | return x * x * x * x;
12 | }
13 | }
14 |
15 |
16 | export default class LightComponent implements Component {
17 | brightness: number;
18 | distance: number;
19 | lightFn: (lightLevel: number) => number;
20 | isStaticLight: boolean;
21 |
22 | constructor(
23 | distance: number,
24 | brightness: number,
25 | isStaticLight: boolean = false,
26 | lightFn: keyof typeof lightApplyFn = 'linear'
27 | ) {
28 | this.distance = distance;
29 | this.brightness = brightness;
30 | this.lightFn = lightApplyFn[lightFn];
31 | this.isStaticLight = isStaticLight;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/MinimapComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class MinimapComponent implements Component {
4 | color: string;
5 |
6 | constructor(color: string) {
7 | this.color = color;
8 | }
9 | }
--------------------------------------------------------------------------------
/src/lib/ecs/components/MoveComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export enum MainDirection {
4 |
5 | Back = -1,
6 | None,
7 | Forward,
8 | }
9 |
10 | export enum SideDirection {
11 | Left = -1,
12 | None,
13 | Right
14 | }
15 |
16 | export default class MoveComponent implements Component {
17 | constructor(
18 | public moveSpeed: number = 0,
19 | public canSlide: boolean = false,
20 | public mainDirection: MainDirection = MainDirection.None,
21 | public sideDirection: SideDirection = SideDirection.None,
22 | ) {}
23 | }
24 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/PlayerComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 | import WeaponComponent from "./WeaponComponent";
3 |
4 | export default class PlayerComponent implements Component {
5 | currentWeapon?: WeaponComponent;
6 | weapons: Record = {};
7 | }
8 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/PositionComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class PositionComponent implements Component {
4 | x: number;
5 | y: number;
6 |
7 | constructor(x = 0, y = 0) {
8 | this.x = x;
9 | this.y = y;
10 | }
11 | }
--------------------------------------------------------------------------------
/src/lib/ecs/components/RotateComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class RotateComponent implements Component {
4 | rotationSpeed: number;
5 | rotationFactor: number;
6 |
7 | constructor(rotationSpeed = 0) {
8 | this.rotationSpeed = rotationSpeed;
9 | this.rotationFactor = 0;
10 | }
11 | }
--------------------------------------------------------------------------------
/src/lib/ecs/components/SpriteComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class SpriteComponent implements Component {
4 | sprite: Sprite;
5 |
6 | constructor(sprite: Sprite) {
7 | this.sprite = sprite;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/TextureComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 |
3 | export default class TextureComponent implements Component {
4 | texture: TextureBitmap;
5 |
6 | constructor(texture: TextureBitmap) {
7 | this.texture = texture;
8 | }
9 | }
--------------------------------------------------------------------------------
/src/lib/ecs/components/WeaponComponent.ts:
--------------------------------------------------------------------------------
1 | import { Component } from "src/lib/ecs/Component";
2 | import AnimatedSpriteComponent from "./AnimatedSpriteComponent";
3 |
4 | export default class WeaponComponent implements Component {
5 | sprite?: AnimatedSpriteComponent;
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/WeaponMeleeComponent.ts:
--------------------------------------------------------------------------------
1 | import AnimatedSpriteComponent from "./AnimatedSpriteComponent";
2 | import WeaponComponent from "./WeaponComponent";
3 |
4 |
5 | interface WeaponMeleeComponentProps {
6 | attackDamage: number;
7 | attackFrequency: number;
8 | sprite?: AnimatedSpriteComponent
9 | }
10 |
11 | export default class WeaponMeleeComponent extends WeaponComponent {
12 |
13 | attackDamage: number;
14 | attackFrequency: number;
15 | attackLastTimeAt: number = +new Date();
16 |
17 | constructor(props: WeaponMeleeComponentProps) {
18 | super();
19 |
20 | const { attackDamage = 15, attackFrequency = 1_000, sprite } = props;
21 |
22 | this.sprite = sprite;
23 | this.attackDamage = attackDamage;
24 | this.attackFrequency = attackFrequency;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/lib/ecs/components/WeaponRangeComponent.ts:
--------------------------------------------------------------------------------
1 | import AnimatedSpriteComponent from "./AnimatedSpriteComponent";
2 | import WeaponComponent from "./WeaponComponent";
3 |
4 | interface WeaponRangeComponentProps {
5 | bulletSprite: string;
6 | bulletTotal: number;
7 | bulletDamage: number;
8 | bulletSpeed: number;
9 | attackDistance: number;
10 | attackFrequency: number;
11 | sprite?: AnimatedSpriteComponent;
12 | }
13 |
14 | export default class WeaponRangeComponent extends WeaponComponent {
15 |
16 | bulletSprite: string;
17 | bulletTotal: number;
18 | bulletSpeed: number;
19 | bulletDamage: number;
20 |
21 | attackDistance: number;
22 | attackFrequency: number;
23 | attackLastTimeAt: number = +new Date();
24 |
25 | constructor(props: WeaponRangeComponentProps) {
26 | super();
27 |
28 | const {
29 | bulletSprite = '',
30 | bulletTotal = 30,
31 | bulletDamage = 15,
32 | bulletSpeed = 5,
33 | attackDistance = 4,
34 | attackFrequency = 1_000,
35 | sprite
36 | } = props;
37 |
38 | this.bulletSprite = bulletSprite;
39 | this.bulletTotal = bulletTotal;
40 | this.bulletDamage = bulletDamage;
41 | this.bulletSpeed = bulletSpeed;
42 |
43 | this.attackDistance = attackDistance;
44 | this.attackFrequency = attackFrequency;
45 |
46 | this.sprite = sprite;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/lib/ecs/index.ts:
--------------------------------------------------------------------------------
1 | import { Component, ComponentContainer } from "./Component";
2 | import { Entity } from "./Entity";
3 | import System from "./System";
4 |
5 | type EntityCallback = (entity: Entity) => void;
6 |
7 | export default class ECS {
8 | protected entities = new Map();
9 | protected systems = new Map>()
10 |
11 | protected componentAddCallbacks: Map> = new Map();
12 | protected componentRemoveCallbacks: Map> = new Map();
13 |
14 | protected nextEntityID = 0;
15 | protected entitiesToDestroy = new Array();
16 |
17 | public start(): void {
18 | for (const system of this.systems.keys()) {
19 | system.start();
20 | }
21 | }
22 |
23 | public update(dt: number): void {
24 | for (const [system, entities] of this.systems.entries()) {
25 | system.update(dt, entities);
26 | }
27 |
28 | while (this.entitiesToDestroy.length > 0) {
29 | this.destroyEntity(this.entitiesToDestroy.pop()!);
30 | }
31 | }
32 |
33 | public destroy(): void {
34 | for (const system of this.systems.keys()) {
35 | system.destroy();
36 | }
37 | }
38 |
39 | protected entitiesByQuery = new Map>();
40 |
41 | public query(componentClasses: Function[]): Set {
42 | const matchingEntities = new Set();
43 |
44 | for (const [entity, components] of this.entities.entries()) {
45 | if (components.all(componentClasses)) {
46 | matchingEntities.add(entity);
47 | }
48 | }
49 |
50 | return matchingEntities
51 | }
52 |
53 | public addEntity(): Entity {
54 | const entity = this.nextEntityID;
55 |
56 | this.nextEntityID++;
57 | this.entities.set(entity, new ComponentContainer());
58 |
59 | return entity;
60 | }
61 |
62 | public removeEntity(entity: Entity) {
63 | this.entitiesToDestroy.push(entity);
64 | }
65 |
66 | private syncEntity(entity: Entity): void {
67 | for (const system of this.systems.keys()) {
68 | this.syncSystem(entity, system);
69 | }
70 | }
71 |
72 | private destroyEntity(entity: Entity) {
73 | this.entities.delete(entity);
74 | for (const entities of this.systems.values()) {
75 | entities.delete(entity);
76 | }
77 | }
78 |
79 |
80 | public getComponents(entity: Entity) {
81 | return this.entities.get(entity)!;
82 | }
83 |
84 | public addComponent(entity: Entity, component: Component) {
85 | this.entities.get(entity)?.add(component);
86 | this.syncEntity(entity);
87 | this.componentAddCallbacks.get(component.constructor)?.forEach(cb => cb(entity));
88 | }
89 |
90 | public onComponentAdd(componentClass: Function, callback: EntityCallback) {
91 | if (this.componentAddCallbacks.has(componentClass)) {
92 | this.componentAddCallbacks.set(componentClass, new Set());
93 | }
94 | this.componentAddCallbacks.get(componentClass)?.add(callback);
95 | }
96 |
97 | public removeComponent(entity: Entity, componentClass: Function) {
98 | this.entities.get(entity)?.delete(componentClass);
99 | this.syncEntity(entity);
100 |
101 | this.componentRemoveCallbacks.get(componentClass)?.forEach(cb => cb(entity));
102 | }
103 |
104 | public onComponentRemove(componentClass: Function, callback: EntityCallback) {
105 | this.componentRemoveCallbacks.get(componentClass)?.delete(callback);
106 | }
107 |
108 | public addSystem(system: System) {
109 | if (system.componentsRequired.size == 0) {
110 | return;
111 | }
112 |
113 | this.systems.set(system, new Set());
114 |
115 | for (const entity of this.entities.keys()) {
116 | this.syncSystem(entity, system);
117 | }
118 | }
119 |
120 | public getSystem(systemClass: { new (...args:any[]): T }): T | undefined {
121 | for (const system of this.systems.keys()) {
122 | if (system instanceof systemClass) {
123 | return system;
124 | }
125 | }
126 | }
127 |
128 | private syncSystem(entity: Entity, system: System): void {
129 | const components = this.entities.get(entity);
130 |
131 | if (components) {
132 | if (components.all(system.componentsRequired)) {
133 | this.systems.get(system)!.add(entity);
134 | } else {
135 | this.systems.get(system)!.delete(entity);
136 | }
137 | }
138 | }
139 |
140 | public removeSystem(system: System): void {
141 | this.systems.delete(system);
142 | }
143 | }
--------------------------------------------------------------------------------
/src/lib/ecs/lib/PolarMap.ts:
--------------------------------------------------------------------------------
1 | import { distance } from "src/lib/utils/math.ts";
2 | import { angle, normalizeAngle } from "src/lib/utils/angle";
3 | import PositionComponent from "src/lib/ecs/components/PositionComponent";
4 | import CircleComponent from "src/lib/ecs/components/CircleComponent";
5 | import { ComponentContainer } from "src/lib/ecs/Component";
6 |
7 | type Radius = number;
8 | type Angle = number;
9 |
10 | export interface PolarPosition {
11 | distance: Radius;
12 | angleFrom: Angle;
13 | angleTo: Angle;
14 | container: ComponentContainer;
15 | }
16 |
17 | export default class PolarMap {
18 | public center?: ComponentContainer;
19 | public entities?: ComponentContainer[];
20 |
21 | protected polarEntities: PolarPosition[] = [];
22 |
23 | public select(distanceTo: number, angleFrom: number, angleTo: number) {
24 | angleFrom = normalizeAngle(angleFrom);
25 | angleTo = normalizeAngle(angleTo);
26 |
27 | return this.polarEntities
28 | .filter((polarEntity) => {
29 | if (distanceTo <= polarEntity.distance) {
30 | return false;
31 | }
32 |
33 | const a1 = 0;
34 | const a2 = normalizeAngle(polarEntity.angleTo - polarEntity.angleFrom);
35 | const b1 = normalizeAngle(angleFrom - polarEntity.angleFrom);
36 | const b2 = normalizeAngle(angleTo - polarEntity.angleFrom);
37 |
38 | return a1 <= b1 && b1 <= a2 && a1 <= b2 && b2 <= a2;
39 | })
40 | .sort((pe1, pe2) => pe2.distance - pe1.distance);
41 | }
42 |
43 | public calculatePolarEntities() {
44 | const centerPosition = this.center?.get(PositionComponent);
45 |
46 | if (!this.entities || !centerPosition) {
47 | return;
48 | }
49 |
50 | this.polarEntities = this.entities
51 | .map((container) => {
52 | const pointCircle = container.get(CircleComponent);
53 | const pointPosition = container.get(PositionComponent);
54 | const a = angle(
55 | centerPosition.x,
56 | centerPosition.y,
57 | pointPosition.x,
58 | pointPosition.y,
59 | );
60 |
61 | const d = distance(
62 | centerPosition.x,
63 | centerPosition.y,
64 | pointPosition.x,
65 | pointPosition.y,
66 | );
67 |
68 | const ta = Math.asin(pointCircle.radius / d) * (180 / Math.PI);
69 |
70 | return {
71 | distance: d,
72 | angleFrom: normalizeAngle(a - ta),
73 | angleTo: normalizeAngle(a + ta),
74 | container,
75 | };
76 | })
77 | .filter((polarEntity) => !isNaN(polarEntity.angleFrom));
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/lib/ecs/lib/PositionMap.ts:
--------------------------------------------------------------------------------
1 | export default class PositionMap {
2 | map: Map;
3 |
4 | rows: number;
5 | cols: number;
6 |
7 | constructor(levelMap: LevelMap) {
8 | this.cols = levelMap[0].length;
9 | this.rows = levelMap.length;
10 |
11 | this.map = new Map();
12 | }
13 |
14 | public set(x: number, y: number, entity: T) {
15 | this.map.set(y * this.cols + x, entity);
16 | }
17 |
18 | public get(x: number, y: number) {
19 | return this.map.get(y * this.cols + x);
20 | }
21 |
22 | public has(x: number, y: number) {
23 | return this.map.has(y * this.cols + x);
24 | }
25 |
26 | public reset(x: number, y: number) {
27 | return this.map.delete(y * this.cols + x);
28 | }
29 |
30 | public clear() {
31 | this.map.clear();
32 | }
33 |
34 | public toArray(): (T | undefined)[][] {
35 | const list: (T | undefined)[][] = [];
36 | for (let y = 0; y < this.rows; y++) {
37 | const row: (T | undefined)[] = [];
38 | for (let x = 0; x < this.cols; x++) {
39 | row.push(this.get(x, y));
40 | }
41 | list.push(row);
42 | }
43 | return list;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/lib/ecs/lib/ScaledMap.ts:
--------------------------------------------------------------------------------
1 | export class ScaledMap {
2 | public width: number;
3 | public height: number;
4 | public scale: number;
5 | public scaledWidth: number;
6 | public scaledHeight: number;
7 |
8 | private readonly data: Float64Array;
9 |
10 | constructor(width: number, height: number, scale = 1) {
11 | this.width = width;
12 | this.height = height;
13 |
14 | this.scale = scale;
15 | this.scaledWidth = Math.round(width * scale);
16 | this.scaledHeight = Math.round(height * scale);
17 |
18 | this.data = new Float64Array(this.scaledWidth * this.scaledHeight);
19 | }
20 |
21 | clean() {
22 | this.data.fill(0);
23 | }
24 |
25 | set(x: number, y: number, val: number) {
26 | const startY = Math.floor(y * this.scale);
27 | const endY = Math.ceil((y + 1) * this.scale);
28 | const startX = Math.floor(x * this.scale);
29 | const endX = Math.ceil((x + 1) * this.scale);
30 |
31 | for (let sy = startY; sy < endY; sy++) {
32 | for (let sx = startX; sx < endX; sx++) {
33 | this.data[sy * this.scaledWidth + sx] = Math.min(1, Math.max(0, val));
34 | }
35 | }
36 | }
37 |
38 | get(x: number, y: number) {
39 | const sx = Math.floor(x * this.scale);
40 | const sy = Math.floor(y * this.scale);
41 | const idx = sy * this.scaledWidth + sx;
42 | return idx >= 0 && idx < this.data.length ? this.data[idx] : 0;
43 | }
44 |
45 | setScaled(x: number, y: number, val: number) {
46 | const idx = Math.floor(y * this.scaledWidth + x);
47 | if (idx >= 0 && idx < this.data.length) {
48 | this.data[idx] = Math.min(1, Math.max(0, val));
49 | }
50 | }
51 |
52 | getScaled(x: number, y: number) {
53 | const idx = Math.floor(y * this.scaledWidth + x);
54 | return idx >= 0 && idx < this.data.length ? this.data[idx] : 0;
55 | }
56 |
57 | getInPercents(px: number, py: number) {
58 | const x = (px * this.scaledWidth) | 0;
59 | const y = (py * this.scaledHeight) | 0;
60 | const idx = y * this.scaledWidth + x;
61 | if (idx >= 0 && idx < this.data.length) return this.data[idx];
62 | return 0;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/lib/ecs/systems/AISystem.ts:
--------------------------------------------------------------------------------
1 | import ECS from "src/lib/ecs";
2 | import System from "src/lib/ecs/System";
3 | import { Entity } from "src/lib/ecs/Entity";
4 | import AIComponent from "src/lib/ecs/components/AIComponent";
5 | import AngleComponent from "src/lib/ecs/components/AngleComponent";
6 | import AnimatedSpriteComponent from "src/lib/ecs/components/AnimatedSpriteComponent";
7 | import CircleComponent from "src/lib/ecs/components/CircleComponent";
8 | import EnemyComponent from "src/lib/ecs/components/EnemyComponent";
9 | import HealthComponent from "src/lib/ecs/components/HealthComponent";
10 | import MoveComponent, {
11 | MainDirection,
12 | SideDirection,
13 | } from "src/lib/ecs/components/MoveComponent";
14 | import PositionComponent from "src/lib/ecs/components/PositionComponent";
15 | import SoundManager from "src/managers/SoundManager";
16 | import { normalizeAngle, radiansToDegrees } from "src/lib/utils/angle";
17 | import PlayerComponent from "src/lib/ecs/components/PlayerComponent";
18 | import WeaponRangeComponent from "src/lib/ecs/components/WeaponRangeComponent";
19 | import BulletComponent from "src/lib/ecs/components/BulletComponent";
20 | import CollisionComponent from "src/lib/ecs/components/CollisionComponent";
21 | import MinimapComponent from "src/lib/ecs/components/MinimapComponent";
22 | import TextureManager from "src/managers/TextureManager";
23 | import SpriteComponent from "src/lib/ecs/components/SpriteComponent";
24 | import WeaponMeleeComponent from "src/lib/ecs/components/WeaponMeleeComponent";
25 | import MapTextureSystem from "./MapTextureSystem";
26 | import DoorComponent from "../components/DoorComponent";
27 |
28 | export default class AISystem extends System {
29 | public readonly componentsRequired = new Set([
30 | AIComponent,
31 | EnemyComponent,
32 | PositionComponent,
33 | AngleComponent,
34 | MoveComponent,
35 | ]);
36 |
37 | protected readonly soundManager: SoundManager;
38 | protected readonly textureManager: TextureManager;
39 |
40 | constructor(
41 | ecs: ECS,
42 | textureManager: TextureManager,
43 | soundManager: SoundManager,
44 | ) {
45 | super(ecs);
46 | this.soundManager = soundManager;
47 | this.textureManager = textureManager;
48 | }
49 |
50 | start(): void {}
51 |
52 | update(dt: number, enemies: Set) {
53 | const [player] = this.ecs.query([
54 | PlayerComponent,
55 | HealthComponent,
56 | CircleComponent,
57 | ]);
58 |
59 | if (typeof player === "undefined") {
60 | return;
61 | }
62 |
63 | const playerContainer = this.ecs.getComponents(player);
64 | const playerPosition = playerContainer.get(PositionComponent);
65 | const playerCircle = playerContainer.get(CircleComponent);
66 |
67 | for (const enemy of enemies) {
68 | const components = this.ecs.getComponents(enemy);
69 |
70 | const enemyAI = components.get(AIComponent);
71 | const enemyHealth = components.get(HealthComponent);
72 | const enemyPosition = components.get(PositionComponent);
73 | const enemyAngle = components.get(AngleComponent);
74 | const enemyCircle = components.get(CircleComponent);
75 | const enemyMove = components.get(MoveComponent);
76 | const enemyAnimation = components.get(AnimatedSpriteComponent);
77 |
78 | if (enemyHealth.current <= 0) {
79 | continue;
80 | }
81 |
82 | const dx = playerPosition.x - enemyPosition.x;
83 | const dy = playerPosition.y - enemyPosition.y;
84 | const d =
85 | Math.sqrt(dx ** 2 + dy ** 2) -
86 | playerCircle.radius -
87 | enemyCircle?.radius;
88 |
89 | const shouldEnemyBeActivated =
90 | enemyAI.activateDistance > d &&
91 | !this.hasTextureBetween(playerPosition, enemyPosition);
92 |
93 | if (!shouldEnemyBeActivated) {
94 | enemyAnimation.switchState("idle", true);
95 | enemyMove.mainDirection = MainDirection.None;
96 | enemyMove.sideDirection = SideDirection.None;
97 | continue;
98 | }
99 |
100 | const angle =
101 | dx <= 0
102 | ? radiansToDegrees(Math.atan(dy / dx)) + 180
103 | : radiansToDegrees(Math.atan(dy / dx));
104 |
105 | enemyAngle.angle = normalizeAngle(angle);
106 |
107 | if (components.has(WeaponRangeComponent)) {
108 | this.attackWithRange(dt, d, player, enemy);
109 | continue;
110 | }
111 |
112 | if (components.has(WeaponMeleeComponent)) {
113 | this.attackClose(dt, d, player, enemy);
114 | }
115 | }
116 | }
117 |
118 | hasTextureBetween(
119 | playerPosition: PositionComponent,
120 | enemyPosition: PositionComponent,
121 | ) {
122 | const textureMap = this.ecs.getSystem(MapTextureSystem)!.textureMap;
123 |
124 | let startX = playerPosition.x;
125 | let startY = playerPosition.y;
126 | const endX = enemyPosition.x;
127 | const endY = enemyPosition.y;
128 |
129 | const dx = startX < endX ? 0.1 : -0.1;
130 | const dy = startY < endY ? 0.1 : -0.1;
131 |
132 | while (startX < endX || startY < endY) {
133 | const textureContainer = textureMap.get(
134 | Math.floor(startX),
135 | Math.floor(startY),
136 | );
137 |
138 | if (textureContainer) {
139 | const doorComponent = textureContainer.get(DoorComponent);
140 |
141 | if (doorComponent) {
142 | return !doorComponent.isOpened;
143 | }
144 |
145 | return true;
146 | }
147 |
148 | if (startX < endX) {
149 | startX += dx;
150 | }
151 | if (startY < endY) {
152 | startY += dy;
153 | }
154 | }
155 |
156 | return false;
157 | }
158 |
159 | attackClose(dt: number, d: number, player: number, enemy: number) {
160 | const playerComponents = this.ecs.getComponents(player);
161 | const playerHealth = playerComponents.get(HealthComponent);
162 |
163 | const enemyComponents = this.ecs.getComponents(enemy);
164 | const enemyAI = enemyComponents.get(AIComponent);
165 | const enemyAnimation = enemyComponents.get(AnimatedSpriteComponent);
166 | const enemyWeapon = enemyComponents.get(WeaponMeleeComponent);
167 | const enemyMove = enemyComponents.get(MoveComponent);
168 |
169 | const shouldEnemyBeMoved = d > 0;
170 | const shouldEnemyAttack = d <= 0;
171 | const shouldEnemyDamage =
172 | enemyAI.actionPassedTime >= enemyWeapon.attackFrequency / 1_000;
173 |
174 | if (shouldEnemyBeMoved) {
175 | enemyAnimation.switchState("walk", true);
176 | enemyMove.mainDirection = MainDirection.Forward;
177 | return;
178 | } else {
179 | enemyMove.mainDirection = MainDirection.None;
180 | enemyMove.sideDirection = SideDirection.None;
181 | }
182 |
183 | if (shouldEnemyAttack) {
184 | enemyAnimation.switchState("attack", true);
185 | enemyAI.actionPassedTime += dt;
186 | } else {
187 | enemyAI.actionPassedTime = 0;
188 | }
189 |
190 | if (shouldEnemyDamage) {
191 | enemyAI.actionPassedTime = 0;
192 | playerHealth.current = Math.max(
193 | 0,
194 | playerHealth.current - enemyWeapon.attackDamage,
195 | );
196 | }
197 | }
198 |
199 | attackWithRange(dt: number, d: number, _: number, enemy: number) {
200 | const components = this.ecs.getComponents(enemy);
201 |
202 | const enemyAI = components.get(AIComponent);
203 | const enemyAngle = components.get(AngleComponent);
204 | const enemyWeapon = components.get(WeaponRangeComponent);
205 | const enemyAnimation = components.get(AnimatedSpriteComponent);
206 | const enemyPosition = components.get(PositionComponent);
207 | const enemyCircle = components.get(CircleComponent);
208 | const enemyMove = components.get(MoveComponent);
209 |
210 | const shouldEnemyBeMoved = enemyWeapon.attackDistance < d && d > 0;
211 |
212 | if (shouldEnemyBeMoved) {
213 | enemyAnimation.switchState("walk", true);
214 | enemyMove.mainDirection = MainDirection.Forward;
215 | return;
216 | } else {
217 | enemyMove.mainDirection = MainDirection.None;
218 | enemyMove.sideDirection = SideDirection.None;
219 | }
220 |
221 | const shouldEnemyAttack = enemyWeapon.attackDistance >= d;
222 | const shouldEnemyDamage =
223 | enemyAI.actionPassedTime >= enemyWeapon.attackFrequency / 1_000;
224 |
225 | if (shouldEnemyAttack) {
226 | enemyAnimation.switchState("attack", true);
227 | enemyAI.actionPassedTime += dt;
228 | } else {
229 | enemyAI.actionPassedTime = 0;
230 | }
231 |
232 | if (shouldEnemyDamage) {
233 | enemyAI.actionPassedTime = 0;
234 |
235 | const entity = this.ecs.addEntity();
236 | const sprite = this.textureManager.get(enemyWeapon.bulletSprite);
237 | const radius =
238 | enemyWeapon.attackDistance === 0 ? enemyCircle.radius : 0.25;
239 |
240 | this.ecs.addComponent(entity, new CollisionComponent());
241 |
242 | if (sprite) {
243 | this.ecs.addComponent(entity, new SpriteComponent(sprite));
244 | }
245 |
246 | this.ecs.addComponent(
247 | entity,
248 | new BulletComponent(enemy, enemyWeapon.bulletDamage),
249 | );
250 | this.ecs.addComponent(
251 | entity,
252 | new PositionComponent(enemyPosition.x, enemyPosition.y),
253 | );
254 | this.ecs.addComponent(entity, new AngleComponent(enemyAngle.angle));
255 | this.ecs.addComponent(entity, new CircleComponent(radius));
256 | this.ecs.addComponent(entity, new MinimapComponent("yellow"));
257 | this.ecs.addComponent(
258 | entity,
259 | new MoveComponent(
260 | enemyWeapon.bulletSpeed,
261 | false,
262 | MainDirection.Forward,
263 | ),
264 | );
265 |
266 | // one frame live entity
267 | if (enemyWeapon.attackDistance === 0) {
268 | this.ecs.removeEntity(entity);
269 | }
270 | }
271 | }
272 |
273 | destroy() {}
274 | }
275 |
--------------------------------------------------------------------------------
/src/lib/ecs/systems/AnimationSystem.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from "src/lib/ecs/Entity";
2 | import System from "src/lib/ecs/System";
3 | import AnimatedSpriteComponent from "src/lib/ecs/components/AnimatedSpriteComponent";
4 |
5 | export default class AnimationSystem extends System {
6 | public readonly componentsRequired = new Set([AnimatedSpriteComponent]);
7 |
8 | start(): void {}
9 |
10 | update(dt: number, entities: Set) {
11 | entities.forEach((entity) => {
12 | this.ecs.getComponents(entity).get(AnimatedSpriteComponent).update(dt);
13 | });
14 | }
15 |
16 | destroy(): void {}
17 | }
18 |
--------------------------------------------------------------------------------
/src/lib/ecs/systems/ControlSystem.ts:
--------------------------------------------------------------------------------
1 | import ECS from "src/lib/ecs";
2 | import { Entity } from "src/lib/ecs/Entity";
3 | import System from "src/lib/ecs/System";
4 | import MoveComponent, {
5 | MainDirection,
6 | SideDirection,
7 | } from "src/lib/ecs/components/MoveComponent";
8 | import RotateComponent from "src/lib/ecs/components/RotateComponent";
9 | import ControlComponent from "src/lib/ecs/components/ControlComponent";
10 | import PlayerComponent from "../components/PlayerComponent";
11 |
12 | const directionKeyCodes: Record = {
13 | KeyW: "up",
14 | KeyS: "down",
15 | KeyA: "left",
16 | KeyD: "right",
17 | };
18 |
19 | const weaponKeyCodes: Record = {
20 | Digit1: 1,
21 | Digit2: 2,
22 | Digit3: 3,
23 | Digit4: 4,
24 | };
25 |
26 | export default class ControlSystem extends System {
27 | componentsRequired = new Set([ControlComponent, MoveComponent, RotateComponent, PlayerComponent]);
28 |
29 | protected readonly container: HTMLElement;
30 |
31 | direction: Record = {
32 | up: false,
33 | down: false,
34 | left: false,
35 | right: false,
36 | };
37 |
38 | lastActiveWeapon: number = 0;
39 |
40 | pointerStartX: number | undefined;
41 | rotationFactor = 0;
42 | isPointerLocked = false;
43 |
44 | constructor(ecs: ECS, container: HTMLElement) {
45 | super(ecs);
46 |
47 | this.container = container;
48 | }
49 |
50 | start(): void {
51 | this.createListeners();
52 | this.requestPointerLock();
53 | }
54 |
55 | update(_: number, entities: Set) {
56 | entities.forEach((entity) => {
57 | const componentContainer = this.ecs.getComponents(entity);
58 | const playerComponent = componentContainer.get(PlayerComponent);
59 | const rotateComponent = componentContainer.get(RotateComponent);
60 | const moveComponent = componentContainer.get(MoveComponent);
61 |
62 | rotateComponent.rotationFactor = this.rotationFactor;
63 |
64 | if (this.direction.up) {
65 | moveComponent.mainDirection = MainDirection.Forward;
66 | } else if (this.direction.down) {
67 | moveComponent.mainDirection = MainDirection.Back;
68 | } else {
69 | moveComponent.mainDirection = MainDirection.None;
70 | }
71 |
72 | if (this.direction.left) {
73 | moveComponent.sideDirection = SideDirection.Left;
74 | } else if (this.direction.right) {
75 | moveComponent.sideDirection = SideDirection.Right;
76 | } else {
77 | moveComponent.sideDirection = SideDirection.None;
78 | }
79 |
80 | if (playerComponent.weapons[this.lastActiveWeapon] && playerComponent.currentWeapon !== playerComponent.weapons[this.lastActiveWeapon]) {
81 | playerComponent.currentWeapon = playerComponent.weapons[this.lastActiveWeapon];
82 | }
83 | });
84 |
85 | this.rotationFactor = 0;
86 | }
87 |
88 | destroy(): void {
89 | document.exitPointerLock();
90 | this.destroyListeners();
91 | }
92 |
93 | setDirection = (keyCode: string, status: boolean) => {
94 | const direction = directionKeyCodes[keyCode];
95 |
96 | if (!direction) {
97 | return;
98 | }
99 |
100 | this.direction[direction] = status;
101 | };
102 |
103 | setWeapon = (keyCode: string) => {
104 | const weaponCode = weaponKeyCodes[keyCode];
105 |
106 | if (!weaponCode) {
107 | return;
108 | }
109 |
110 | this.lastActiveWeapon = weaponCode;
111 | };
112 |
113 | handleDocumentKeyDown = (e: KeyboardEvent) => {
114 | this.setDirection(e.code, true);
115 | this.setWeapon(e.code);
116 | };
117 |
118 | handleDocumentKeyUp = (e: KeyboardEvent) => {
119 | this.setDirection(e.code, false);
120 | };
121 |
122 | handleDocumentMouseMove = (e: MouseEvent) => {
123 | this.rotationFactor = e.movementX;
124 | };
125 |
126 | handlePointerLockChange = () => {
127 | if (this.isPointerLocked) {
128 | document.addEventListener("mousemove", this.handleDocumentMouseMove);
129 | } else {
130 | document.removeEventListener("mousemove", this.handleDocumentMouseMove);
131 | }
132 |
133 | this.isPointerLocked = !this.isPointerLocked;
134 | };
135 |
136 | requestPointerLock = () => {
137 | this.container.requestPointerLock();
138 | this.isPointerLocked = true;
139 | };
140 |
141 | createListeners() {
142 | document.addEventListener("keydown", this.handleDocumentKeyDown);
143 | document.addEventListener("keyup", this.handleDocumentKeyUp);
144 | document.addEventListener("pointerlockchange", this.handlePointerLockChange);
145 | this.container.addEventListener("click", this.requestPointerLock);
146 | }
147 |
148 | destroyListeners() {
149 | document.removeEventListener("keydown", this.handleDocumentKeyDown);
150 | document.removeEventListener("keyup", this.handleDocumentKeyUp);
151 | document.removeEventListener("pointerlockchange", this.handlePointerLockChange);
152 | this.container.removeEventListener("click", this.requestPointerLock);
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/lib/ecs/systems/DoorsSystem.ts:
--------------------------------------------------------------------------------
1 | import System from "src/lib/ecs/System";
2 | import { Entity } from "src/lib/ecs/Entity";
3 | import DoorComponent from "src/lib/ecs/components/DoorComponent.ts";
4 | import PositionComponent from "src/lib/ecs/components/PositionComponent.ts";
5 | import PlayerComponent from "src/lib/ecs/components/PlayerComponent.ts";
6 | import BoxComponent from "src/lib/ecs/components/BoxComponent.ts";
7 | import { distance } from "src/lib/utils/math";
8 |
9 | export default class DoorsSystem extends System {
10 | public readonly componentsRequired = new Set([DoorComponent]);
11 |
12 | private doorsAnimations = new Map<
13 | Entity,
14 | { remainingAnimationTime: number }
15 | >();
16 |
17 | start(): void {}
18 | destroy(): void {}
19 |
20 | update(dt: number, entities: Set) {
21 | const [player] = this.ecs.query([PlayerComponent]);
22 | const playerContainer = this.ecs.getComponents(player);
23 |
24 | if (!playerContainer) {
25 | return;
26 | }
27 |
28 | const playerPosition = playerContainer.get(PositionComponent);
29 |
30 | entities.forEach((entity) => {
31 | const components = this.ecs.getComponents(entity);
32 | const door = components.get(DoorComponent);
33 | const doorBox = components.get(BoxComponent);
34 | const doorPosition = components.get(PositionComponent);
35 |
36 | if (!door || !doorPosition) {
37 | return;
38 | }
39 |
40 | const anim = this.doorsAnimations.get(entity);
41 | if (anim) {
42 | if (anim.remainingAnimationTime <= 0) {
43 | this.doorsAnimations.delete(entity);
44 | door.isOpened = !door.isOpened;
45 | door.offset = door.isOpened ? doorBox.size : 0;
46 | return;
47 | }
48 |
49 | const offset = (dt / door.animationTime) * doorBox.size;
50 | const axisOffset = door.isOpened ? -offset : offset;
51 | door.offset += axisOffset;
52 |
53 | anim.remainingAnimationTime = anim.remainingAnimationTime - dt;
54 | } else {
55 | const toPlayerDistance = distance(
56 | playerPosition.x,
57 | playerPosition.y,
58 | doorPosition.x,
59 | doorPosition.y,
60 | );
61 |
62 | if (
63 | (!door.isOpened && toPlayerDistance < 1.5) ||
64 | (door.isOpened && toPlayerDistance > 2.5)
65 | ) {
66 | this.doorsAnimations.set(entity, {
67 | remainingAnimationTime: door.animationTime,
68 | });
69 | }
70 | }
71 | });
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/lib/ecs/systems/LightSystem/index.ts:
--------------------------------------------------------------------------------
1 | import System from "src/lib/ecs/System.ts";
2 | import { Entity } from "src/lib/ecs/Entity.ts";
3 | import LightComponent from "src/lib/ecs/components/LightComponent.ts";
4 | import PositionComponent from "src/lib/ecs/components/PositionComponent.ts";
5 | import { ComponentContainer } from "src/lib/ecs/Component.ts";
6 | import MapTextureSystem from "src/lib/ecs/systems/MapTextureSystem.ts";
7 | import RenderSystem from "src/lib/ecs/systems/RenderSystem";
8 |
9 | import { LightCasting2D } from "./LightCasting2D.ts";
10 |
11 | export default class LightSystem extends System {
12 | public readonly componentsRequired = new Set([LightComponent]);
13 | private lastUpdateTime = 0;
14 | private updatePerSecond = 30;
15 | private quality = 16;
16 | private globalLightLevel = 0.1;
17 | private lightBias = 0.01;
18 | private existedLights = new Set();
19 | private listOfLightnings: {
20 | entity: number;
21 | cmp: LightComponent;
22 | lightCasting: LightCasting2D;
23 | pos: Vector2D;
24 | }[] = [];
25 |
26 | start(): void {
27 | this.ecs.onComponentAdd(LightComponent, (entity) => {
28 | const light = this.ecs.getComponents(entity).get(LightComponent);
29 | const pos = this.ecs.getComponents(entity).get(PositionComponent);
30 | this.listOfLightnings.push({
31 | entity,
32 | cmp: light,
33 | lightCasting: new LightCasting2D(light.distance, this.quality),
34 | pos,
35 | });
36 | this.existedLights.add(entity);
37 | });
38 | this.ecs.onComponentRemove(LightComponent, (entity) => {
39 | this.existedLights.delete(entity);
40 | this.listOfLightnings = this.listOfLightnings.filter(
41 | (lightCastingInstance) => lightCastingInstance.entity !== entity,
42 | );
43 | });
44 | }
45 |
46 | destroy(): void {
47 | this.listOfLightnings.length = 0;
48 | }
49 |
50 | update(_: number, entities: Set) {
51 | for (const entity of entities) {
52 | if (this.existedLights.has(entity)) continue;
53 | const lightCmp = this.ecs.getComponents(entity).get(LightComponent);
54 | const pos = this.ecs.getComponents(entity).get(PositionComponent);
55 | this.listOfLightnings.push({
56 | entity,
57 | cmp: lightCmp,
58 | lightCasting: new LightCasting2D(lightCmp.distance, this.quality),
59 | pos,
60 | });
61 | this.existedLights.add(entity);
62 | }
63 |
64 | if (Date.now() - this.lastUpdateTime > 1000 / this.updatePerSecond) {
65 | this.lastUpdateTime = Date.now();
66 |
67 | for (let i = 0; i < this.listOfLightnings.length; i++) {
68 | this.listOfLightnings[i].pos = this.ecs
69 | .getComponents(this.listOfLightnings[i].entity)
70 | .get(PositionComponent);
71 | this.updateLight(
72 | this.listOfLightnings[i].lightCasting,
73 | this.ecs.getComponents(this.listOfLightnings[i].entity),
74 | );
75 | }
76 | }
77 | }
78 |
79 | updateLight(
80 | lightCastingInstance: LightCasting2D,
81 | container: ComponentContainer,
82 | ) {
83 | const lightCmp = container.get(LightComponent);
84 | if (
85 | !lightCmp.isStaticLight ||
86 | lightCastingInstance.worldEdges.length === 0
87 | ) {
88 | const renderSystem = this.ecs.getSystem(RenderSystem)!;
89 |
90 | const map = this.ecs.getSystem(MapTextureSystem)!.textureMap;
91 | lightCastingInstance.vecEdges = map.toArray().flatMap((el) =>
92 | el.flatMap((mapEntity) => {
93 | if (!mapEntity) return [];
94 | const renderer = renderSystem.mapEntityRenders.find((render) =>
95 | render.canRender(mapEntity!),
96 | );
97 | if (!renderer) return [];
98 | return renderer.getArmature(mapEntity);
99 | }),
100 | );
101 | }
102 | if (
103 | !lightCmp.isStaticLight ||
104 | lightCastingInstance.vecVisibilityPolygonPoints.length === 0
105 | ) {
106 | const positionCmp = container.get(PositionComponent);
107 | lightCastingInstance.calculateVisibilityPolygon(
108 | positionCmp.x,
109 | positionCmp.y,
110 | );
111 | }
112 | }
113 |
114 | getLightingLevelForPoint(x: number, y: number) {
115 | let finalLightLevel = this.globalLightLevel;
116 | for (let i = 0; i < this.listOfLightnings.length; i++) {
117 | const val = this.listOfLightnings[i];
118 | const lightPower = val.lightCasting.getLightLevelInPoint(
119 | x + (val.pos.x - x) * this.lightBias,
120 | y + (val.pos.y - y) * this.lightBias,
121 | );
122 | const lightLevel = val.cmp.brightness * val.cmp.lightFn(lightPower);
123 | finalLightLevel += lightLevel;
124 | }
125 | return finalLightLevel;
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/lib/ecs/systems/MapItemSystem.ts:
--------------------------------------------------------------------------------
1 | import ECS from "src/lib/ecs";
2 | import System from "src/lib/ecs/System";
3 | import { Entity } from "src/lib/ecs/Entity";
4 | import SoundManager from "src/managers/SoundManager";
5 | import AnimationManager from "src/managers/AnimationManager";
6 | import ItemComponent from "src/lib/ecs/components/ItemComponent";
7 | import PlayerComponent from "src/lib/ecs/components/PlayerComponent";
8 | import PositionComponent from "src/lib/ecs/components/PositionComponent";
9 | import HealthComponent from "src/lib/ecs/components/HealthComponent";
10 | import WeaponRangeComponent from "src/lib/ecs/components/WeaponRangeComponent";
11 | import { WEAPON_PISTOL_INDEX } from "./WeaponSystem";
12 | import { generatePistolWeapon } from "src/levels/generators/components";
13 |
14 | export default class MapItemSystem extends System {
15 | public readonly componentsRequired = new Set([
16 | PositionComponent,
17 | ItemComponent,
18 | ]);
19 |
20 | protected readonly animationManager: AnimationManager;
21 | protected readonly soundManager: SoundManager;
22 |
23 | constructor(
24 | ecs: ECS,
25 | animationManager: AnimationManager,
26 | soundManager: SoundManager
27 | ) {
28 | super(ecs);
29 |
30 | this.animationManager = animationManager;
31 | this.soundManager = soundManager;
32 | }
33 |
34 | start(): void {}
35 |
36 | update(_: number, entities: Set) {
37 | const [player] = this.ecs.query([PlayerComponent, PositionComponent]);
38 |
39 | const playerContainer = this.ecs.getComponents(player);
40 |
41 | if (!playerContainer) {
42 | return;
43 | }
44 |
45 | const playerPlayer = playerContainer.get(PlayerComponent);
46 | const playerPosition = playerContainer.get(PositionComponent);
47 | const playerHealth = playerContainer.get(HealthComponent);
48 | const playerWeapon = playerContainer.get(WeaponRangeComponent);
49 |
50 | entities.forEach((entity) => {
51 | const entityItem = this.ecs.getComponents(entity).get(ItemComponent);
52 | const entityPosition = this.ecs
53 | .getComponents(entity)
54 | .get(PositionComponent);
55 |
56 | if (
57 | Math.floor(playerPosition.x) === Math.floor(entityPosition.x) &&
58 | Math.floor(playerPosition.y) === Math.floor(entityPosition.y)
59 | ) {
60 | if (!playerWeapon && entityItem.type === "pistol_weapon") {
61 | const pistolWeapon = generatePistolWeapon(this.animationManager);
62 |
63 | this.soundManager.playSound("pick");
64 |
65 | playerPlayer.currentWeapon = pistolWeapon;
66 |
67 | if (!playerPlayer.weapons[WEAPON_PISTOL_INDEX]) {
68 | playerPlayer.weapons[WEAPON_PISTOL_INDEX] = pistolWeapon;
69 | } else {
70 | (
71 | playerPlayer.weapons[WEAPON_PISTOL_INDEX] as WeaponRangeComponent
72 | ).bulletTotal += 30;
73 | }
74 |
75 | this.ecs.removeEntity(entity);
76 | }
77 |
78 | if (
79 | playerPlayer.weapons[WEAPON_PISTOL_INDEX] &&
80 | entityItem.type === "pistol_ammo"
81 | ) {
82 | this.soundManager.playSound("pick");
83 | (
84 | playerPlayer.weapons[WEAPON_PISTOL_INDEX] as WeaponRangeComponent
85 | ).bulletTotal += entityItem.value;
86 | this.ecs.removeEntity(entity);
87 | }
88 |
89 | if (playerHealth && entityItem.type === "health_pack") {
90 | playerHealth.current += entityItem.value;
91 | this.ecs.removeEntity(entity);
92 | }
93 | }
94 | });
95 | }
96 |
97 | destroy(): void {}
98 | }
99 |
--------------------------------------------------------------------------------
/src/lib/ecs/systems/MapPolarSystem.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from "src/lib/ecs/Entity";
2 | import System from "src/lib/ecs/System";
3 | import AnimatedSpriteComponent from "src/lib/ecs/components/AnimatedSpriteComponent";
4 | import CircleComponent from "src/lib/ecs/components/CircleComponent";
5 | import PlayerComponent from "src/lib/ecs/components/PlayerComponent";
6 | import PolarMap from "src/lib/ecs/lib/PolarMap";
7 | import PositionComponent from "src/lib/ecs/components/PositionComponent";
8 | import SpriteComponent from "src/lib/ecs/components/SpriteComponent";
9 |
10 | export default class MapPolarSystem extends System {
11 | public readonly componentsRequired = new Set([PositionComponent]);
12 | public readonly polarMap: PolarMap = new PolarMap();
13 |
14 | start(): void {}
15 |
16 | update(_: number, entities: Set) {
17 | const [player] = this.ecs.query([PlayerComponent, CircleComponent, PositionComponent]);
18 |
19 | const spriteContainers = [];
20 |
21 | for (const entity of entities) {
22 | if (
23 | this.ecs.getComponents(entity).has(AnimatedSpriteComponent) ||
24 | this.ecs.getComponents(entity).has(SpriteComponent)
25 | ) {
26 | spriteContainers.push(this.ecs.getComponents(entity));
27 | }
28 | }
29 |
30 | this.polarMap.center = this.ecs.getComponents(player);
31 | this.polarMap.entities = spriteContainers;
32 | this.polarMap.calculatePolarEntities();
33 | }
34 |
35 | destroy(): void {}
36 | }
37 |
--------------------------------------------------------------------------------
/src/lib/ecs/systems/MapTextureSystem.ts:
--------------------------------------------------------------------------------
1 | import ECS from "src/lib/ecs";
2 | import System from "src/lib/ecs/System";
3 | import { ComponentContainer } from "src/lib/ecs/Component";
4 | import PositionMap from "src/lib/ecs/lib/PositionMap";
5 | import PositionComponent from "src/lib/ecs/components/PositionComponent";
6 | import TextureComponent from "src/lib/ecs/components/TextureComponent";
7 |
8 | export default class MapTextureSystem extends System {
9 | public readonly componentsRequired = new Set([
10 | PositionComponent,
11 | TextureComponent,
12 | ]);
13 | public readonly textureMap: PositionMap;
14 |
15 | constructor(ecs: ECS, level: Level) {
16 | super(ecs);
17 |
18 | this.textureMap = new PositionMap(level.map);
19 | }
20 |
21 | start(): void {
22 | this.ecs.query([PositionComponent, TextureComponent]).forEach((entity) => {
23 | const container = this.ecs.getComponents(entity);
24 | const position = container.get(PositionComponent);
25 |
26 | this.textureMap.set(
27 | Math.floor(position.x),
28 | Math.floor(position.y),
29 | container
30 | );
31 | });
32 |
33 | this.ecs.onComponentAdd(PositionComponent, (entity) => {
34 | const container = this.ecs.getComponents(entity);
35 | const position = container.get(PositionComponent);
36 |
37 | if (!container.has(TextureComponent)) {
38 | return;
39 | }
40 |
41 | this.textureMap.set(
42 | Math.floor(position.x),
43 | Math.floor(position.y),
44 | container
45 | );
46 | });
47 |
48 | this.ecs.onComponentRemove(PositionComponent, (entity) => {
49 | const container = this.ecs.getComponents(entity);
50 | const position = container.get(PositionComponent);
51 |
52 | if (!container.has(TextureComponent)) {
53 | return;
54 | }
55 |
56 | this.textureMap.reset(Math.floor(position.x), Math.floor(position.y));
57 | });
58 | }
59 |
60 | update() {}
61 | destroy(): void {}
62 | }
63 |
--------------------------------------------------------------------------------
/src/lib/ecs/systems/MinimapSystem.ts:
--------------------------------------------------------------------------------
1 | import ECS from "src/lib/ecs";
2 | import Canvas from "src/lib/Canvas/DefaultCanvas";
3 | import System from "src/lib/ecs/System";
4 | import { Entity } from "src/lib/ecs/Entity";
5 | import BoxComponent from "src/lib/ecs/components/BoxComponent";
6 | import CircleComponent from "src/lib/ecs/components/CircleComponent";
7 | import MinimapComponent from "src/lib/ecs/components/MinimapComponent";
8 | import PositionComponent from "src/lib/ecs/components/PositionComponent";
9 | import RenderSystem from "src/lib/ecs/systems/RenderSystem";
10 |
11 | export default class MinimapSystem extends System {
12 | public readonly componentsRequired = new Set([
13 | MinimapComponent,
14 | PositionComponent,
15 | ]);
16 |
17 | protected readonly scale: number = 20;
18 | protected readonly canvas: Canvas;
19 | protected readonly offset: {
20 | top?: number;
21 | left?: number;
22 | bottom?: number;
23 | right?: number;
24 | } = { top: 0, left: 0, bottom: 10, right: 20 };
25 | protected readonly container: HTMLElement;
26 |
27 | constructor(ecs: ECS, container: HTMLElement, level: Level) {
28 | super(ecs);
29 |
30 | const cols = level.map[0].length;
31 | const rows = level.map.length;
32 |
33 | this.container = container;
34 |
35 | let style = "z-index: 3;position: absolute;";
36 | if (this.offset.right) style += `right: ${this.offset.right}px;`;
37 | if (this.offset.top) style += `top: ${this.offset.top}px;`;
38 | if (this.offset.left) style += `left: ${this.offset.left}px;`;
39 | if (this.offset.bottom) style += `bottom: ${this.offset.bottom}px;`;
40 | this.canvas = new Canvas({
41 | id: "minimap",
42 | height: rows * this.scale,
43 | width: cols * this.scale,
44 | style,
45 | });
46 | }
47 |
48 | start() {
49 | this.container.appendChild(this.canvas.element);
50 | }
51 |
52 | update(_: number, entities: Set) {
53 | this.canvas.clear();
54 | this.canvas.drawBackground("green");
55 |
56 | const renderSystem = this.ecs.getSystem(RenderSystem)!;
57 |
58 | for (const entity of entities) {
59 | const components = this.ecs.getComponents(entity);
60 | const { color } = components.get(MinimapComponent);
61 |
62 | const renderer = renderSystem.mapEntityRenders.find((render) =>
63 | render.canRender(components!),
64 | );
65 | if (renderer) {
66 | const edges = renderer.getArmature(components);
67 | this.drawPolygon(edges, color);
68 | continue;
69 | }
70 |
71 | const { x, y } = components.get(PositionComponent);
72 | if (components.has(BoxComponent)) {
73 | const { size } = components.get(BoxComponent);
74 | this.drawSquare(x, y, size, color);
75 | }
76 |
77 | if (components.has(CircleComponent)) {
78 | const { radius } = components.get(CircleComponent);
79 | this.drawCircle(x, y, radius, color);
80 | }
81 | }
82 | }
83 |
84 | destroy(): void {
85 | this.canvas.element.remove();
86 | }
87 |
88 | drawPolygon(paths: number[], color: string) {
89 | this.canvas.drawPolygon({
90 | paths: paths.map((point) => point * this.scale),
91 | color,
92 | });
93 | }
94 |
95 | drawSquare(x: number, y: number, size: number, color: string) {
96 | this.canvas.drawRect({
97 | x: x * this.scale,
98 | y: y * this.scale,
99 | width: size * this.scale,
100 | height: size * this.scale,
101 | color,
102 | });
103 | }
104 |
105 | drawCircle(x: number, y: number, radius: number, color: string) {
106 | this.canvas.drawCircle({
107 | x: x * this.scale,
108 | y: y * this.scale,
109 | radius: radius * this.scale,
110 | color,
111 | });
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/lib/ecs/systems/MoveSystem.ts:
--------------------------------------------------------------------------------
1 | import { degreeToRadians } from "src/lib/utils/angle";
2 | import { Entity } from "src/lib/ecs/Entity";
3 | import System from "src/lib/ecs/System";
4 | import AngleComponent from "src/lib/ecs/components/AngleComponent";
5 | import MoveComponent from "src/lib/ecs/components/MoveComponent";
6 | import PositionComponent from "src/lib/ecs/components/PositionComponent";
7 | import CollisionComponent from "src/lib/ecs/components/CollisionComponent";
8 | import MapTextureSystem from "./MapTextureSystem";
9 | import { ComponentContainer } from "src/lib/ecs/Component.ts";
10 | import DoorComponent from "src/lib/ecs/components/DoorComponent.ts";
11 |
12 | export default class MoveSystem extends System {
13 | public readonly componentsRequired = new Set([
14 | PositionComponent,
15 | AngleComponent,
16 | MoveComponent,
17 | CollisionComponent,
18 | ]);
19 |
20 | start(): void {}
21 |
22 | destroy(): void {}
23 |
24 | update(dt: number, entities: Set) {
25 | entities.forEach((entity) => {
26 | this.move(dt, entity);
27 | });
28 | }
29 |
30 | protected move(dt: number, entity: Entity) {
31 | const components = this.ecs.getComponents(entity);
32 | const angleComponent = components.get(AngleComponent);
33 | const positionComponent = components.get(PositionComponent);
34 | const collisionComponent = components.get(CollisionComponent);
35 | const moveComponent = components.get(MoveComponent);
36 |
37 | const m = Number(moveComponent.mainDirection);
38 | const s = Number(moveComponent.sideDirection) * -1;
39 |
40 | if (m || s) {
41 | const mainAngle = degreeToRadians(angleComponent.angle - 360);
42 | const mainCos = Math.cos(mainAngle);
43 | const mainSin = Math.sin(mainAngle);
44 |
45 | const sideAngle = degreeToRadians(angleComponent.angle - 90);
46 | const sideCos = Math.cos(sideAngle);
47 | const sideSin = Math.sin(sideAngle);
48 |
49 | let newX =
50 | positionComponent.x +
51 | (m * mainCos + s * sideCos) * moveComponent.moveSpeed * dt;
52 | let newY =
53 | positionComponent.y +
54 | (m * mainSin + s * sideSin) * moveComponent.moveSpeed * dt;
55 |
56 | const { collidedX, collidedY, collidedWith } = this.getCollision(
57 | positionComponent,
58 | new PositionComponent(newX, newY),
59 | );
60 |
61 | const hasCollision = collidedX || collidedY;
62 |
63 | if (!hasCollision) {
64 | positionComponent.x = newX;
65 | positionComponent.y = newY;
66 | return;
67 | }
68 |
69 | if (collidedWith) {
70 | collisionComponent.collidedEntity = collidedWith;
71 | collisionComponent.isCollided = true;
72 | }
73 |
74 | if (moveComponent.canSlide) {
75 | if (!collidedX) {
76 | positionComponent.x = newX;
77 | } else {
78 | newX = positionComponent.x;
79 | }
80 |
81 | if (!collidedY) {
82 | positionComponent.y = newY;
83 | } else {
84 | newY = positionComponent.y;
85 | }
86 |
87 | positionComponent.x = newX;
88 | positionComponent.y = newY;
89 | }
90 | }
91 | }
92 |
93 | private getCollision(
94 | currentPos: PositionComponent,
95 | nexPos: PositionComponent,
96 | ) {
97 | const textureMap = this.ecs.getSystem(MapTextureSystem)!.textureMap;
98 |
99 | let collidedWith: ComponentContainer | undefined = undefined;
100 | let collidedX = false;
101 | let collidedY = false;
102 |
103 | if (nexPos.x <= 0 || nexPos.x > textureMap.cols) {
104 | collidedX = true;
105 | }
106 |
107 | const collideWithTextureByX = textureMap.get(
108 | Math.floor(nexPos.x),
109 | Math.floor(currentPos.y),
110 | );
111 |
112 | if (collideWithTextureByX && this.isCollidedEntity(collideWithTextureByX)) {
113 | collidedX = true;
114 | collidedWith = collideWithTextureByX;
115 | }
116 |
117 | if (nexPos.y <= 0 || nexPos.y > textureMap.rows) {
118 | collidedY = true;
119 | }
120 |
121 | const collideWithTextureByY = textureMap.get(
122 | Math.floor(currentPos.x),
123 | Math.floor(nexPos.y),
124 | );
125 | if (collideWithTextureByY && this.isCollidedEntity(collideWithTextureByY)) {
126 | collidedY = true;
127 | collidedWith = collideWithTextureByY;
128 | }
129 |
130 | return { collidedX, collidedY, collidedWith };
131 | }
132 |
133 | private isCollidedEntity(entityContainer: ComponentContainer): boolean {
134 | const doorCmp = entityContainer.get(DoorComponent);
135 | if (doorCmp) return !doorCmp.isOpened;
136 | return true;
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/lib/ecs/systems/RenderSystem/EntityRenders/DoorRender.ts:
--------------------------------------------------------------------------------
1 | import { ComponentContainer } from "src/lib/ecs/Component.ts";
2 | import TextureComponent from "src/lib/ecs/components/TextureComponent.ts";
3 | import DoorComponent from "src/lib/ecs/components/DoorComponent.ts";
4 | import PositionComponent from "src/lib/ecs/components/PositionComponent.ts";
5 | import { EntityRender } from "src/lib/ecs/systems/RenderSystem/EntityRenders/IEntityRender.ts";
6 |
7 | import { Vec2D } from "src/lib/utils/math";
8 | import { degreeToRadians, normalizeAngleInRad } from "src/lib/utils/angle";
9 |
10 | export class DoorRender extends EntityRender {
11 | private doorWidth = 0.1;
12 |
13 | canRender(mapEntity: ComponentContainer): boolean {
14 | return mapEntity.all([TextureComponent, DoorComponent]);
15 | }
16 |
17 | getArmature(mapEntity: ComponentContainer) {
18 | const pos = mapEntity.get(PositionComponent);
19 | const door = mapEntity.get(DoorComponent);
20 | const halfDoorWidth = this.doorWidth / 2;
21 | if (!door.isVertical) {
22 | return [
23 | // top
24 | pos.x + door.offset,
25 | pos.y + 0.5 - halfDoorWidth,
26 | pos.x + 1,
27 | pos.y + 0.5 - halfDoorWidth,
28 | // bottom
29 | pos.x + door.offset,
30 | pos.y + 0.5 + halfDoorWidth,
31 | pos.x + 1,
32 | pos.y + 0.5 + halfDoorWidth,
33 | // left
34 | pos.x + door.offset,
35 | pos.y + 0.5 - halfDoorWidth,
36 | pos.x + door.offset,
37 | pos.y + 0.5 + halfDoorWidth,
38 | // right
39 | pos.x + 1,
40 | pos.y + 0.5 - halfDoorWidth,
41 | pos.x + 1,
42 | pos.y + 0.5 + halfDoorWidth,
43 | ];
44 | }
45 | return [
46 | // top
47 | pos.x + 0.5 - halfDoorWidth,
48 | pos.y + door.offset,
49 | pos.x + 0.5 + halfDoorWidth,
50 | pos.y + door.offset,
51 | // bottom
52 | pos.x + 0.5 - halfDoorWidth,
53 | pos.y + 1,
54 | pos.x + 0.5 + halfDoorWidth,
55 | pos.y + 1,
56 | // left
57 | pos.x + 0.5 - halfDoorWidth,
58 | pos.y + door.offset,
59 | pos.x + 0.5 - halfDoorWidth,
60 | pos.y + 1,
61 | // right
62 | pos.x + 0.5 + halfDoorWidth,
63 | pos.y + door.offset,
64 | pos.x + 0.5 + halfDoorWidth,
65 | pos.y + 1,
66 | ];
67 | }
68 |
69 | render(
70 | mapEntity: ComponentContainer,
71 | screenHeight: number,
72 | rayAngle: number,
73 | side: number,
74 | sideDistX: number,
75 | sideDistY: number,
76 | deltaDistX: number,
77 | deltaDistY: number,
78 | mapX: number,
79 | mapY: number,
80 | playerPos: Vector2D,
81 | stepX: number,
82 | stepY: number,
83 | rayDirX: number,
84 | rayDirY: number,
85 | fishEyeFixCoef: number,
86 | ) {
87 | const doorCmp = mapEntity.get(DoorComponent);
88 | const doorPosition = mapEntity.get(PositionComponent);
89 |
90 | let doorPositionX = doorPosition.x;
91 | let doorPositionY = doorPosition.y;
92 | if (side === 1) {
93 | doorPositionX += doorCmp.offset;
94 | } else {
95 | doorPositionY += doorCmp.offset;
96 | }
97 |
98 | const isDoorOpened =
99 | side === 1 ? doorPositionX >= mapX + 1 : doorPositionY >= mapY + 1;
100 | if (isDoorOpened) return;
101 |
102 | const isHitWall =
103 | side === 1
104 | ? sideDistY - deltaDistY * (0.5 + this.doorWidth / 2) > sideDistX
105 | : sideDistX - deltaDistX * (0.5 + this.doorWidth / 2) > sideDistY;
106 |
107 | if (isHitWall) return;
108 |
109 | let hitDoorPart: "frontFace" | "sideFace" | null = null;
110 |
111 | let offset =
112 | side === 0
113 | ? stepX * (0.5 - this.doorWidth / 2)
114 | : stepY * (0.5 - this.doorWidth / 2);
115 |
116 | const perpWallDistOnFrontFace = this.calculatePerpWallDist(
117 | side,
118 | mapX,
119 | mapY,
120 | playerPos,
121 | stepX,
122 | stepY,
123 | rayDirX,
124 | rayDirY,
125 | offset,
126 | );
127 |
128 | let rayX = playerPos.x + rayDirX * perpWallDistOnFrontFace;
129 | let rayY = playerPos.y + rayDirY * perpWallDistOnFrontFace;
130 |
131 | // check front hit closing/opening door
132 | const isHitDoor = side === 1 ? rayX > doorPositionX : rayY > doorPositionY;
133 |
134 | if (isHitDoor) {
135 | hitDoorPart = "frontFace";
136 | }
137 |
138 | if (!hitDoorPart) {
139 | const rayAngleRad = degreeToRadians(rayAngle);
140 | if (side === 1) {
141 | const closestDoorCornetPoint = {
142 | x: doorPositionX,
143 | y: mapY + 0.5 - this.doorWidth / 2,
144 | };
145 | const angToClosestCorner = normalizeAngleInRad(
146 | Math.atan2(
147 | closestDoorCornetPoint.y - playerPos.y,
148 | closestDoorCornetPoint.x - playerPos.x,
149 | ),
150 | );
151 | const angleToFaresCorner = normalizeAngleInRad(
152 | Math.atan2(
153 | closestDoorCornetPoint.y + this.doorWidth - playerPos.y,
154 | closestDoorCornetPoint.x - playerPos.x,
155 | ),
156 | );
157 | if (
158 | rayAngleRad > angToClosestCorner &&
159 | rayAngleRad < angleToFaresCorner
160 | ) {
161 | hitDoorPart = "sideFace";
162 | const angle =
163 | rayAngleRad < Math.PI ? rayAngleRad : rayAngleRad - Math.PI;
164 |
165 | const leg = doorPositionX - rayX;
166 | const hypotenuse = leg / Math.cos(angle);
167 | offset += Math.sqrt(hypotenuse ** 2 - leg ** 2) * stepY;
168 | } else {
169 | return;
170 | }
171 | } else {
172 | const closestDoorCornetPoint = {
173 | x: mapX + 0.5 - this.doorWidth / 2,
174 | y: doorPositionY,
175 | };
176 | const angToClosestCorner = Math.atan2(
177 | closestDoorCornetPoint.y - playerPos.y,
178 | closestDoorCornetPoint.x - playerPos.x,
179 | );
180 | const angleToFaresCorner = Math.atan2(
181 | closestDoorCornetPoint.y - playerPos.y,
182 | closestDoorCornetPoint.x + this.doorWidth - playerPos.x,
183 | );
184 | if (
185 | rayAngleRad > angleToFaresCorner &&
186 | rayAngleRad < angToClosestCorner
187 | ) {
188 | hitDoorPart = "sideFace";
189 | const leg = doorPositionY - rayY;
190 | const hypotenuse = leg / Math.sin(rayAngleRad);
191 | offset += Math.sqrt(hypotenuse ** 2 - leg ** 2) * stepX;
192 | } else {
193 | return;
194 | }
195 | }
196 | }
197 |
198 | if (!hitDoorPart) return;
199 |
200 | const perpWallDist = this.calculatePerpWallDist(
201 | side,
202 | mapX,
203 | mapY,
204 | playerPos,
205 | stepX,
206 | stepY,
207 | rayDirX,
208 | rayDirY,
209 | offset,
210 | );
211 |
212 | rayX = playerPos.x + rayDirX * perpWallDist;
213 | rayY = playerPos.y + rayDirY * perpWallDist;
214 |
215 | // Determine texture offset based on the door's orientation
216 | const textureOffset = doorCmp.isVertical
217 | ? Vec2D.from(rayX - doorPositionX, Math.floor(rayY) - doorPositionY)
218 | : Vec2D.from(Math.floor(rayX) - doorPositionX, rayY - doorPositionY);
219 |
220 | const texture = mapEntity.get(TextureComponent).texture;
221 |
222 | // Calculate texture X-coordinate
223 | const texturePositionX =
224 | hitDoorPart === "frontFace"
225 | ? Math.floor(
226 | (texture.width *
227 | (rayX + textureOffset.x + rayY + textureOffset.y)) %
228 | texture.width,
229 | )
230 | : 0;
231 |
232 | // Correct the fish-eye effect
233 | const correctedDist = perpWallDist * fishEyeFixCoef;
234 | const wallHeight = Math.floor(screenHeight / 2 / correctedDist);
235 |
236 | return {
237 | rayX,
238 | rayY,
239 | texturePositionX: texturePositionX,
240 | texture: texture,
241 | entityHeight: wallHeight,
242 | distance: correctedDist,
243 | };
244 | }
245 |
246 | calculatePerpWallDist(
247 | side: number,
248 | mapX: number,
249 | mapY: number,
250 | startRayPos: Vector2D,
251 | stepX: number,
252 | stepY: number,
253 | rayDirX: number,
254 | rayDirY: number,
255 | offset: number = 0,
256 | ) {
257 | return side === 0
258 | ? Math.abs((mapX - startRayPos.x + offset + (1 - stepX) / 2) / rayDirX)
259 | : Math.abs((mapY - startRayPos.y + offset + (1 - stepY) / 2) / rayDirY);
260 | }
261 | }
262 |
--------------------------------------------------------------------------------
/src/lib/ecs/systems/RenderSystem/EntityRenders/IEntityRender.ts:
--------------------------------------------------------------------------------
1 | import { ComponentContainer } from "src/lib/ecs/Component.ts";
2 |
3 | export type RenderLineInfo = {
4 | rayX: number;
5 | rayY: number;
6 | distance: number;
7 | texturePositionX: number;
8 | texture: TextureBitmap;
9 | entityHeight: number;
10 | };
11 |
12 | /** ArmatureEdge = [sx1, sy1, ex1, ey1, sx2, sy2, ex2, ey2...] */
13 | export type ArmatureEdge = number[];
14 |
15 | export abstract class EntityRender {
16 | abstract canRender(mapEntity: ComponentContainer): boolean;
17 |
18 | abstract getArmature(mapEntity: ComponentContainer): ArmatureEdge;
19 |
20 | abstract render(
21 | mapEntity: ComponentContainer,
22 | screenHeight: number,
23 | rayAngle: number,
24 | side: number,
25 | sideDistX: number,
26 | sideDistY: number,
27 | deltaDistX: number,
28 | deltaDistY: number,
29 | mapX: number,
30 | mapY: number,
31 | playerPos: Vector2D,
32 | stepX: number,
33 | stepY: number,
34 | rayDirX: number,
35 | rayDirY: number,
36 | fishEyeFixCoef: number,
37 | ): RenderLineInfo | undefined;
38 | }
39 |
--------------------------------------------------------------------------------
/src/lib/ecs/systems/RenderSystem/EntityRenders/WallRender.ts:
--------------------------------------------------------------------------------
1 | import { EntityRender } from "src/lib/ecs/systems/RenderSystem/EntityRenders/IEntityRender.ts";
2 | import { ComponentContainer } from "src/lib/ecs/Component.ts";
3 | import TextureComponent from "src/lib/ecs/components/TextureComponent.ts";
4 | import PositionComponent from "src/lib/ecs/components/PositionComponent.ts";
5 |
6 | export class WallRender extends EntityRender {
7 | canRender(mapEntity: ComponentContainer): boolean {
8 | return mapEntity.has(TextureComponent);
9 | }
10 |
11 | getArmature(mapEntity: ComponentContainer) {
12 | const pos = mapEntity.get(PositionComponent);
13 | return [
14 | pos.x,
15 | pos.y,
16 | pos.x + 1,
17 | pos.y,
18 | pos.x,
19 | pos.y,
20 | pos.x,
21 | pos.y + 1,
22 | pos.x + 1,
23 | pos.y + 1,
24 | pos.x,
25 | pos.y + 1,
26 | pos.x + 1,
27 | pos.y + 1,
28 | pos.x + 1,
29 | pos.y,
30 | ];
31 | }
32 |
33 | render(
34 | mapEntity: ComponentContainer,
35 | screenHeight: number,
36 | _rayAngle: number,
37 | side: number,
38 | _sideDistX: number,
39 | _sideDistY: number,
40 | _deltaDistX: number,
41 | _deltaDistY: number,
42 | mapX: number,
43 | mapY: number,
44 | playerPos: Vector2D,
45 | stepX: number,
46 | stepY: number,
47 | rayDirX: number,
48 | rayDirY: number,
49 | fishEyeFixCoef: number,
50 | ) {
51 | const perpWallDist =
52 | side === 0
53 | ? (mapX - playerPos.x + (1 - stepX) / 2) / rayDirX
54 | : (mapY - playerPos.y + (1 - stepY) / 2) / rayDirY;
55 |
56 | // Correct the fish-eye effect
57 | const correctedDist = perpWallDist * fishEyeFixCoef;
58 |
59 | const wallHeight = Math.floor(screenHeight / 2 / correctedDist);
60 |
61 | const rayX = playerPos.x + rayDirX * perpWallDist;
62 | const rayY = playerPos.y + rayDirY * perpWallDist;
63 |
64 | const texture = mapEntity.get(TextureComponent).texture;
65 | const texturePositionX = Math.floor(
66 | (texture.width * (rayX + rayY)) % texture.width,
67 | );
68 |
69 | return {
70 | texturePositionX: texturePositionX,
71 | texture: texture,
72 | entityHeight: wallHeight,
73 | rayX,
74 | rayY,
75 | distance: correctedDist,
76 | };
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/lib/ecs/systems/RotateSystem.ts:
--------------------------------------------------------------------------------
1 | import System from "src/lib/ecs/System";
2 | import { Entity } from "src/lib/ecs/Entity";
3 | import { normalizeAngle } from "src/lib/utils/angle";
4 | import AngleComponent from "src/lib/ecs/components/AngleComponent";
5 | import RotateComponent from "src/lib/ecs/components/RotateComponent";
6 |
7 | export default class RotateSystem extends System {
8 | public readonly componentsRequired = new Set([
9 | AngleComponent,
10 | RotateComponent,
11 | ]);
12 |
13 | start(): void {}
14 |
15 | destroy(): void {}
16 |
17 | update(dt: number, entities: Set) {
18 | entities.forEach((entity) => {
19 | const components = this.ecs.getComponents(entity);
20 | const angleComponent = components.get(AngleComponent);
21 | const { rotationFactor, rotationSpeed } = components.get(RotateComponent);
22 |
23 | angleComponent.angle = normalizeAngle(
24 | angleComponent.angle + rotationFactor * rotationSpeed * dt,
25 | );
26 | });
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/lib/image.ts:
--------------------------------------------------------------------------------
1 | export async function extractTextureBitmap(url: string) {
2 | const image = await loadImage(url);
3 | const imageData = await extractImageData(image);
4 | const colors = await extractColors(image.height, image.width, imageData.data);
5 |
6 | return {
7 | height: image.height,
8 | width: image.width,
9 | colors,
10 | } as TextureBitmap;
11 | }
12 |
13 | async function loadImage(url: string): Promise {
14 | return new Promise((resolve, reject) => {
15 | const element = document.createElement("img");
16 |
17 | element.src = url;
18 | element.onerror = () => reject();
19 | element.onload = () => resolve(element);
20 | });
21 | }
22 |
23 | async function extractImageData(image: HTMLImageElement) {
24 | const canvas = document.createElement("canvas");
25 | canvas.width = image.width;
26 | canvas.height = image.height;
27 |
28 | const context = canvas.getContext("2d")!;
29 |
30 | context.drawImage(image, 0, 0, image.width, image.height);
31 |
32 | return context.getImageData(0, 0, image.width, image.height);
33 | }
34 |
35 | async function extractColors(
36 | height: number,
37 | width: number,
38 | imageData: Uint8ClampedArray
39 | ) {
40 | const colors: Color[][] = [];
41 | for (let y = 0; y < height; y++) {
42 | const row: Color[] = [];
43 | for (let x = 0; x < width; x++) {
44 | const i = x * 4 + y * width * 4;
45 | row.push({
46 | r: imageData[i],
47 | g: imageData[i + 1],
48 | b: imageData[i + 2],
49 | a: imageData[i + 3],
50 | });
51 | }
52 | colors.push(row);
53 | }
54 | return colors;
55 | }
56 |
--------------------------------------------------------------------------------
/src/lib/loop.ts:
--------------------------------------------------------------------------------
1 | export interface Loop {
2 | play: () => void;
3 | pause: () => void;
4 | checkRunning: () => boolean;
5 | }
6 |
7 | export default function createLoop(cb: (dt: number) => void): Loop {
8 | let isRunning: boolean = false;
9 | let previousTime: number;
10 |
11 | function play() {
12 | previousTime = performance.now();
13 | isRunning = true;
14 |
15 | function loop() {
16 | if (!isRunning) {
17 | return;
18 | }
19 | const currentTime = performance.now();
20 | const dt = (currentTime - previousTime) / 1000;
21 |
22 | cb(dt);
23 |
24 | previousTime = currentTime;
25 |
26 | requestAnimationFrame(loop);
27 | }
28 |
29 | requestAnimationFrame(loop);
30 | }
31 |
32 | function pause() {
33 | isRunning = false;
34 | }
35 |
36 | function checkRunning() {
37 | return isRunning;
38 | }
39 |
40 | return {
41 | play,
42 | pause,
43 | checkRunning,
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/lib/scenario.ts:
--------------------------------------------------------------------------------
1 | import ECS from "src/lib/ecs";
2 | import AIComponent from "src/lib/ecs/components/AIComponent";
3 | import AngleComponent from "src/lib/ecs/components/AngleComponent";
4 | import AnimatedSpriteComponent from "src/lib/ecs/components/AnimatedSpriteComponent";
5 | import AnimationManager from "src/managers/AnimationManager";
6 | import BoxComponent from "src/lib/ecs/components/BoxComponent";
7 | import CameraComponent from "src/lib/ecs/components/CameraComponent";
8 | import CircleComponent from "src/lib/ecs/components/CircleComponent";
9 | import ControlComponent from "src/lib/ecs/components/ControlComponent";
10 | import EnemyComponent from "src/lib/ecs/components/EnemyComponent";
11 | import HealthComponent from "src/lib/ecs/components/HealthComponent";
12 | import MinimapComponent from "src/lib/ecs/components/MinimapComponent";
13 | import MoveComponent from "src/lib/ecs/components/MoveComponent";
14 | import PositionComponent from "src/lib/ecs/components/PositionComponent";
15 | import RotateComponent from "src/lib/ecs/components/RotateComponent";
16 | import TextureComponent from "src/lib/ecs/components/TextureComponent";
17 | import TextureManager from "src/managers/TextureManager";
18 | import WeaponMeleeComponent from "./ecs/components/WeaponMeleeComponent";
19 | import WeaponRangeComponent from "./ecs/components/WeaponRangeComponent";
20 | import CollisionComponent from "./ecs/components/CollisionComponent";
21 | import SpriteComponent from "./ecs/components/SpriteComponent";
22 | import PlayerComponent from "./ecs/components/PlayerComponent";
23 | import ItemComponent from "./ecs/components/ItemComponent";
24 | import DoorComponent from "src/lib/ecs/components/DoorComponent.ts";
25 | import LightComponent from "src/lib/ecs/components/LightComponent.ts";
26 | import {
27 | WEAPON_KNIFE_INDEX,
28 | WEAPON_PISTOL_INDEX,
29 | } from "./ecs/systems/WeaponSystem";
30 | import {
31 | generateKnifeWeapon,
32 | generatePistolWeapon,
33 | } from "src/levels/generators/components";
34 |
35 | export function createLevelEntities(
36 | ecs: ECS,
37 | level: Level,
38 | playerState: PlayerState,
39 | textureManager: TextureManager,
40 | animationManager: AnimationManager,
41 | ) {
42 | const player = ecs.addEntity();
43 |
44 | const playerHealth = playerState.health || level.player.health;
45 | const playerComponent = new PlayerComponent();
46 |
47 | const knifeWeapon = generateKnifeWeapon(animationManager);
48 |
49 | playerComponent.currentWeapon = knifeWeapon;
50 | playerComponent.weapons[WEAPON_KNIFE_INDEX] = knifeWeapon;
51 |
52 | if (playerState.ammo) {
53 | const pistolWeapon = generatePistolWeapon(
54 | animationManager,
55 | playerState.ammo,
56 | );
57 |
58 | playerComponent.currentWeapon = pistolWeapon;
59 | playerComponent.weapons[WEAPON_PISTOL_INDEX] = pistolWeapon;
60 | }
61 |
62 | ecs.addComponent(player, playerComponent);
63 | ecs.addComponent(player, new LightComponent(5, 0.5));
64 | ecs.addComponent(player, new ControlComponent());
65 | ecs.addComponent(player, new CircleComponent(0.4));
66 | ecs.addComponent(
67 | player,
68 | new PositionComponent(level.player.x, level.player.y),
69 | );
70 | ecs.addComponent(player, new HealthComponent(playerHealth, playerHealth));
71 | ecs.addComponent(player, new AngleComponent(level.player.angle));
72 | ecs.addComponent(player, new MoveComponent(3, true));
73 | ecs.addComponent(player, new CollisionComponent());
74 | ecs.addComponent(player, new RotateComponent(360 / 30));
75 | ecs.addComponent(player, new CameraComponent(60));
76 | ecs.addComponent(player, new MinimapComponent("black"));
77 |
78 | // items
79 | level.items?.forEach((item) => {
80 | const entity = ecs.addEntity();
81 |
82 | ecs.addComponent(entity, new PositionComponent(item.x, item.y));
83 | ecs.addComponent(entity, new PositionComponent(item.x, item.y));
84 | ecs.addComponent(entity, new CircleComponent(item.radius));
85 | ecs.addComponent(entity, new MinimapComponent("orange"));
86 |
87 | ecs.addComponent(
88 | entity,
89 | new SpriteComponent(textureManager.get(item.type)),
90 | );
91 | ecs.addComponent(entity, new ItemComponent(item.type, item.value));
92 | });
93 |
94 | // enemies
95 | level.enemies?.forEach((enemy) => {
96 | const entity = ecs.addEntity();
97 |
98 | ecs.addComponent(entity, new MoveComponent(1, true));
99 | ecs.addComponent(entity, new CollisionComponent());
100 | ecs.addComponent(entity, new EnemyComponent());
101 | ecs.addComponent(entity, new CircleComponent(enemy.radius));
102 | ecs.addComponent(entity, new PositionComponent(enemy.x, enemy.y));
103 | ecs.addComponent(entity, new HealthComponent(enemy.health, enemy.health));
104 | ecs.addComponent(entity, new AngleComponent(enemy.angle));
105 | ecs.addComponent(entity, new RotateComponent());
106 | ecs.addComponent(entity, new MinimapComponent("red"));
107 |
108 | if (enemy.aiDistance) {
109 | ecs.addComponent(entity, new AIComponent(enemy.aiDistance));
110 | }
111 |
112 | if (enemy.rangeWeapon) {
113 | ecs.addComponent(
114 | entity,
115 | new WeaponRangeComponent({
116 | bulletSprite: enemy.rangeWeapon.bulletSprite,
117 | bulletTotal: Infinity,
118 | bulletDamage: enemy.rangeWeapon.bulletDamage,
119 | bulletSpeed: enemy.rangeWeapon.bulletSpeed,
120 | attackDistance: enemy.rangeWeapon.attackDistance,
121 | attackFrequency: enemy.rangeWeapon.attackFrequency,
122 | }),
123 | );
124 | }
125 |
126 | if (enemy.meleeWeapon) {
127 | ecs.addComponent(
128 | entity,
129 | new WeaponMeleeComponent({
130 | attackDamage: enemy.meleeWeapon.damage,
131 | attackFrequency: enemy.meleeWeapon.frequency,
132 | }),
133 | );
134 | }
135 |
136 | switch (enemy.type) {
137 | case "zombie":
138 | ecs.addComponent(
139 | entity,
140 | new AnimatedSpriteComponent("idle", {
141 | attack: animationManager.get("zombieAttack"),
142 | idle: animationManager.get("zombieIdle"),
143 | damage: animationManager.get("zombieDamage"),
144 | death: animationManager.get("zombieDeath"),
145 | walk: animationManager.get("zombieWalk"),
146 | }),
147 | );
148 | break;
149 | case "soldier":
150 | ecs.addComponent(
151 | entity,
152 | new AnimatedSpriteComponent("idle", {
153 | attack: animationManager.get("soldierAttack"),
154 | idle: animationManager.get("soldierIdle"),
155 | damage: animationManager.get("soldierDamage"),
156 | death: animationManager.get("soldierDeath"),
157 | walk: animationManager.get("soldierWalk"),
158 | }),
159 | );
160 | break;
161 | case "slayer":
162 | ecs.addComponent(
163 | entity,
164 | new AnimatedSpriteComponent("idle", {
165 | attack: animationManager.get("slayerAttack"),
166 | idle: animationManager.get("slayerIdle"),
167 | damage: animationManager.get("slayerDamage"),
168 | death: animationManager.get("slayerDeath"),
169 | walk: animationManager.get("slayerWalk"),
170 | }),
171 | );
172 | break;
173 | case "flyguy":
174 | ecs.addComponent(
175 | entity,
176 | new AnimatedSpriteComponent("idle", {
177 | attack: animationManager.get("flyguyAttack"),
178 | idle: animationManager.get("flyguyIdle"),
179 | damage: animationManager.get("flyguyDamage"),
180 | death: animationManager.get("flyguyDeath"),
181 | walk: animationManager.get("flyguyWalk"),
182 | }),
183 | );
184 | break;
185 | case "commando":
186 | ecs.addComponent(
187 | entity,
188 | new AnimatedSpriteComponent("idle", {
189 | attack: animationManager.get("commandoAttack"),
190 | idle: animationManager.get("commandoIdle"),
191 | damage: animationManager.get("commandoDamage"),
192 | death: animationManager.get("commandoDeath"),
193 | walk: animationManager.get("commandoWalk"),
194 | }),
195 | );
196 | break;
197 | case "tank":
198 | ecs.addComponent(
199 | entity,
200 | new AnimatedSpriteComponent("idle", {
201 | attack: animationManager.get("tankAttack"),
202 | idle: animationManager.get("tankIdle"),
203 | damage: animationManager.get("tankDamage"),
204 | death: animationManager.get("tankDeath"),
205 | walk: animationManager.get("tankWalk"),
206 | }),
207 | );
208 | break;
209 | }
210 | });
211 |
212 | // exit
213 |
214 | if (level.endingScenario.name === "exit") {
215 | const exit = ecs.addEntity();
216 |
217 | ecs.addComponent(exit, new BoxComponent(1));
218 | ecs.addComponent(
219 | exit,
220 | new PositionComponent(
221 | level.endingScenario.position.x,
222 | level.endingScenario.position.y,
223 | ),
224 | );
225 | ecs.addComponent(exit, new MinimapComponent("yellow"));
226 | }
227 |
228 | // walls
229 | level.map.forEach((row, y) => {
230 | row.forEach((col, x) => {
231 | const mapItem = level.mapEntities[col];
232 | if (mapItem.type === "empty") {
233 | return;
234 | }
235 |
236 | if (mapItem.type === "light") {
237 | const light = ecs.addEntity();
238 | ecs.addComponent(light, new LightComponent(4, 1));
239 | ecs.addComponent(light, new PositionComponent(x + 0.5, y + 0.5));
240 | ecs.addComponent(light, new MinimapComponent("white"));
241 | ecs.addComponent(light, new CircleComponent(0.1));
242 | return;
243 | }
244 | const mapItemEntity = ecs.addEntity();
245 | const texture = textureManager.get(mapItem.texture);
246 |
247 | ecs.addComponent(mapItemEntity, new BoxComponent(1));
248 | ecs.addComponent(mapItemEntity, new PositionComponent(x, y));
249 | ecs.addComponent(mapItemEntity, new TextureComponent(texture));
250 |
251 | if (mapItem.type === "wall") {
252 | ecs.addComponent(mapItemEntity, new MinimapComponent("grey"));
253 | } else if (mapItem.type === "door") {
254 | const [aboveBloc, underBloc] = [
255 | level.map[y - 1][x],
256 | level.map[y + 1][x],
257 | ];
258 | const isVerticalDoor =
259 | level.mapEntities[aboveBloc]?.type === "wall" &&
260 | level.mapEntities[underBloc]?.type === "wall";
261 | ecs.addComponent(
262 | mapItemEntity,
263 | new DoorComponent(false, isVerticalDoor),
264 | );
265 |
266 | ecs.addComponent(mapItemEntity, new MinimapComponent("blue"));
267 | }
268 | });
269 | });
270 | }
271 |
--------------------------------------------------------------------------------
/src/lib/utils/angle.ts:
--------------------------------------------------------------------------------
1 | export function degreeToRadians(degree: number) {
2 | return ((degree * Math.PI) / 180) % (2 * Math.PI);
3 | }
4 |
5 | export function radiansToDegrees(radians: number) {
6 | return (180 * radians) / Math.PI;
7 | }
8 |
9 | export function angle(x1: number, y1: number, x2: number, y2: number): number {
10 | const angleRadians = Math.atan2(y2 - y1, x2 - x1);
11 | const angleDegrees = angleRadians * (180 / Math.PI);
12 |
13 | return angleDegrees < 0 ? angleDegrees + 360 : angleDegrees;
14 | }
15 |
16 | export function normalizeAngle(a: number) {
17 | return (a + 360) % 360;
18 | }
19 |
20 | export function normalizeAngleInRad(a: number) {
21 | return (a + 2 * Math.PI) % (2 * Math.PI);
22 | }
23 |
--------------------------------------------------------------------------------
/src/lib/utils/color.ts:
--------------------------------------------------------------------------------
1 | export function overlayColor(
2 | baseColor: Color,
3 | overlayColor: Color,
4 | coverageRatio: number,
5 | ): Color {
6 | if (baseColor.a === 0) {
7 | return baseColor;
8 | }
9 |
10 | const invCoverageRatio = 1 - coverageRatio;
11 | const effectiveAlpha = overlayColor.a * coverageRatio;
12 |
13 | return {
14 | r: baseColor.r * invCoverageRatio + overlayColor.r * effectiveAlpha,
15 | g: baseColor.g * invCoverageRatio + overlayColor.g * effectiveAlpha,
16 | b: baseColor.b * invCoverageRatio + overlayColor.b * effectiveAlpha,
17 | a: baseColor.a + overlayColor.a * coverageRatio * invCoverageRatio,
18 | };
19 | }
20 |
21 | export function applyBrightness(color: Color, lightLevel?: number) {
22 | if (lightLevel === undefined) return color;
23 | return {
24 | r: Math.min(color.r * lightLevel, 255),
25 | g: Math.min(color.g * lightLevel, 255),
26 | b: Math.min(color.b * lightLevel, 255),
27 | a: color.a,
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/src/lib/utils/geometry.ts:
--------------------------------------------------------------------------------
1 | function distSquare(x: number, y: number, x2: number, y2: number) {
2 | return Math.pow(x - x2, 2) + Math.pow(y - y2, 2);
3 | }
4 |
5 | export function getDistance(sx: number, sy: number, ex: number, ey: number) {
6 | return Math.sqrt(distSquare(sx, sy, ex, ey));
7 | }
8 |
9 | export function getLineIntersectPoint(
10 | x1: number,
11 | y1: number,
12 | x2: number,
13 | y2: number,
14 | x3: number,
15 | y3: number,
16 | x4: number,
17 | y4: number,
18 | ) {
19 | if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) return false;
20 | const denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
21 | if (denominator === 0) return false;
22 | const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
23 | const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;
24 | if (ua < 0 || ua > 1 || ub < 0 || ub > 1) return false;
25 | return { x: x1 + ua * (x2 - x1), y: y1 + ua * (y2 - y1) };
26 | }
27 |
28 | export function isLinesHasIntersections(
29 | x1: number,
30 | y1: number,
31 | x2: number,
32 | y2: number,
33 | x3: number,
34 | y3: number,
35 | x4: number,
36 | y4: number,
37 | ) {
38 | if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) return false;
39 | const denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
40 | if (denominator === 0) return false;
41 | const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
42 | const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;
43 | return !(ua < 0 || ua > 1 || ub < 0 || ub > 1);
44 | }
45 |
46 | export function distToSegment(
47 | x: number,
48 | y: number,
49 | sx: number,
50 | sy: number,
51 | ex: number,
52 | ey: number,
53 | ) {
54 | const l2 = distSquare(sx, sy, ex, ey);
55 | if (l2 === 0) return getDistance(x, y, sx, sy);
56 | let t = ((x - sx) * (ex - sx) + (y - sy) * (ey - sy)) / l2;
57 | t = Math.max(0, Math.min(1, t));
58 | return getDistance(x, y, sx + t * (ex - sx), sy + t * (ey - sy));
59 | }
60 |
61 | export function getDistanceFrom2DPointToLine(
62 | pointX: number,
63 | pointY: number,
64 | pointOnLineX: number,
65 | pointOnLineY: number,
66 | anotherPointOnLineX: number,
67 | anotherPointOnLineY: number,
68 | ) {
69 | const A = anotherPointOnLineY - pointOnLineY;
70 | const B = pointOnLineX - anotherPointOnLineX;
71 | const C =
72 | anotherPointOnLineX * pointOnLineY - pointOnLineX * anotherPointOnLineY;
73 | return Math.abs(A * pointX + B * pointY + C) / Math.sqrt(A * A + B * B);
74 | }
75 |
76 | export function is2DPointInTriangle(
77 | pointX: number,
78 | pointY: number,
79 | triangleP1x: number,
80 | triangleP1y: number,
81 | triangleP2x: number,
82 | triangleP2y: number,
83 | triangleP3x: number,
84 | triangleP3y: number,
85 | ) {
86 | const a =
87 | (triangleP1x - pointX) * (triangleP2y - triangleP1y) -
88 | (triangleP2x - triangleP1x) * (triangleP1y - pointY);
89 | const b =
90 | (triangleP2x - pointX) * (triangleP3y - triangleP2y) -
91 | (triangleP3x - triangleP2x) * (triangleP2y - pointY);
92 | const c =
93 | (triangleP3x - pointX) * (triangleP1y - triangleP3y) -
94 | (triangleP1x - triangleP3x) * (triangleP3y - pointY);
95 | return (a >= 0 && b >= 0 && c >= 0) || (a <= 0 && b <= 0 && c <= 0);
96 | }
97 |
98 | export function isSquareIntersectTriangle(
99 | sx: number,
100 | sy: number,
101 | sSize: number,
102 | t1x: number,
103 | t1y: number,
104 | t2x: number,
105 | t2y: number,
106 | t3x: number,
107 | t3y: number,
108 | ) {
109 | // prettier-ignore
110 | return (
111 | isLinesHasIntersections(sx, sy, sx + sSize, sy, t1x, t1y, t2x, t2y) ||
112 | isLinesHasIntersections(sx + sSize, sy, sx + sSize, sy + sSize, t2x, t2y, t3x, t3y,) ||
113 | isLinesHasIntersections(sx + sSize, sy + sSize, sx, sy + sSize, t3x, t3y, t1x, t1y,) ||
114 | isLinesHasIntersections(sx, sy + sSize, sx, sy, t1x, t1y, t2x, t2y) ||
115 | isLinesHasIntersections(sx, sy, sx + sSize, sy, t2x, t2y, t3x, t3y) ||
116 | isLinesHasIntersections(sx, sy, sx + sSize, sy + sSize, t3x, t3y, t1x, t1y,) ||
117 | isLinesHasIntersections(sx + sSize, sy, sx + sSize, sy + sSize, t1x, t1y, t2x, t2y,) ||
118 | isLinesHasIntersections(sx + sSize, sy + sSize, sx, sy + sSize, t2x, t2y, t3x, t3y,) ||
119 | isLinesHasIntersections(sx + sSize, sy + sSize, sx, sy, t3x, t3y, t1x, t1y)
120 | );
121 | }
122 |
--------------------------------------------------------------------------------
/src/lib/utils/math.ts:
--------------------------------------------------------------------------------
1 | export function minmax(value: number, min: number, max: number) {
2 | return Math.max(min, Math.min(value, max));
3 | }
4 |
5 | export function distance(
6 | x1: number,
7 | y1: number,
8 | x2: number,
9 | y2: number,
10 | ): number {
11 | return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
12 | }
13 |
14 | export function lerp(from: number, to: number, percent: number) {
15 | const val = minmax(percent, 0, 1);
16 | return from + (to - from) * val;
17 | }
18 |
19 | export class Vec2D implements Vector2D {
20 | private constructor(
21 | public x: number,
22 | public y: number,
23 | ) {}
24 |
25 | static from(x: number, y: number) {
26 | return new Vec2D(x, y);
27 | }
28 | static fromObj(cords: Vector2D) {
29 | return new Vec2D(cords.x, cords.y);
30 | }
31 |
32 | static zeros() {
33 | return new Vec2D(0, 0);
34 | }
35 |
36 | add(vec: Vector2D): Vec2D {
37 | return Vec2D.from(this.x + vec.x, this.y + vec.y);
38 | }
39 |
40 | addX(x: number): Vec2D {
41 | return Vec2D.from(this.x + x, this.y);
42 | }
43 | addY(y: number): Vec2D {
44 | return Vec2D.from(this.x, this.y + y);
45 | }
46 |
47 | subtract(vec: Vector2D): Vec2D {
48 | return Vec2D.from(this.x - vec.x, this.y - vec.y);
49 | }
50 |
51 | subtractX(x: number): Vec2D {
52 | return Vec2D.from(this.x - x, this.y);
53 | }
54 |
55 | subtractY(y: number): Vec2D {
56 | return Vec2D.from(this.x, this.y - y);
57 | }
58 |
59 | divide(vec: Vector2D): Vec2D {
60 | return Vec2D.from(this.x / vec.x, this.y / vec.y);
61 | }
62 |
63 | multiply(vec: Vector2D): Vec2D {
64 | return Vec2D.from(this.x * vec.x, this.y * vec.y);
65 | }
66 |
67 | magnitude(): number {
68 | return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
69 | }
70 |
71 | unit() {
72 | const mag = this.magnitude();
73 | return Vec2D.from(this.x / mag, this.y / mag);
74 | }
75 |
76 | isEqual(vec: Vector2D) {
77 | return this.x === vec.x && this.y === vec.y;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import * as presets from "./presets.ts";
2 |
3 | import SoundManager from "./managers/SoundManager.ts";
4 | import TextureManager from "./managers/TextureManager.ts";
5 | import AnimationManager from "./managers/AnimationManager.ts";
6 | import { createScenario } from "./scenario.ts"
7 |
8 | const container = document.getElementById('app')!;
9 | const soundManager = new SoundManager();
10 | const textureManager = new TextureManager();
11 | const animationManager = new AnimationManager();
12 |
13 | window.onload = async () => {
14 | try {
15 | await Promise.all([
16 | await soundManager.load(presets.sounds),
17 | await textureManager.load([...presets.textures, ...presets.sprites]),
18 | await animationManager.load(presets.animation),
19 | ]);
20 |
21 | container.innerHTML = '';
22 |
23 | createScenario({
24 | container,
25 | soundManager,
26 | textureManager,
27 | animationManager,
28 | });
29 |
30 | } catch (err) {
31 | console.warn(err);
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/src/managers/AnimationManager.ts:
--------------------------------------------------------------------------------
1 | import { extractTextureBitmap } from "src/lib/image";
2 |
3 | export default class AnimationManager {
4 | private animations: Record = {};
5 |
6 | async load(presets: AnimationSpritePreset[]) {
7 | for (const preset of presets) {
8 | this.animations[preset.id] = await Promise.all(
9 | preset.frames.map(async url => await extractTextureBitmap(url))
10 | );
11 | }
12 | }
13 |
14 | get(id: string) {
15 | return this.animations[id];
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/managers/SoundManager.ts:
--------------------------------------------------------------------------------
1 | export default class SoundManager {
2 | protected sounds: Record = {};
3 | protected currentBackgroundId: string = ''
4 | protected isMuted: boolean = false;
5 |
6 | async load(presets: SoundPreset[]) {
7 | await Promise.all(presets.map(preset => new Promise((resolve, reject) => {
8 | const audio = new Audio(preset.url);
9 |
10 | audio.onload = () => resolve(void 0);
11 | audio.onabort = () => reject();
12 | audio.onerror = () => reject();
13 | audio.load();
14 |
15 | resolve(void 0);
16 |
17 | this.sounds[preset.id] = audio;
18 | })));
19 | }
20 |
21 | checkMuted() {
22 | return this.isMuted;
23 | }
24 |
25 | mute() {
26 | this.isMuted = true;
27 | this.pauseBackground();
28 | }
29 |
30 | unmute() {
31 | this.isMuted = false;
32 | this.resumeBackground();
33 | }
34 |
35 | playSound(id: string, volume: number = 1) {
36 | const audio = this.sounds[id];
37 | const copy = audio.cloneNode() as HTMLAudioElement;
38 |
39 | copy.volume = volume;
40 | copy.play();
41 | }
42 |
43 | playBackground(id: string) {
44 | this.currentBackgroundId = id;
45 |
46 | this.sounds[id].loop = true;
47 | this.sounds[id].play();
48 | }
49 |
50 | resumeBackground() {
51 | if (this.sounds[this.currentBackgroundId]) {
52 | this.sounds[this.currentBackgroundId].play();
53 | }
54 | }
55 |
56 | pauseBackground() {
57 | if (this.sounds[this.currentBackgroundId]) {
58 | this.sounds[this.currentBackgroundId].pause();
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/managers/TextureManager.ts:
--------------------------------------------------------------------------------
1 | import { extractTextureBitmap } from "src/lib/image";
2 |
3 | export default class TextureManager {
4 | private textures: Record = {};
5 |
6 | async load(presets: TexturePreset[]) {
7 | for (const preset of presets) {
8 | this.textures[preset.id] = await extractTextureBitmap(preset.url);
9 | }
10 | }
11 |
12 | get(id: string) {
13 | return this.textures[id];
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/presets.ts:
--------------------------------------------------------------------------------
1 | export const textures: TexturePreset[] = [
2 | {
3 | id: "TECH_1C",
4 | url: "./assets/textures/TECH_1C.PNG"
5 | },
6 | {
7 | id: "TECH_1E",
8 | url: "./assets/textures/TECH_1E.PNG"
9 | },
10 | {
11 | id: "TECH_2F",
12 | url: "./assets/textures/TECH_2F.PNG"
13 | },
14 | {
15 | id: "TECH_3B",
16 | url: "./assets/textures/TECH_3B.PNG"
17 | },
18 | {
19 | id: "TECH_4E",
20 | url: "./assets/textures/TECH_4E.PNG"
21 | },
22 | {
23 | id: "TECH_4F",
24 | url: "./assets/textures/TECH_4F.PNG",
25 | },
26 | {
27 | id: "DOOR_1A",
28 | url: "./assets/textures/DOOR_1A.PNG",
29 | },
30 | {
31 | id: "DOOR_1C",
32 | url: "./assets/textures/DOOR_1C.PNG",
33 | },
34 | {
35 | id: "DOOR_1E",
36 | url: "./assets/textures/DOOR_1E.PNG",
37 | },
38 | {
39 | id: "floor",
40 | url: "./assets/textures/FLOOR_1A.PNG",
41 | }
42 | ];
43 |
44 | export const sprites: SpritePreset[] = [
45 | {
46 | id: 'health_pack',
47 | url: './assets/items/health_pack.png'
48 | },
49 | {
50 | id: 'pistol_weapon',
51 | url: './assets/items/pistol_weapon.png'
52 | },
53 | {
54 | id: 'pistol_ammo',
55 | url: './assets/items/pistol_ammo.png'
56 | },
57 | {
58 | id: 'pistol_bullet',
59 | url: './assets/weapons/pistol_bullet.png'
60 | },
61 | {
62 | id: 'shotgun_bullet',
63 | url: './assets/weapons/shotgun_bullet.gif'
64 | },
65 | ];
66 |
67 | export const animation: AnimationSpritePreset[] = [
68 | {
69 | id: "knifeIdle",
70 | frames: [
71 | "./assets/weapons/knife_1.png",
72 | ],
73 | },
74 | {
75 | id: "knifeAttack",
76 | frames: [
77 | "./assets/weapons/knife_1.png",
78 | "./assets/weapons/knife_2.png",
79 | "./assets/weapons/knife_3.png",
80 | "./assets/weapons/knife_4.png",
81 | "./assets/weapons/knife_5.png",
82 | ],
83 | },
84 | {
85 | id: "pistolIdle",
86 | frames: [
87 | "./assets/weapons/pistol_1.png",
88 | ],
89 | },
90 | {
91 | id: "pistolAttack",
92 | frames: [
93 | "./assets/weapons/pistol_1.png",
94 | "./assets/weapons/pistol_2.png",
95 | "./assets/weapons/pistol_3.png",
96 | "./assets/weapons/pistol_4.png",
97 | "./assets/weapons/pistol_5.png",
98 | ],
99 | },
100 | {
101 | id: "zombieIdle",
102 | frames: [
103 | "./assets/characters/ZombieIdle.png",
104 | ],
105 | },
106 | {
107 | id: "zombieWalk",
108 | frames: [
109 | "./assets/characters/ZombieWalk1.png",
110 | "./assets/characters/ZombieWalk2.png",
111 | "./assets/characters/ZombieWalk3.png",
112 | "./assets/characters/ZombieWalk4.png",
113 | ],
114 | },
115 | {
116 | id: "zombieDamage",
117 | frames: [
118 | "./assets/characters/ZombieDamage1.png",
119 | "./assets/characters/ZombieDamage2.png",
120 | ],
121 | },
122 | {
123 | id: "zombieDeath",
124 | frames: [
125 | "./assets/characters/ZombieDeath1.png",
126 | "./assets/characters/ZombieDeath2.png",
127 | "./assets/characters/ZombieDeath3.png",
128 | "./assets/characters/ZombieDeath4.png",
129 | ],
130 | },
131 | {
132 | id: "zombieAttack",
133 | frames: [
134 | "./assets/characters/ZombieAttack1.png",
135 | "./assets/characters/ZombieAttack2.png",
136 | ],
137 | },
138 | // flyguy
139 | {
140 | id: "flyguyIdle",
141 | frames: [
142 | "./assets/characters/FlyguyIdle.png",
143 | ],
144 | },
145 | {
146 | id: "flyguyWalk",
147 | frames: [
148 | "./assets/characters/FlyguyWalk1.png",
149 | "./assets/characters/FlyguyWalk2.png",
150 | "./assets/characters/FlyguyWalk3.png",
151 | "./assets/characters/FlyguyWalk4.png",
152 | ],
153 | },
154 | {
155 | id: "flyguyDamage",
156 | frames: [
157 | "./assets/characters/FlyguyDamage1.png",
158 | "./assets/characters/FlyguyDamage2.png",
159 | ],
160 | },
161 | {
162 | id: "flyguyDeath",
163 | frames: [
164 | "./assets/characters/FlyguyDeath1.png",
165 | "./assets/characters/FlyguyDeath2.png",
166 | "./assets/characters/FlyguyDeath3.png",
167 | "./assets/characters/FlyguyDeath4.png",
168 | ],
169 | },
170 | {
171 | id: "flyguyAttack",
172 | frames: [
173 | "./assets/characters/FlyguyAttack1.png",
174 | "./assets/characters/FlyguyAttack2.png",
175 | ],
176 | },
177 | // soldier
178 | {
179 | id: "soldierIdle",
180 | frames: [
181 | "./assets/characters/SoldierIdle.png",
182 | ],
183 | },
184 | {
185 | id: "soldierWalk",
186 | frames: [
187 | "./assets/characters/SoldierWalk1.png",
188 | "./assets/characters/SoldierWalk2.png",
189 | "./assets/characters/SoldierWalk3.png",
190 | "./assets/characters/SoldierWalk4.png",
191 | ],
192 | },
193 | {
194 | id: "soldierDamage",
195 | frames: [
196 | "./assets/characters/SoldierDamage1.png",
197 | "./assets/characters/SoldierDamage2.png",
198 | ],
199 | },
200 | {
201 | id: "soldierDeath",
202 | frames: [
203 | "./assets/characters/SoldierDeath1.png",
204 | "./assets/characters/SoldierDeath2.png",
205 | "./assets/characters/SoldierDeath3.png",
206 | "./assets/characters/SoldierDeath4.png",
207 | ],
208 | },
209 | {
210 | id: "soldierAttack",
211 | frames: [
212 | "./assets/characters/SoldierAttack1.png",
213 | "./assets/characters/SoldierAttack2.png",
214 | ],
215 | },
216 | // commando
217 | {
218 | id: "commandoIdle",
219 | frames: [
220 | "./assets/characters/CommandoIdle.png",
221 | ],
222 | },
223 | {
224 | id: "commandoWalk",
225 | frames: [
226 | "./assets/characters/CommandoWalk1.png",
227 | "./assets/characters/CommandoWalk2.png",
228 | "./assets/characters/CommandoWalk3.png",
229 | "./assets/characters/CommandoWalk4.png",
230 | ],
231 | },
232 | {
233 | id: "commandoDamage",
234 | frames: [
235 | "./assets/characters/CommandoDamage1.png",
236 | "./assets/characters/CommandoDamage2.png",
237 | ],
238 | },
239 | {
240 | id: "commandoDeath",
241 | frames: [
242 | "./assets/characters/CommandoDeath1.png",
243 | "./assets/characters/CommandoDeath2.png",
244 | "./assets/characters/CommandoDeath3.png",
245 | "./assets/characters/CommandoDeath4.png",
246 | ],
247 | },
248 | {
249 | id: "commandoAttack",
250 | frames: [
251 | "./assets/characters/CommandoAttack1.png",
252 | "./assets/characters/CommandoAttack2.png",
253 | ],
254 | },
255 | // tank
256 | {
257 | id: "tankIdle",
258 | frames: [
259 | "./assets/characters/TankIdle.png",
260 | ],
261 | },
262 | {
263 | id: "tankWalk",
264 | frames: [
265 | "./assets/characters/TankWalk1.png",
266 | "./assets/characters/TankWalk2.png",
267 | "./assets/characters/TankWalk3.png",
268 | "./assets/characters/TankWalk4.png",
269 | ],
270 | },
271 | {
272 | id: "tankDamage",
273 | frames: [
274 | "./assets/characters/TankDamage1.png",
275 | "./assets/characters/TankDamage2.png",
276 | ],
277 | },
278 | {
279 | id: "tankDeath",
280 | frames: [
281 | "./assets/characters/TankDeath1.png",
282 | "./assets/characters/TankDeath2.png",
283 | "./assets/characters/TankDeath3.png",
284 | "./assets/characters/TankDeath4.png",
285 | ],
286 | },
287 | {
288 | id: "tankAttack",
289 | frames: [
290 | "./assets/characters/TankAttack1.png",
291 | "./assets/characters/TankAttack2.png",
292 | ],
293 | },
294 | ];
295 |
296 | export const sounds: SoundPreset[] = [
297 | ...[
298 | 'hurt',
299 | 'pick',
300 | 'gun-shot',
301 | 'lazer-shot',
302 | 'attack-zombie',
303 | 'attack-knife',
304 | ].map(id => ({
305 | id,
306 | url: `./assets/sounds/${id}.mp3`,
307 | volume: 1,
308 | })),
309 | ...[
310 | //'dead-lift-yeti',
311 | 'heavy-duty-zoo',
312 | //'on-the-edge-reakt',
313 | //'scorcher-abbynoise',
314 | 'shocking-red-abbynoise',
315 | 'zombie-world-alex-besss',
316 | ].map(id => ({
317 | id,
318 | url: `./assets/music/${id}.mp3`,
319 | volume: 0.8,
320 | })),
321 | ];
322 |
--------------------------------------------------------------------------------
/src/scenario.ts:
--------------------------------------------------------------------------------
1 | import levels from "./levels";
2 | import AnimationManager from "./managers/AnimationManager";
3 | import SoundManager from "./managers/SoundManager";
4 | import TextureManager from "./managers/TextureManager";
5 |
6 | import LevelScene from "./scenes/LevelScene";
7 | import TitleScene from "./scenes/TitleScene";
8 |
9 | interface ScenarioProps {
10 | container: HTMLElement;
11 | soundManager: SoundManager;
12 | textureManager: TextureManager;
13 | animationManager: AnimationManager;
14 | }
15 |
16 | export function createScenario({
17 | container,
18 | soundManager,
19 | textureManager,
20 | animationManager,
21 | }: ScenarioProps) {
22 | let levelIndex = 0;
23 |
24 | const playerState: PlayerState = {
25 | health: 100,
26 | };
27 |
28 | const showFinalScene = () => {
29 | const scene = new TitleScene(container, playerState, "Congratulation!", ["You survived a zombie invasion"]);
30 | scene.start();
31 | };
32 |
33 | const showFailedScene = () => {
34 | const scene = new TitleScene(container, playerState, "You died");
35 | scene.start();
36 | };
37 |
38 | const switchToLevelNextScene = (playerState: PlayerState) => {
39 | const level = levels[levelIndex];
40 |
41 | if (!level) {
42 | showFinalScene();
43 | return;
44 | }
45 |
46 | levelIndex++;
47 |
48 | const scene = new LevelScene({
49 | level,
50 | container,
51 | soundManager,
52 | textureManager,
53 | animationManager,
54 | playerState,
55 | });
56 |
57 | scene.onComplete(() => {
58 | if (level.music) {
59 | soundManager.pauseBackground();
60 | }
61 | scene.destroy();
62 | switchToLevelNextScene(scene.playerState);
63 | });
64 |
65 | scene.onFailed(() => {
66 | if (level.music) {
67 | soundManager.pauseBackground();
68 | }
69 | scene.destroy();
70 | showFailedScene();
71 | });
72 |
73 | scene.start();
74 |
75 | if (level.music) {
76 | soundManager.playBackground(level.music);
77 | }
78 | };
79 |
80 | const startScene = new TitleScene(container, playerState, "Shoot or run",
81 | [
82 | "Use WASD and mouse to play",
83 | "Use M to mute",
84 | "",
85 | "Press any key to start"
86 | ]);
87 |
88 | startScene.onComplete(() => {
89 | startScene.destroy();
90 | switchToLevelNextScene(playerState);
91 | });
92 |
93 | startScene.start();
94 | }
95 |
--------------------------------------------------------------------------------
/src/scenes/BaseScene.ts:
--------------------------------------------------------------------------------
1 | export default interface BaseScene {
2 | playerState: PlayerState;
3 | onComplete(cb: () => void): void;
4 | start(): void;
5 | destroy(): void;
6 | }
--------------------------------------------------------------------------------
/src/scenes/LevelScene.ts:
--------------------------------------------------------------------------------
1 | import BaseScene from "./BaseScene";
2 | import ECS from "src/lib/ecs";
3 | import createLoop, { Loop } from "src/lib/loop.ts";
4 | import { createLevelEntities } from "src/lib/scenario";
5 | import SoundManager from "src/managers/SoundManager";
6 | import AISystem from "src/lib/ecs/systems/AISystem";
7 | import AnimationManager from "src/managers/AnimationManager";
8 | import AnimationSystem from "src/lib/ecs/systems/AnimationSystem";
9 | import ControlSystem from "src/lib/ecs/systems/ControlSystem";
10 | import HealthComponent from "src/lib/ecs/components/HealthComponent";
11 | import MinimapSystem from "src/lib/ecs/systems/MinimapSystem";
12 | import MoveSystem from "src/lib/ecs/systems/MoveSystem";
13 | import PositionComponent from "src/lib/ecs/components/PositionComponent";
14 | import RenderSystem from "src/lib/ecs/systems/RenderSystem";
15 | import RotateSystem from "src/lib/ecs/systems/RotateSystem";
16 | import TextureManager from "src/managers/TextureManager";
17 | import WeaponSystem, {
18 | WEAPON_PISTOL_INDEX,
19 | } from "src/lib/ecs/systems/WeaponSystem";
20 | import MapItemSystem from "src/lib/ecs/systems/MapItemSystem";
21 | import MapPolarSystem from "src/lib/ecs/systems/MapPolarSystem";
22 | import MapTextureSystem from "src/lib/ecs/systems/MapTextureSystem";
23 | import EnemyComponent from "src/lib/ecs/components/EnemyComponent";
24 | import PlayerComponent from "src/lib/ecs/components/PlayerComponent";
25 | import WeaponRangeComponent from "src/lib/ecs/components/WeaponRangeComponent";
26 | import LevelPlayerView from "src/views/LevelPlayerView";
27 | import DoorsSystem from "src/lib/ecs/systems/DoorsSystem.ts";
28 | import LightSystem from "src/lib/ecs/systems/LightSystem";
29 |
30 | const KEY_CONTROL_PAUSE = "KeyM";
31 |
32 | interface LevelSceneProps {
33 | container: HTMLElement;
34 | level: Level;
35 | playerState: PlayerState;
36 | soundManager: SoundManager;
37 | textureManager: TextureManager;
38 | animationManager: AnimationManager;
39 | }
40 |
41 | export default class LevelScene implements BaseScene {
42 | public playerState: PlayerState;
43 |
44 | protected onCompleteCallback?: () => void;
45 | protected onFailedCallback?: () => void;
46 |
47 | protected readonly level: Level;
48 | protected readonly loop: Loop;
49 | protected readonly ecs: ECS;
50 | protected startedAt: number = +new Date();
51 |
52 | private timeLeft?: number;
53 | private levelPlayerView: LevelPlayerView;
54 | private soundManager: SoundManager;
55 |
56 | constructor({
57 | container,
58 | level,
59 | soundManager,
60 | textureManager,
61 | animationManager,
62 | playerState,
63 | }: LevelSceneProps) {
64 | this.level = level;
65 | this.playerState = playerState;
66 |
67 | this.timeLeft =
68 | level.endingScenario.name === "survive"
69 | ? level.endingScenario?.timer
70 | : undefined;
71 | this.soundManager = soundManager;
72 | this.levelPlayerView = new LevelPlayerView(container);
73 |
74 | const ecs = new ECS();
75 |
76 | createLevelEntities(
77 | ecs,
78 | level,
79 | playerState,
80 | textureManager,
81 | animationManager,
82 | );
83 |
84 | ecs.addSystem(new MapTextureSystem(ecs, level));
85 | ecs.addSystem(new MapPolarSystem(ecs));
86 | ecs.addSystem(new MapItemSystem(ecs, animationManager, soundManager));
87 | ecs.addSystem(new ControlSystem(ecs, container));
88 | ecs.addSystem(new MoveSystem(ecs));
89 | ecs.addSystem(new AnimationSystem(ecs));
90 | ecs.addSystem(new AISystem(ecs, textureManager, soundManager));
91 | ecs.addSystem(
92 | new WeaponSystem(
93 | ecs,
94 | container,
95 | animationManager,
96 | textureManager,
97 | soundManager,
98 | ),
99 | );
100 | ecs.addSystem(new RotateSystem(ecs));
101 | ecs.addSystem(new DoorsSystem(ecs));
102 | ecs.addSystem(new LightSystem(ecs));
103 | ecs.addSystem(new RenderSystem(ecs, container, level, textureManager));
104 | ecs.addSystem(new MinimapSystem(ecs, container, level));
105 |
106 | this.ecs = ecs;
107 | this.loop = createLoop(this.onTick);
108 | }
109 |
110 | getPlayerContainer() {
111 | const [player] = this.ecs.query([PlayerComponent, HealthComponent]);
112 |
113 | if (typeof player === "undefined") {
114 | return;
115 | }
116 |
117 | return this.ecs.getComponents(player);
118 | }
119 |
120 | shouldLevelBeCompleted() {
121 | const playerContainer = this.getPlayerContainer();
122 |
123 | if (!playerContainer) {
124 | return false;
125 | }
126 |
127 | const { endingScenario } = this.level;
128 | const enemies = this.ecs.query([EnemyComponent, HealthComponent]);
129 |
130 | switch (endingScenario.name) {
131 | case "exit":
132 | return (
133 | Math.floor(playerContainer.get(PositionComponent).x) ===
134 | endingScenario.position.x &&
135 | Math.floor(playerContainer.get(PositionComponent).y) ===
136 | endingScenario.position.y
137 | );
138 | case "enemy":
139 | for (const enemy of enemies) {
140 | if (this.ecs.getComponents(enemy).get(HealthComponent).current > 0) {
141 | return false;
142 | }
143 | }
144 | return true;
145 | case "survive":
146 | if (this.timeLeft !== undefined) {
147 | return this.timeLeft <= 0;
148 | }
149 | return false;
150 | }
151 | }
152 |
153 | shouldLevelBeFailed() {
154 | const playerContainer = this.getPlayerContainer();
155 |
156 | if (!playerContainer) {
157 | return true;
158 | }
159 |
160 | return playerContainer.get(HealthComponent).current <= 0;
161 | }
162 |
163 | onTick = (dt: number) => {
164 | this.ecs.update(dt);
165 |
166 | this.updatePlayerView(dt);
167 |
168 | if (this.onCompleteCallback && this.shouldLevelBeCompleted()) {
169 | window.requestAnimationFrame(this.onCompleteCallback);
170 | }
171 |
172 | if (this.onFailedCallback && this.shouldLevelBeFailed()) {
173 | window.requestAnimationFrame(this.onFailedCallback);
174 | }
175 | };
176 |
177 | updatePlayerView(dt: number) {
178 | const playerContainer = this.getPlayerContainer();
179 |
180 | if (!playerContainer) {
181 | return;
182 | }
183 |
184 | if (this.timeLeft) {
185 | this.timeLeft = Math.max(0, this.timeLeft - dt);
186 | }
187 |
188 | this.playerState.health = playerContainer.get(HealthComponent).current;
189 | this.playerState.ammo = (
190 | playerContainer.get(PlayerComponent).weapons[
191 | WEAPON_PISTOL_INDEX
192 | ] as WeaponRangeComponent
193 | )?.bulletTotal;
194 |
195 | this.levelPlayerView.render({
196 | soundMuted: this.soundManager.checkMuted(),
197 | ammo: this.playerState.ammo,
198 | health: this.playerState.health,
199 | timeLeft: this.timeLeft,
200 | });
201 | }
202 |
203 | onComplete = (cb: () => void) => {
204 | this.onCompleteCallback = cb;
205 | };
206 |
207 | onFailed = (cb: () => void) => {
208 | this.onFailedCallback = cb;
209 | };
210 |
211 | toogleMusicControl() {
212 | if (this.soundManager.checkMuted()) {
213 | this.soundManager.unmute();
214 | } else {
215 | this.soundManager.mute();
216 | }
217 | }
218 |
219 | // @TODO: extract to another system
220 | handleDocumentKeypress = (e: KeyboardEvent) => {
221 | if (e.code === KEY_CONTROL_PAUSE) {
222 | this.toogleMusicControl();
223 | }
224 | };
225 |
226 | createListeners() {
227 | document.addEventListener("keypress", this.handleDocumentKeypress);
228 | }
229 |
230 | destroyListeners() {
231 | document.removeEventListener("keypress", this.handleDocumentKeypress);
232 | }
233 |
234 | start() {
235 | this.startedAt = +new Date();
236 | this.ecs.start();
237 | this.loop.play();
238 | this.levelPlayerView.render({
239 | soundMuted: this.soundManager.checkMuted(),
240 | ammo: this.playerState.ammo,
241 | health: this.playerState.health,
242 | timeLeft: this.timeLeft,
243 | });
244 | this.createListeners();
245 | }
246 |
247 | destroy() {
248 | this.loop.pause();
249 | this.ecs.destroy();
250 | this.levelPlayerView.destroy();
251 | this.destroyListeners();
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/src/scenes/TitleScene.ts:
--------------------------------------------------------------------------------
1 | import ContentView from "src/views/ContentView";
2 | import BaseScene from "./BaseScene";
3 |
4 | export default class TitleScene implements BaseScene {
5 | public playerState: PlayerState;
6 |
7 | protected readonly view: ContentView;
8 | protected readonly container: HTMLElement;
9 | protected onCompleteCallback?: () => void;
10 |
11 | constructor(container: HTMLElement, playerState: PlayerState, title: string, subtitle?: string[]) {
12 | this.container = container;
13 | this.playerState = playerState;
14 | this.view = new ContentView(title, subtitle);
15 | this.createListeners();
16 | }
17 |
18 | onComplete(cb: () => void): void {
19 | this.onCompleteCallback = cb;
20 | }
21 |
22 | start() {
23 | this.container.appendChild(this.view.canvas.element);
24 | this.view.render();
25 | }
26 |
27 | destroy() {
28 | this.destroyListeners()
29 | this.view.canvas.element.remove();
30 | }
31 |
32 | createListeners() {
33 | document.addEventListener('keydown', this.handleDocumentKeydown);
34 | document.addEventListener('pointerdown', this.handleDocumentClick);
35 | }
36 |
37 | destroyListeners() {
38 | document.removeEventListener('keydown', this.handleDocumentKeydown);
39 | document.removeEventListener('pointerdown', this.handleDocumentClick);
40 | }
41 |
42 | handleDocumentKeydown = () => {
43 | if (this.onCompleteCallback) {
44 | window.requestAnimationFrame(this.onCompleteCallback);
45 | }
46 | }
47 |
48 | handleDocumentClick = () => {
49 | if (this.onCompleteCallback) {
50 | window.requestAnimationFrame(this.onCompleteCallback);
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/src/views/ContentView.ts:
--------------------------------------------------------------------------------
1 | import Canvas from "src/lib/Canvas/DefaultCanvas";
2 |
3 | export default class ContentView {
4 | readonly title: string;
5 | readonly subtitle: string[];
6 | readonly canvas: Canvas;
7 | readonly width: number = 640;
8 | readonly height: number = 480;
9 |
10 | constructor(title: string, subtitle: string[] = []) {
11 | this.title = title;
12 | this.subtitle = subtitle;
13 | this.canvas = new Canvas({
14 | id: "content",
15 | height: this.height,
16 | width: this.width,
17 | style: "border: 1px solid black",
18 | });
19 | }
20 |
21 | renderText(y: number, fontSize: number, text: string) {
22 | this.canvas.drawText({
23 | x: this.width / 2,
24 | y,
25 | align: 'center',
26 | text,
27 | color: 'white',
28 | font: `${fontSize}px Lucida Console`
29 | });
30 | }
31 |
32 | render() {
33 | this.canvas.clear();
34 | this.canvas.drawBackground('black');
35 |
36 | const lineHeight = 40;
37 |
38 | if (this.subtitle?.length) {
39 | let y = this.height / 2 - lineHeight * this.subtitle.length / 2;
40 | this.renderText(y, 60, this.title);
41 |
42 | y += lineHeight;
43 | this.subtitle?.forEach((line) => {
44 | y += lineHeight;
45 | this.renderText(y, 30, line)
46 | });
47 | } else {
48 | this.renderText(this.height / 2, 40, this.title)
49 | }
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/src/views/LevelPlayerView.ts:
--------------------------------------------------------------------------------
1 | import Canvas from "src/lib/Canvas/DefaultCanvas";
2 | import { lerp, minmax } from "src/lib/utils/math";
3 |
4 | export default class LevelPlayerView {
5 | // Component not
6 | protected readonly width: number = 640;
7 | protected readonly height: number = 480;
8 |
9 | protected readonly canvas: Canvas;
10 | protected readonly container: HTMLElement;
11 |
12 | private readonly icons = ["health", "bullets", "timer"];
13 | private iconsImages: Record = {};
14 |
15 | constructor(container: HTMLElement) {
16 | this.container = container;
17 |
18 | this.canvas = new Canvas({
19 | id: "ui",
20 | height: this.height,
21 | width: this.width,
22 | });
23 |
24 | this.container.appendChild(this.canvas.element);
25 | this.loadIcons();
26 | }
27 |
28 | private loadIcons() {
29 | for (const icon of this.icons) {
30 | const img = new Image();
31 | img.src = `/fps/assets/icons/${icon}.png`;
32 | this.iconsImages[icon] = img;
33 | }
34 | }
35 |
36 | render(state: {
37 | health: number;
38 | soundMuted: boolean;
39 | ammo?: number;
40 | timeLeft?: number;
41 | }) {
42 | this.canvas.clear();
43 |
44 | this.canvas.drawText({
45 | x: this.width - 10,
46 | y: 30,
47 | text: state.soundMuted ? "Music off" : "Music on",
48 | color: "grey",
49 | align: "right",
50 | font: "18px serif",
51 | });
52 |
53 | if (state.health) {
54 | this.drawHealth(state.health, { x: 10, y: 10 });
55 | }
56 |
57 | if (state.ammo) {
58 | this.drawAmmo(state.ammo, { x: 10, y: 40 });
59 | }
60 |
61 | if (state.timeLeft !== undefined) {
62 | this.drawTimer(state.timeLeft, { x: this.canvas.width / 2 - 50, y: 10 });
63 | }
64 | }
65 |
66 | drawHealth(healthValue: number, position: Vector2D) {
67 | const pulseSpeed = lerp(15, 2, minmax(healthValue / 100, 0, 1));
68 | const angle = (Date.now() / 1500) * pulseSpeed;
69 | const scale =
70 | 0.6 +
71 | 0.4 *
72 | (0.1 * Math.cos(angle) -
73 | 0.3 * Math.cos(4 * angle) +
74 | Math.abs(Math.cos(angle)));
75 |
76 | this.drawIcon("health", {
77 | x: position.x,
78 | y: position.y,
79 | width: 24,
80 | height: 24,
81 | scale,
82 | });
83 | this.canvas.drawText({
84 | x: position.x + 30,
85 | y: position.y + 20,
86 | text: healthValue.toString(),
87 | align: "left",
88 | color: "red",
89 | font: "24px serif",
90 | });
91 | }
92 |
93 | drawAmmo(bulletTotal: number, position: Vector2D) {
94 | this.drawIcon("bullets", {
95 | x: position.x,
96 | y: position.y,
97 | width: 24,
98 | height: 24,
99 | });
100 | this.canvas.drawText({
101 | x: position.x + 30,
102 | y: position.y + 20,
103 | text: bulletTotal.toString(),
104 | align: "left",
105 | color: "white",
106 | font: "24px serif",
107 | });
108 | }
109 |
110 | drawTimer(time: number, position: Vector2D) {
111 | this.drawIcon("timer", {
112 | x: position.x,
113 | y: position.y,
114 | width: 24,
115 | height: 24,
116 | });
117 | this.canvas.drawText({
118 | x: position.x + 30,
119 | y: position.y + 20,
120 | text: time.toFixed(2),
121 | align: "left",
122 | color: "white",
123 | font: "24px serif",
124 | });
125 | }
126 |
127 | drawIcon(
128 | iconName: string,
129 | config: {
130 | x: number;
131 | y: number;
132 | width: number;
133 | height: number;
134 | scale?: number;
135 | },
136 | ) {
137 | const scale = config.scale ?? 1;
138 | this.canvas.context.drawImage(
139 | this.iconsImages[iconName],
140 | config.x + (config.width / 2) * (1 - scale),
141 | config.y + (config.height / 2) * (1 - scale),
142 | config.width * scale,
143 | config.height * scale,
144 | );
145 | }
146 |
147 | destroy(): void {
148 | this.canvas.element.remove();
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "paths": {
22 | "src/*": ["./src/*"]
23 | },
24 | },
25 | "include": ["src"]
26 | }
27 |
--------------------------------------------------------------------------------
/vite.config.mts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from "url";
2 | import { defineConfig } from "vite";
3 |
4 | export default defineConfig({
5 | base: "/fps/",
6 | server: {
7 | port: 3000,
8 | },
9 | resolve: {
10 | alias: {
11 | 'src': fileURLToPath(new URL('./src', import.meta.url))
12 | }
13 | },
14 | build: {
15 | target: "esnext",
16 | },
17 | });
18 |
--------------------------------------------------------------------------------