Something something games
This commit is contained in:
		| @@ -7,6 +7,9 @@ const nextConfig = { | |||||||
|   experimental: { |   experimental: { | ||||||
|     reactCompiler: true, |     reactCompiler: true, | ||||||
|     ppr: "incremental", |     ppr: "incremental", | ||||||
|  |     turbo: { | ||||||
|  |        | ||||||
|  |     } | ||||||
|   }, |   }, | ||||||
|   serverExternalPackages: ["typeorm", "better-sqlite3"], |   serverExternalPackages: ["typeorm", "better-sqlite3"], | ||||||
|   reactStrictMode: true, |   reactStrictMode: true, | ||||||
|   | |||||||
							
								
								
									
										3173
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3173
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										66
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										66
									
								
								package.json
									
									
									
									
									
								
							| @@ -3,7 +3,7 @@ | |||||||
|   "version": "0.1.0", |   "version": "0.1.0", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "next dev", |     "dev": "next dev --turbopack", | ||||||
|     "build": "next build", |     "build": "next build", | ||||||
|     "build:analyse": "ANALYZE=true npm run build", |     "build:analyse": "ANALYZE=true npm run build", | ||||||
|     "start": "next start", |     "start": "next start", | ||||||
| @@ -11,51 +11,59 @@ | |||||||
|     "lint:fix": "next lint --fix" |     "lint:fix": "next lint --fix" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@aws-sdk/client-s3": "^3.712.0", |     "@aws-sdk/client-s3": "^3.750.0", | ||||||
|  |     "@dimforge/rapier2d-compat": "^0.14.0", | ||||||
|     "@heroicons/react": "^2.2.0", |     "@heroicons/react": "^2.2.0", | ||||||
|     "@mdx-js/loader": "^3.1.0", |     "@mdx-js/loader": "^3.1.0", | ||||||
|     "@mdx-js/react": "^3.1.0", |     "@mdx-js/react": "^3.1.0", | ||||||
|     "@next/bundle-analyzer": "^15.1.0", |     "@next/bundle-analyzer": "^15.1.7", | ||||||
|     "@next/mdx": "^15.1.0", |     "@next/mdx": "^15.1.7", | ||||||
|     "@tailwindcss/typography": "^0.5.15", |     "@pixi-essentials/object-pool": "^1.0.1", | ||||||
|     "@tanstack/react-query": "^5.62.7", |     "@pixi/events": "^7.4.2", | ||||||
|     "@tanstack/react-virtual": "^3.11.1", |     "@tailwindcss/postcss": "^4.0.8", | ||||||
|     "@trpc/client": "^11.0.0-rc.660", |     "@tailwindcss/typography": "^0.5.16", | ||||||
|     "@trpc/react-query": "^11.0.0-rc.660", |     "@tanstack/react-query": "^5.66.9", | ||||||
|     "@trpc/server": "^11.0.0-rc.660", |     "@tanstack/react-virtual": "^3.13.0", | ||||||
|  |     "@trpc/client": "^11.0.0-rc.802", | ||||||
|  |     "@trpc/react-query": "^11.0.0-rc.802", | ||||||
|  |     "@trpc/server": "^11.0.0-rc.802", | ||||||
|     "@types/better-sqlite3": "^7.6.12", |     "@types/better-sqlite3": "^7.6.12", | ||||||
|     "@types/mdx": "^2.0.13", |     "@types/mdx": "^2.0.13", | ||||||
|     "@types/node": "^22.10.2", |     "@types/node": "^22.13.5", | ||||||
|     "@types/react": "^19.0.1", |     "@types/react": "^19.0.10", | ||||||
|     "@types/react-dom": "^19.0.2", |     "@types/react-dom": "^19.0.4", | ||||||
|     "@typescript-eslint/eslint-plugin": "^8.18.0", |     "@typescript-eslint/eslint-plugin": "^8.14.1", | ||||||
|     "autoprefixer": "^10.4.20", |     "autoprefixer": "^10.4.20", | ||||||
|     "babel-plugin-react-compiler": "beta", |     "babel-plugin-react-compiler": "beta", | ||||||
|     "better-sqlite3": "^11.7.0", |     "better-sqlite3": "^11.8.1", | ||||||
|     "client-only": "^0.0.1", |     "client-only": "^0.0.1", | ||||||
|     "drizzle-kit": "^0.30.1", |     "drizzle-kit": "^0.30.4", | ||||||
|     "drizzle-orm": "^0.38.2", |     "drizzle-orm": "^0.39.3", | ||||||
|     "eslint": "^9.17.0", |     "eslint": "^9.21.0", | ||||||
|     "eslint-config-next": "^15.1.0", |     "eslint-config-next": "^15.1.7", | ||||||
|     "exif-reader": "^2.0.1", |     "eventemitter3": "^5.0.1", | ||||||
|     "framer-motion": "^11.14.4", |     "exif-reader": "^2.0.2", | ||||||
|     "glob": "^11.0.0", |     "framer-motion": "^12.4.7", | ||||||
|  |     "glob": "^11.0.1", | ||||||
|     "million": "^3.1.11", |     "million": "^3.1.11", | ||||||
|     "next": "15.1.1-canary.5", |     "next": "15.2.0-canary.69", | ||||||
|     "next-auth": "5.0.0-beta.25", |     "next-auth": "5.0.0-beta.25", | ||||||
|     "postcss": "^8.4.49", |     "pixi-filters": "^6.1.0", | ||||||
|  |     "pixi-viewport": "^6.0.3", | ||||||
|  |     "pixi.js": "^8.8.0", | ||||||
|  |     "postcss": "^8.5.3", | ||||||
|     "radash": "^12.1.0", |     "radash": "^12.1.0", | ||||||
|     "react": "19.0.0", |     "react": "19.0.0", | ||||||
|     "react-dom": "19.0.0", |     "react-dom": "19.0.0", | ||||||
|     "react-zoom-pan-pinch": "^3.6.1", |     "react-zoom-pan-pinch": "^3.7.0", | ||||||
|     "reflect-metadata": "^0.2.2", |     "reflect-metadata": "^0.2.2", | ||||||
|     "server-only": "^0.0.1", |     "server-only": "^0.0.1", | ||||||
|     "sharp": "^0.33.5", |     "sharp": "^0.33.5", | ||||||
|     "superjson": "^2.2.2", |     "superjson": "^2.2.2", | ||||||
|     "tailwind-scrollbar": "^3.1.0", |     "tailwind-scrollbar": "^4.0.0", | ||||||
|     "tailwindcss": "^3.4.16", |     "tailwindcss": "^4.0.8", | ||||||
|     "typescript": "^5.7.2", |     "typescript": "^5.7.3", | ||||||
|     "yet-another-react-lightbox": "^3.21.7", |     "yet-another-react-lightbox": "^3.21.7", | ||||||
|     "zod": "^3.24.1" |     "zod": "^3.24.2" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,3 @@ | |||||||
| module.exports = { | module.exports = { | ||||||
|   plugins: { |   plugins: ["@tailwindcss/postcss"], | ||||||
|     tailwindcss: {}, |  | ||||||
|     autoprefixer: {}, |  | ||||||
|   }, |  | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								src/app/(root)/games/map/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/app/(root)/games/map/page.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | import Wrapper from "./wrapper"; | ||||||
|  |  | ||||||
|  | export default function Game(): React.JSX.Element { | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Wrapper/> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								src/app/(root)/games/map/wrapper.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/app/(root)/games/map/wrapper.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | "use client"; | ||||||
|  |  | ||||||
|  | import dynamic from "next/dynamic"; | ||||||
|  | import React from "react"; | ||||||
|  |  | ||||||
|  | const MapGen = dynamic<Record<string, never>>(() => import('../../../../games/games/MapGen/MapGenWrapper').then((module) => module.default), { | ||||||
|  |   ssr: false, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default function Wrapper(): React.ReactNode { | ||||||
|  |   return ( | ||||||
|  |     <div className="flex flex-1"> | ||||||
|  |       <MapGen/> | ||||||
|  |     </div> | ||||||
|  |   ) | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								src/app/(root)/games/player/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/app/(root)/games/player/page.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | import Wrapper from "./wrapper"; | ||||||
|  |  | ||||||
|  | export default function Game(): React.JSX.Element { | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Wrapper/> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								src/app/(root)/games/player/wrapper.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/app/(root)/games/player/wrapper.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | "use client"; | ||||||
|  |  | ||||||
|  | import dynamic from "next/dynamic"; | ||||||
|  | import React from "react"; | ||||||
|  |  | ||||||
|  | const TestGame = dynamic<Record<string, never>>(() => import('../../../../games/games/TestGame/TestGameWrapper').then((module) => module.default), { | ||||||
|  |   ssr: false, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default function Wrapper(): React.ReactNode { | ||||||
|  |   return ( | ||||||
|  |     <div className="flex flex-1"> | ||||||
|  |       <TestGame/> | ||||||
|  |     </div> | ||||||
|  |   ) | ||||||
|  | } | ||||||
| @@ -1,6 +1,4 @@ | |||||||
| @tailwind base; | @import "tailwindcss"; | ||||||
| @tailwind components; |  | ||||||
| @tailwind utilities; |  | ||||||
|  |  | ||||||
| @layer utilities { | @layer utilities { | ||||||
|   .text-balance { |   .text-balance { | ||||||
|   | |||||||
							
								
								
									
										82
									
								
								src/games/games/MapGen/MapGenApp.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/games/games/MapGen/MapGenApp.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | |||||||
|  | import BaseGameApp from '@/core/BaseGameApp/BaseGameApp'; | ||||||
|  | import Registry from '@/utils/Registry'; | ||||||
|  | import Control from '@/control/Control'; | ||||||
|  | import Tile, { TileDir } from './Tile'; | ||||||
|  | import Vector from '@/games/lib/utils/Vector'; | ||||||
|  | import { min, shuffle } from 'radash'; | ||||||
|  |  | ||||||
|  | const grid = new Vector(64, 256); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * A test "game" to start generating useful examples, building up classes as needed etc | ||||||
|  |  */ | ||||||
|  | class TestGameApp extends BaseGameApp { | ||||||
|  |   private _control: Control; | ||||||
|  |   private _tileGrid: Tile[][] = Array.from({ length: grid.x }).map(() => Array.from({ length: grid.y })); | ||||||
|  |   private _allTiles: Tile[] = Array.from({ length: grid.x * grid.y }); | ||||||
|  |  | ||||||
|  |   constructor(canvas: HTMLCanvasElement | null) { | ||||||
|  |     super(canvas); | ||||||
|  |     this._control = new Control(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected async _start(canvas: HTMLCanvasElement): Promise<void> { | ||||||
|  |     await super._start(canvas); | ||||||
|  |     Registry.register('Control', this._control); | ||||||
|  |  | ||||||
|  |     for (let x = 0; x < grid.x; x++) { | ||||||
|  |       for (let y = 0; y < grid.y; y++) { | ||||||
|  |         this._tileGrid[x][y] = new Tile({ x, y }); | ||||||
|  |         this._allTiles[x + y * grid.x] = this._tileGrid[x][y]; | ||||||
|  |         this._viewport.addChild(this._tileGrid[x][y]); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     for (let x = 0; x < grid.x; x++) { | ||||||
|  |       for (let y = 0; y < grid.y; y++) { | ||||||
|  |         if (x > 0) { | ||||||
|  |           this._tileGrid[x][y].link(this._tileGrid[x - 1][y], TileDir.west); | ||||||
|  |         } | ||||||
|  |         if (x < grid.x - 1) { | ||||||
|  |           this._tileGrid[x][y].link(this._tileGrid[x + 1][y], TileDir.east); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (y > 0) { | ||||||
|  |           this._tileGrid[x][y].link(this._tileGrid[x][y - 1], TileDir.north); | ||||||
|  |         } | ||||||
|  |         if (y < grid.y - 1) { | ||||||
|  |           this._tileGrid[x][y].link(this._tileGrid[x][y + 1], TileDir.south); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     for (let x = 0; x < grid.x; x++) { | ||||||
|  |       for (let y = 0; y < grid.y; y++) { | ||||||
|  |         this._tileGrid[x][y].rescore() | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Update the logic of the game | ||||||
|  |    * | ||||||
|  |    * @param timeDelta - The time difference since the last update (in ms) | ||||||
|  |    */ | ||||||
|  |   protected async _update(timeDelta: number): Promise<void> { | ||||||
|  |     await super._update(timeDelta); | ||||||
|  |  | ||||||
|  |     while (true) { | ||||||
|  |  | ||||||
|  |       const activeTiles = this._allTiles.filter((tile) => !tile.selected && tile.score > 0); | ||||||
|  |       if (activeTiles.length === 0) { | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |       const lowestScore = min(activeTiles, t => t.score)!.score; | ||||||
|  |       const lowestTiles = activeTiles.filter((tile) => tile.score === lowestScore); | ||||||
|  |       const selectedTile = shuffle(lowestTiles)[0]; | ||||||
|  |       selectedTile.select(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default TestGameApp; | ||||||
							
								
								
									
										24
									
								
								src/games/games/MapGen/MapGenWrapper.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/games/games/MapGen/MapGenWrapper.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | import React, { useEffect, useRef } from 'react'; | ||||||
|  | import MapGenApp from './MapGenApp'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * The wrapper for Test Game so it can be dynamically loaded with the WebGL hook | ||||||
|  |  */ | ||||||
|  | function TestGameWrapper(): React.ReactElement { | ||||||
|  |   const canvas = useRef<HTMLCanvasElement>(null); | ||||||
|  |   const gameCreated = useRef<boolean>(false); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (!gameCreated.current) { | ||||||
|  |       console.log('Creating game'); | ||||||
|  |       gameCreated.current = true; | ||||||
|  |       new MapGenApp(canvas.current); | ||||||
|  |     } | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <canvas ref={canvas} /> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default TestGameWrapper; | ||||||
							
								
								
									
										212
									
								
								src/games/games/MapGen/Tile.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								src/games/games/MapGen/Tile.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,212 @@ | |||||||
|  | import Container from "@/games/lib/visual/Container"; | ||||||
|  | import Graphics from "@/games/lib/visual/Graphics"; | ||||||
|  | import Text from "@/games/lib/visual/Text"; | ||||||
|  |  | ||||||
|  | const TileTypes = { | ||||||
|  |   grass: "grass", | ||||||
|  |   dirt: "dirt", | ||||||
|  |   rock: "rock", | ||||||
|  |   iron: "iron", | ||||||
|  |   copper: "copper", | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export enum TileDir { | ||||||
|  |   north, | ||||||
|  |   east, | ||||||
|  |   south, | ||||||
|  |   west | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | type TileData = { | ||||||
|  |   available: boolean, | ||||||
|  |   colour: number | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const tileData: { [key in keyof typeof TileTypes]: TileData } = { | ||||||
|  |   grass: { | ||||||
|  |     available: false, | ||||||
|  |     colour: 0x117c13 | ||||||
|  |   }, | ||||||
|  |   rock: { | ||||||
|  |     available: false, | ||||||
|  |     colour: 0xc6bfb8 | ||||||
|  |   }, | ||||||
|  |   dirt: { | ||||||
|  |     available: false, | ||||||
|  |     colour: 0x402905 | ||||||
|  |   }, | ||||||
|  |   copper: { | ||||||
|  |     available: false, | ||||||
|  |     colour: 0xb87333 | ||||||
|  |   }, | ||||||
|  |   iron: { | ||||||
|  |     available: false, | ||||||
|  |     colour: 0xa19d94 | ||||||
|  |   }, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class Tile extends Container { | ||||||
|  |   private static readonly size = { | ||||||
|  |     x: 30, | ||||||
|  |     y: 30 | ||||||
|  |   } | ||||||
|  |   private _graphic: Graphics = new Graphics(); | ||||||
|  |   private _point: { x: number, y: number }; | ||||||
|  |   private _text: Text = new Text({ | ||||||
|  |     position: { | ||||||
|  |       x: Tile.size.x / 2, | ||||||
|  |       y: Tile.size.y / 2, | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   private _choices = structuredClone(tileData); | ||||||
|  |   private _selected: null | keyof typeof tileData = null; | ||||||
|  |   public score = 0; | ||||||
|  |   public neighbors: Tile[] = []; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   constructor(point: { x: number, y: number }) { | ||||||
|  |     super({ | ||||||
|  |       position: { | ||||||
|  |         x: point.x * Tile.size.x, | ||||||
|  |         y: point.y * Tile.size.y, | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     this._point = point; | ||||||
|  |     this.addChild(this._graphic, this._text); | ||||||
|  |  | ||||||
|  |     this._graphic.rect(0, 0, Tile.size.x, Tile.size.y).fill({ color: 0xcccccc }).stroke(); | ||||||
|  |     this._text.text = "-1"; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   link(tile: Tile, dir: TileDir) { | ||||||
|  |     this.neighbors[dir] = tile; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   reset() { | ||||||
|  |     this._graphic.clear(); | ||||||
|  |     this._graphic.rect(0, 0, Tile.size.x, Tile.size.y).fill({ color: 0xcccccc }).stroke(); | ||||||
|  |     Object.values(this._choices).forEach((choice) => choice.available = false); | ||||||
|  |     this._text.text = "-1"; | ||||||
|  |     this._point = { | ||||||
|  |       x: -1, | ||||||
|  |       y: -1 | ||||||
|  |     } | ||||||
|  |     this._selected = null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   _calculateAvailable() { | ||||||
|  |     if (!this.neighbors[TileDir.north]) { | ||||||
|  |       this._choices.grass.available = true; | ||||||
|  |       this._choices.dirt.available = true; | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.neighbors.forEach((neighbor) => { | ||||||
|  |       if (neighbor.selected === 'grass') { | ||||||
|  |         this._choices.dirt.available = true; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (neighbor.selected === 'dirt') { | ||||||
|  |         this._choices.dirt.available = true; | ||||||
|  |         this._choices.rock.available = true; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (neighbor.selected === 'rock') { | ||||||
|  |         this._choices.rock.available = true; | ||||||
|  |         this._choices.iron.available = true; | ||||||
|  |         this._choices.copper.available = true; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (neighbor.selected === 'iron') { | ||||||
|  |         this._choices.rock.available = true; | ||||||
|  |         this._choices.iron.available = true; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (neighbor.selected === 'copper') { | ||||||
|  |         this._choices.rock.available = true; | ||||||
|  |         this._choices.copper.available = true; | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     if (this._point.y < 32) { | ||||||
|  |       this._choices.iron.available = false; | ||||||
|  |     } | ||||||
|  |     if (this._point.y < 64) { | ||||||
|  |       this._choices.copper.available = false; | ||||||
|  |       this._choices.dirt.available = true; | ||||||
|  |       this._choices.rock.available = true; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   rescore(): number { | ||||||
|  |     if (this._selected) { | ||||||
|  |       return 0; | ||||||
|  |     } | ||||||
|  |     this._calculateAvailable(); | ||||||
|  |  | ||||||
|  |     this.score = Object.values(this._choices).reduce((count, choice) => { | ||||||
|  |       return choice.available ? count + 1 : count; | ||||||
|  |     }, 0); | ||||||
|  |  | ||||||
|  |     this._text.text = this.score; | ||||||
|  |     return this.score; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   select(): void { | ||||||
|  |  | ||||||
|  |     //const available = Object.entries(this._choices).filter((choice) => choice[1].available); | ||||||
|  |     //this._selected = shuffle(available)[0][0] as keyof typeof tileData; | ||||||
|  |     const available = Object.entries(this._choices).filter((choice) => choice[1].available) as [keyof typeof TileTypes, TileData][]; | ||||||
|  |  | ||||||
|  |     const weightedAvailable = available.map(([tileType, tileData]) => { | ||||||
|  |       const weightedEntry = { | ||||||
|  |         type: tileType, | ||||||
|  |         weight: 4 | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       if (tileType === 'grass') { | ||||||
|  |         weightedEntry.weight = 8; | ||||||
|  |       } | ||||||
|  |       else if (tileType === 'dirt') { | ||||||
|  |         if (this._point.y === 0) { | ||||||
|  |           weightedEntry.weight = 2; | ||||||
|  |         } else { | ||||||
|  |           weightedEntry.weight += 20 - Math.floor(((Math.min(this._point.y, 63) / 64) * 20) + 3); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       else if (tileType === 'iron') { | ||||||
|  |         weightedEntry.weight = 3; | ||||||
|  |       } | ||||||
|  |       else if (tileType === 'copper') { | ||||||
|  |         weightedEntry.weight = 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return weightedEntry; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const totalWeight = weightedAvailable.reduce((weight, avail) => weight + avail.weight, 0); | ||||||
|  |     let pick = Math.floor(Math.random() * totalWeight) | ||||||
|  |  | ||||||
|  |     for (let i = 0; i < weightedAvailable.length; i++) { | ||||||
|  |       if (pick < weightedAvailable[i].weight) { | ||||||
|  |         this._selected = weightedAvailable[i].type; | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |       pick -= weightedAvailable[i].weight; | ||||||
|  |     } | ||||||
|  |     if (this._selected === null) { | ||||||
|  |       throw new Error('Selection not done'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this._graphic.clear(); | ||||||
|  |     this._graphic.rect(0, 0, Tile.size.x, Tile.size.y).fill({ color: this._choices[this._selected].colour }); | ||||||
|  |     this._text.text = ""; | ||||||
|  |  | ||||||
|  |     this.neighbors.forEach((neighbor) => neighbor.rescore()); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get selected() { | ||||||
|  |     return this._selected; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										41
									
								
								src/games/games/TestGame/TestGameApp.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/games/games/TestGame/TestGameApp.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | import BaseGameApp from '@/core/BaseGameApp/BaseGameApp'; | ||||||
|  | import Registry from '@/utils/Registry'; | ||||||
|  | import Control from '@/control/Control'; | ||||||
|  |  | ||||||
|  | import Player from './components/Player/Player'; | ||||||
|  | import Trees from './components/Trees'; | ||||||
|  | import Walls from './components/Walls/Walls'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * A test "game" to start generating useful examples, building up classes as needed etc | ||||||
|  |  */ | ||||||
|  | class TestGameApp extends BaseGameApp { | ||||||
|  |   private _player!: Player; | ||||||
|  |   private _control: Control; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   constructor(canvas: HTMLCanvasElement | null) { | ||||||
|  |     super(canvas); | ||||||
|  |     this._control = new Control(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected async _start(canvas: HTMLCanvasElement): Promise<void> { | ||||||
|  |     await super._start(canvas); | ||||||
|  |     Registry.register('Control', this._control); | ||||||
|  |     this._viewport.addChild(new Trees({ numberOfTrees: 200 })); | ||||||
|  |     this._player = this._viewport.addChild(new Player()); | ||||||
|  |     this._viewport.addChild(new Walls()); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Update the logic of the game | ||||||
|  |    * | ||||||
|  |    * @param timeDelta - The time difference since the last update (in ms) | ||||||
|  |    */ | ||||||
|  |   protected async _update(timeDelta: number): Promise<void> { | ||||||
|  |     this._player.update(timeDelta); | ||||||
|  |     await super._update(timeDelta); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default TestGameApp; | ||||||
							
								
								
									
										24
									
								
								src/games/games/TestGame/TestGameWrapper.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/games/games/TestGame/TestGameWrapper.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | import React, { useEffect, useRef } from 'react'; | ||||||
|  | import TestGameApp from './TestGameApp'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * The wrapper for Test Game so it can be dynamically loaded with the WebGL hook | ||||||
|  |  */ | ||||||
|  | function TestGameWrapper(): React.ReactElement { | ||||||
|  |   const canvas = useRef<HTMLCanvasElement>(null); | ||||||
|  |   const gameCreated = useRef<boolean>(false); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (!gameCreated.current) { | ||||||
|  |       console.log('Creating game'); | ||||||
|  |       gameCreated.current = true; | ||||||
|  |       new TestGameApp(canvas.current); | ||||||
|  |     } | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <canvas ref={canvas} /> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default TestGameWrapper; | ||||||
							
								
								
									
										132
									
								
								src/games/games/TestGame/components/Player/Player.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								src/games/games/TestGame/components/Player/Player.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | |||||||
|  | import Collidable from '@/control/Collidable'; | ||||||
|  | import Rapier from '@/utils/Rapier'; | ||||||
|  | import Registry from '@/utils/Registry'; | ||||||
|  | import Vector from '@/utils/Vector'; | ||||||
|  | import Container from '@/visual/Container'; | ||||||
|  | import Graphics from '@/visual/Graphics'; | ||||||
|  |  | ||||||
|  | import BaseAttack from './attack/BaseAttack'; | ||||||
|  | import Forward from './attack/Forward'; | ||||||
|  |  | ||||||
|  | import type { Viewport } from '@/visual/pixi'; | ||||||
|  | import type Control from '@/control/Control'; | ||||||
|  | import { assign } from 'radash'; | ||||||
|  |  | ||||||
|  | type PlayerSettings = ConstructorParameters<typeof Collidable>[0] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * - TODO | ||||||
|  |  */ | ||||||
|  | class Player extends Collidable { | ||||||
|  |   private _control: Control; | ||||||
|  |   private _characterGraphics: Graphics; | ||||||
|  |  | ||||||
|  |   private _attacks: BaseAttack[] = []; | ||||||
|  |   private _rapier: Rapier; | ||||||
|  |   private _collider: InstanceType<Rapier['rapier']['Collider']>; | ||||||
|  |   private _characterController: InstanceType<Rapier['rapier']['KinematicCharacterController']>; | ||||||
|  |  | ||||||
|  |   constructor(settings?: PlayerSettings) { | ||||||
|  |     super(assign(structuredClone(settings ?? {}), { | ||||||
|  |       position: { | ||||||
|  |         x: 960, | ||||||
|  |         y: 540 | ||||||
|  |       } | ||||||
|  |     })); | ||||||
|  |     const viewport = Registry.fetch<Viewport>('Viewport'); | ||||||
|  |     this._rapier = Registry.fetch<Rapier>('Rapier'); | ||||||
|  |     this._control = Registry.fetch<Control>('Control'); | ||||||
|  |  | ||||||
|  |     this._characterGraphics = viewport.addChild(new Graphics()); | ||||||
|  |     this._characterGraphics.setStrokeStyle({ | ||||||
|  |       color: 0x00DD00, | ||||||
|  |       width: 1 | ||||||
|  |     }).regularPoly(-10, -10, 20, 3).fill(0x00AA00).stroke(); | ||||||
|  |     this._characterGraphics.pivot.set(-10, -10); | ||||||
|  |     this.addChild(this._characterGraphics); | ||||||
|  |  | ||||||
|  |     viewport.follow(this, { radius: 250 }); | ||||||
|  |     viewport.addChild(this); | ||||||
|  |  | ||||||
|  |     const projectileContainer = new Container(); | ||||||
|  |     viewport.addChild(projectileContainer); | ||||||
|  |     const forwardAttack = new Forward(projectileContainer); | ||||||
|  |     this._attacks.push(forwardAttack); | ||||||
|  |     this.addChild(forwardAttack); | ||||||
|  |  | ||||||
|  |     const trianglePoints = { | ||||||
|  |       a: new Vector(0, -20), | ||||||
|  |       b: new Vector(10 * Math.sqrt(3), 10), | ||||||
|  |       c: new Vector(-10 * Math.sqrt(3), 10) | ||||||
|  |     }; | ||||||
|  |     this._collider = this._rapier.createCollider(this, 'triangle', trianglePoints); | ||||||
|  |     this._collider.setRestitution(0); | ||||||
|  |     this._collider.setTranslation(this.position); | ||||||
|  |  | ||||||
|  |     this._rapier.addColliderMemberGroups(this._collider, 'player'); | ||||||
|  |     this._rapier.addColliderFilterGroups(this._collider, 'enemy', 'enemyProjectile', 'wall'); | ||||||
|  |  | ||||||
|  |     this._characterController = this._rapier.world.createCharacterController(0.01); | ||||||
|  |  | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   update(timeDelta: number): void { | ||||||
|  |     let impulse = new Vector(0, 0); | ||||||
|  |     let speed = 250; | ||||||
|  |  | ||||||
|  |     if (this._control.isDown('up')) { | ||||||
|  |       impulse.y -= 1; | ||||||
|  |     } | ||||||
|  |     if (this._control.isDown('down')) { | ||||||
|  |       impulse.y += 1; | ||||||
|  |     } | ||||||
|  |     if (this._control.isDown('left')) { | ||||||
|  |       impulse.x -= 1; | ||||||
|  |     } | ||||||
|  |     if (this._control.isDown('right')) { | ||||||
|  |       impulse.x += 1; | ||||||
|  |     } | ||||||
|  |     if (this._control.isDown('shift')) { | ||||||
|  |       speed *= 4; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (impulse.magnitude() > 0) { | ||||||
|  |       const normImpulse = impulse.normalize(); | ||||||
|  |       this.rotation = Math.atan2(normImpulse.y, normImpulse.x) - Math.atan2(1, 0) + Math.PI; | ||||||
|  |       impulse = impulse.normalize().multiplyScalar(speed); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this._characterController.computeColliderMovement( | ||||||
|  |       this._collider, | ||||||
|  |       impulse.multiplyScalar(timeDelta) | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const correctedMovement = this._characterController.computedMovement(); | ||||||
|  |     this.position.add(correctedMovement); | ||||||
|  |     // console.log('impulse', impulse); | ||||||
|  |     // console.log('corrected', correctedMovement) | ||||||
|  |     //this._collider.setTranslation(this.position); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     if (this._control.isDown(' ')) { | ||||||
|  |       const modifiedRotation = this.rotation - (Math.PI / 2); | ||||||
|  |       const currentVector = new Vector(Math.cos(modifiedRotation), Math.sin(modifiedRotation)); | ||||||
|  |       this._attacks[0].fire({ | ||||||
|  |         playerVector: currentVector | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this._attacks.forEach((attack) => { | ||||||
|  |       attack.update(timeDelta); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   collide(collided: Collidable, colliding: boolean): void { | ||||||
|  |     if (!colliding) { | ||||||
|  |       console.log('player no longer colliding'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Player; | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | import Container from '../../../../../lib/visual/Container'; | ||||||
|  | import type Vector from '../../../../../lib/utils/Vector'; | ||||||
|  |  | ||||||
|  | type fireState = { | ||||||
|  |   playerVector: Vector | ||||||
|  | } | ||||||
|  |  | ||||||
|  | abstract class BaseAttack extends Container { | ||||||
|  |   protected _projectileContainer: Container; | ||||||
|  |  | ||||||
|  |   constructor(projectileContainer: Container) { | ||||||
|  |     super(); | ||||||
|  |     this._projectileContainer = projectileContainer; | ||||||
|  |   } | ||||||
|  |   abstract fire(state: fireState): void; | ||||||
|  |   abstract update(timeDelta: number): void; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | export default BaseAttack; | ||||||
							
								
								
									
										159
									
								
								src/games/games/TestGame/components/Player/attack/Forward.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/games/games/TestGame/components/Player/attack/Forward.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | |||||||
|  | import Collidable from '@/control/Collidable'; | ||||||
|  | import Pool from '@/utils/Pool'; | ||||||
|  | import Registry from '@/utils/Registry'; | ||||||
|  | import Vector from '@/utils/Vector'; | ||||||
|  | import Graphics from '@/visual/Graphics'; | ||||||
|  |  | ||||||
|  | import BaseAttack from './BaseAttack'; | ||||||
|  |  | ||||||
|  | import type Rapier from '@/utils/Rapier'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * - TODO | ||||||
|  |  */ | ||||||
|  | class ForwardProjectile extends Collidable { | ||||||
|  |   private _speed: number = 600; | ||||||
|  |   private _size: number = 5; | ||||||
|  |   private _graphic: Graphics; | ||||||
|  |  | ||||||
|  |   private _lifeTime: { | ||||||
|  |     lifeTime: number, | ||||||
|  |     lifeTimeRemaining: number | ||||||
|  |   } = { | ||||||
|  |       lifeTime: 2500, | ||||||
|  |       lifeTimeRemaining: 0 | ||||||
|  |     }; | ||||||
|  |   private _collider!: InstanceType<Rapier['rapier']['Collider']>; | ||||||
|  |   private _rigidBody!: InstanceType<Rapier['rapier']['RigidBody']>; | ||||||
|  |   private _rapier: Rapier; | ||||||
|  |   private _alive: boolean = true; | ||||||
|  |  | ||||||
|  |   constructor() { | ||||||
|  |     super(); | ||||||
|  |     this._rapier = Registry.fetch<Rapier>('Rapier'); | ||||||
|  |     this._graphic = new Graphics(); | ||||||
|  |     this._graphic.circle(this._size / 2, this._size / 2, this._size).fill(0xAA0000); | ||||||
|  |     this.addChild(this._graphic); | ||||||
|  |     this.pivot.set(this._size / 2, this._size / 2); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   alive(): boolean { | ||||||
|  |     return this._lifeTime.lifeTimeRemaining > 0 && this._alive; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   fire(vector: Vector): void { | ||||||
|  |     this._alive = true; | ||||||
|  |     this._addPhysics(); | ||||||
|  |     this._rigidBody.setTranslation(this.position, true); | ||||||
|  |     this._rigidBody.setLinvel(vector.multiplyScalar(this._speed), true); | ||||||
|  |     this._lifeTime.lifeTimeRemaining = this._lifeTime.lifeTime; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    * | ||||||
|  |    * @param timeDelta - TODO | ||||||
|  |    */ | ||||||
|  |   update(timeDelta: number): void { | ||||||
|  |     this.position.copyFrom(this._rigidBody.translation()); | ||||||
|  |  | ||||||
|  |     if (this._lifeTime.lifeTimeRemaining > 0) { | ||||||
|  |       this._lifeTime.lifeTimeRemaining = Math.max(0, this._lifeTime.lifeTimeRemaining - timeDelta); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Gets called when we've collided with something.   | ||||||
|  |    * Use to kill the projectile. | ||||||
|  |    * | ||||||
|  |    * @param collided - The Collidable this projectile has hit  | ||||||
|  |    * @param colliding - Are we currently colliding? | ||||||
|  |    */ | ||||||
|  |   collide(collided: Collidable, colliding: boolean): void { | ||||||
|  |     if (colliding) { | ||||||
|  |       this._alive = false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   private _addPhysics(): void { | ||||||
|  |     this._rigidBody = this._rapier.createRigidBody(this, 'Dynamic', false, true); | ||||||
|  |     this._collider = this._rapier.createCollider(this, 'ball', { radius: this._size }, this._rigidBody); | ||||||
|  |     this._collider.setRestitution(0); | ||||||
|  |     this._rapier.addColliderMemberGroups(this._collider, 'playerProjectile'); | ||||||
|  |     this._rapier.addColliderFilterGroups(this._collider, 'enemy', 'wall'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   removePhysics(): void { | ||||||
|  |     this._rapier.removeRigidBody(this._rigidBody); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * - TODO | ||||||
|  |  */ | ||||||
|  | class Forward extends BaseAttack { | ||||||
|  |   private _projectilePool: Pool<ForwardProjectile> = new Pool(ForwardProjectile, 50); | ||||||
|  |   private _activeProjectiles: ForwardProjectile[] = []; | ||||||
|  |   private _coolDown: { | ||||||
|  |     coolDown: number, | ||||||
|  |     coolDownRemaining: number | ||||||
|  |   } = { | ||||||
|  |       coolDown: 250, | ||||||
|  |       coolDownRemaining: 0 | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   fire(state: Parameters<BaseAttack['fire']>[0]): void { | ||||||
|  |     if (this._coolDown.coolDownRemaining > 0) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     const newBullet = this._projectilePool.allocate(); | ||||||
|  |  | ||||||
|  |     const newPoint = this._projectileContainer.toLocal(new Vector(0, 0), this); | ||||||
|  |     newBullet.position.copyFrom(newPoint); | ||||||
|  |     this._projectileContainer.addChild(newBullet); | ||||||
|  |     this._activeProjectiles.push(newBullet); | ||||||
|  |  | ||||||
|  |     newBullet.fire(state.playerVector.clone()); | ||||||
|  |  | ||||||
|  |     this._coolDown.coolDownRemaining = this._coolDown.coolDown; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   update(timeDelta: number): void { | ||||||
|  |     if (this._coolDown.coolDownRemaining > 0) { | ||||||
|  |       this._coolDown.coolDownRemaining = Math.max(0, this._coolDown.coolDownRemaining - timeDelta); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this._activeProjectiles = this._activeProjectiles.filter((projectile) => { | ||||||
|  |       projectile.update(timeDelta); | ||||||
|  |       const alive = projectile.alive(); | ||||||
|  |       if (!alive) { | ||||||
|  |         projectile.removePhysics(); | ||||||
|  |         this._projectileContainer.removeChild(projectile); | ||||||
|  |         this._projectilePool.release(projectile); | ||||||
|  |       } | ||||||
|  |       return alive; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Forward; | ||||||
|  |  | ||||||
							
								
								
									
										31
									
								
								src/games/games/TestGame/components/Trees.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/games/games/TestGame/components/Trees.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import Pool from '@/utils/Pool'; | ||||||
|  | import Container from '@/visual/Container'; | ||||||
|  | import Graphics from '@/visual/Graphics'; | ||||||
|  | import { PIXI } from '@/visual/pixi'; | ||||||
|  |  | ||||||
|  | type TreesSettings = ConstructorParameters<typeof Container>[0] & { | ||||||
|  |   numberOfTrees: number | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class Trees extends Container { | ||||||
|  |   private readonly _allTrees: Graphics[] = new Array<Graphics>(200); | ||||||
|  |   private readonly _treePool: Pool<Graphics>; | ||||||
|  |   constructor(settings: TreesSettings) { | ||||||
|  |     super(settings); | ||||||
|  |     this._treePool = new Pool(Graphics, settings.numberOfTrees); | ||||||
|  |  | ||||||
|  |     for (let i = 0; i < settings.numberOfTrees; i++) { | ||||||
|  |       const tree = this.addChild(this._treePool.allocate()); | ||||||
|  |       const treeColour = ((Math.random() * 80) + 70); | ||||||
|  |       tree.regularPoly(-10, 10, 20, 3).fill({ color: new PIXI.Color([0, (treeColour - 10) / 255, 0]) }); | ||||||
|  |       tree.regularPoly(-10, 0, 20, 3).fill({ color: new PIXI.Color([0, treeColour / 255, 0]) }); | ||||||
|  |       tree.regularPoly(-10, -10, 20, 3).fill({ color: new PIXI.Color([0, (treeColour + 10) / 255, 0]) }); | ||||||
|  |       tree.position.set(Math.random() * 1920, Math.random() * 1080); | ||||||
|  |       tree.cullable = true; | ||||||
|  |       this._allTrees.push(tree); | ||||||
|  |     } | ||||||
|  |     this.cullable = true; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Trees; | ||||||
							
								
								
									
										76
									
								
								src/games/games/TestGame/components/Walls/Walls.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/games/games/TestGame/components/Walls/Walls.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | |||||||
|  | import Collidable from '@/control/Collidable'; | ||||||
|  | import Registry from '@/utils/Registry'; | ||||||
|  | import Container from '@/visual/Container'; | ||||||
|  | import Graphics from '@/visual/Graphics'; | ||||||
|  | import { PIXI } from '@/visual/pixi'; | ||||||
|  |  | ||||||
|  | import type Rapier from '@/utils/Rapier'; | ||||||
|  |  | ||||||
|  | type WallsSettings = ConstructorParameters<typeof Container>[0] & { | ||||||
|  |   numberOfTrees: number | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type wallData = { | ||||||
|  |   graphic: Graphics, | ||||||
|  |   collider: InstanceType<Rapier['rapier']['Collider']>, | ||||||
|  |   rigidBody: InstanceType<Rapier['rapier']['RigidBody']>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * - TODO | ||||||
|  |  */ | ||||||
|  | class Walls extends Collidable { | ||||||
|  |   private _rapier: Rapier; | ||||||
|  |   private _walls: wallData[] = []; | ||||||
|  |  | ||||||
|  |   constructor(settings?: WallsSettings) { | ||||||
|  |     super(settings); | ||||||
|  |     this._rapier = Registry.fetch<Rapier>('Rapier'); | ||||||
|  |     this._walls.push(this._createWall(10, 1110, -10, 540)); | ||||||
|  |     this._walls.push(this._createWall(10, 1110, 1930, 540)); | ||||||
|  |     this._walls.push(this._createWall(1950, 10, 960, -10)); | ||||||
|  |     this._walls.push(this._createWall(1950, 10, 960, 1090)); | ||||||
|  |  | ||||||
|  |     this._walls.forEach((wall) => { | ||||||
|  |       this.addChild(wall.graphic); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    * | ||||||
|  |    * @param width - TODO | ||||||
|  |    * @param height  | ||||||
|  |    * @param x  | ||||||
|  |    * @param y  | ||||||
|  |    */ | ||||||
|  |   private _createWall(width: number, height: number, x: number, y: number): wallData { | ||||||
|  |     const wall = new Graphics(); | ||||||
|  |     wall.rect(-width / 2, -height / 2, width, height).fill({ color: new PIXI.Color([0, 0, 0]) }); | ||||||
|  |     wall.position.set(x, y); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     const rigidBody = this._rapier.createRigidBody(this, 'Fixed'); | ||||||
|  |     const collider = this._rapier.createCollider(this, 'cuboid', { halfHeight: height / 2, halfWidth: width / 2 }, rigidBody); | ||||||
|  |     collider.setRestitution(0); | ||||||
|  |  | ||||||
|  |     rigidBody.setTranslation(this.position.add({ x, y }), true); | ||||||
|  |  | ||||||
|  |     this._rapier.addColliderMemberGroups(collider, 'wall'); | ||||||
|  |     this._rapier.addColliderFilterGroups(collider, 'player', 'enemy', 'enemyProjectile', 'playerProjectile'); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       graphic: wall, | ||||||
|  |       collider, | ||||||
|  |       rigidBody | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   collide(collided: Collidable, colliding: boolean): void { | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Walls; | ||||||
							
								
								
									
										7
									
								
								src/games/lib/control/Collidable.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/games/lib/control/Collidable.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | import Container from '@/visual/Container'; | ||||||
|  |  | ||||||
|  | abstract class Collidable extends Container { | ||||||
|  |   abstract collide(collided: Collidable, colliding: boolean): void; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Collidable; | ||||||
							
								
								
									
										87
									
								
								src/games/lib/control/Control.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/games/lib/control/Control.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | |||||||
|  | type state = 'down' | 'up'; | ||||||
|  |  | ||||||
|  | // - TODO - Pull this from the actual game creating the controls | ||||||
|  | const GameControls = { | ||||||
|  |   down: 'down', | ||||||
|  |   up: 'up', | ||||||
|  |   left: 'left', | ||||||
|  |   right: 'right', | ||||||
|  |   shift: 'shift', | ||||||
|  |   ' ': ' ', | ||||||
|  | } as const; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Abstraction for game controls, the game should say what controls it expects and then we can remap the actual controls separately.   | ||||||
|  |  * i.e. Both "ArrowDown" and "Down" from the keyboard listener should be the games "down" event.   | ||||||
|  |  * Also handles when the keys are pressed or not. | ||||||
|  |  */ | ||||||
|  | class Control { | ||||||
|  |   readonly gameControls = GameControls; | ||||||
|  |   private _controlMap: { [key: string]: keyof typeof GameControls } = { | ||||||
|  |     ArrowDown: GameControls.down, | ||||||
|  |     Down: GameControls.down, | ||||||
|  |     ArrowUp: GameControls.up, | ||||||
|  |     Up: GameControls.up, | ||||||
|  |     ArrowLeft: GameControls.left, | ||||||
|  |     Left: GameControls.left, | ||||||
|  |     ArrowRight: GameControls.right, | ||||||
|  |     Right: GameControls.right, | ||||||
|  |     Shift: GameControls.shift, | ||||||
|  |     ' ': GameControls[' '], | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private _controlState: Map<keyof typeof GameControls, { | ||||||
|  |     state: state, | ||||||
|  |  | ||||||
|  |     consumed: boolean | ||||||
|  |   }> = new Map(); | ||||||
|  |  | ||||||
|  |   constructor() { | ||||||
|  |     this._setKeys(); | ||||||
|  |     window.addEventListener('keydown', (event) => { | ||||||
|  |       if (this._controlState.get(this._controlMap[event.key])?.state !== 'down') { | ||||||
|  |         this._controlState.set(this._controlMap[event.key], { state: 'down', consumed: false }); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     window.addEventListener('keyup', (event) => { | ||||||
|  |       if (this._controlState.get(this._controlMap[event.key])?.state !== 'up') { | ||||||
|  |         this._controlState.set(this._controlMap[event.key], { state: 'up', consumed: false }); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   private _setKeys(): void { | ||||||
|  |     Object.keys(GameControls).forEach((key) => { | ||||||
|  |       this._controlState.set(key as keyof typeof GameControls, { state: 'up', consumed: false }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    * | ||||||
|  |    * @param key - TODO | ||||||
|  |    */ | ||||||
|  |   isDown(key: keyof typeof GameControls): boolean { | ||||||
|  |     return this._controlState.get(key)?.state === 'down'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Consume a down key event once | ||||||
|  |    *  | ||||||
|  |    * @param key  - TODO | ||||||
|  |    */ | ||||||
|  |   onceDown(key: keyof typeof GameControls): boolean { | ||||||
|  |     const keyState = this._controlState.get(key); | ||||||
|  |     if (keyState && keyState.state === 'down' && !keyState.consumed) { | ||||||
|  |       keyState.consumed = true; | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Control; | ||||||
							
								
								
									
										96
									
								
								src/games/lib/core/BaseGameApp/BaseGameApp.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/games/lib/core/BaseGameApp/BaseGameApp.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | |||||||
|  | import { PIXI, Viewport } from '@/visual/pixi'; | ||||||
|  | import Text from '@/visual/Text'; | ||||||
|  | import Rapier from '@/utils/Rapier'; | ||||||
|  | import Registry from '@/utils/Registry'; | ||||||
|  | import Ticker from '@/utils/Ticker'; | ||||||
|  | import Vector from '@/utils/Vector'; | ||||||
|  | import config from './BaseGameAppConfig'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * - TODO | ||||||
|  |  */ | ||||||
|  | class BaseGameApp { | ||||||
|  |   protected readonly _app: PIXI.Application; | ||||||
|  |   protected readonly _ticker: Ticker = new Ticker(); | ||||||
|  |   protected _fps: Text = new Text(config.fps); | ||||||
|  |   protected _viewport!: Viewport; | ||||||
|  |   protected _rapier!: Rapier; | ||||||
|  |  | ||||||
|  |   constructor(canvas: HTMLCanvasElement | null) { | ||||||
|  |     if (canvas === null) { | ||||||
|  |       throw new Error('No canvas'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this._app = new PIXI.Application(); | ||||||
|  |  | ||||||
|  |     void this._start(canvas); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   protected async _start(canvas: HTMLCanvasElement): Promise<void> { | ||||||
|  |     await this._app.init({ | ||||||
|  |       backgroundColor: 0x999999, | ||||||
|  |       canvas: canvas, | ||||||
|  |       antialias: true, | ||||||
|  |       resizeTo: canvas.parentElement as HTMLElement | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     this._app.ticker.autoStart = false; | ||||||
|  |     this._app.ticker.stop(); | ||||||
|  |  | ||||||
|  |     this._viewport = new Viewport({ | ||||||
|  |       screenWidth: this._app.canvas.width, | ||||||
|  |       screenHeight: this._app.canvas.height, | ||||||
|  |       worldWidth: 1920, | ||||||
|  |       worldHeight: 1080, | ||||||
|  |       passiveWheel: false, | ||||||
|  |       noTicker: true, | ||||||
|  |       events: this._app.renderer.events | ||||||
|  |     }); | ||||||
|  |     Registry.register('Viewport', this._viewport); | ||||||
|  |     this._rapier = new Rapier(new Vector(0, 0)); | ||||||
|  |     Registry.register('Rapier', this._rapier); | ||||||
|  |  | ||||||
|  |     this._viewport.ensureVisible(0, 0, 1920, 1080, true); | ||||||
|  |     this._viewport.fitWorld(true); | ||||||
|  |     this._viewport.drag(); | ||||||
|  |  | ||||||
|  |     this._app.stage.addChild(this._viewport); | ||||||
|  |     this._app.stage.addChild(this._fps); | ||||||
|  |  | ||||||
|  |     await this._rapier.setup(); | ||||||
|  |  | ||||||
|  |     this._ticker.ticker(1000 / 60, this._update.bind(this), this._updateComplete.bind(this)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Update the logic of the game   | ||||||
|  |    * - TODO - Figure out if this should be called before or after game updates | ||||||
|  |    * | ||||||
|  |    * @param timeDelta - The time difference since the last update (in ms) | ||||||
|  |    */ | ||||||
|  |   protected async _update(timeDelta: number): Promise<void> { | ||||||
|  |     this._rapier.debugRender(); | ||||||
|  |     this._viewport.update(timeDelta); | ||||||
|  |     this._rapier.update(); | ||||||
|  |     this._fps.text = this._ticker.currentTicksPerSecond; | ||||||
|  |     return Promise.resolve(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * When the update is complete, kick off a render | ||||||
|  |    * | ||||||
|  |    * @param updated - Was there actually an update in the last raf? | ||||||
|  |    */ | ||||||
|  |   protected async _updateComplete(updated: boolean): Promise<void> { | ||||||
|  |     this._viewport.resize(this._app.canvas.width, this._app.canvas.height); | ||||||
|  |     this._viewport.fitWorld(true); | ||||||
|  |     this._app.renderer.render(this._app.stage); | ||||||
|  |     return Promise.resolve(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default BaseGameApp; | ||||||
							
								
								
									
										10
									
								
								src/games/lib/core/BaseGameApp/BaseGameAppConfig.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/games/lib/core/BaseGameApp/BaseGameAppConfig.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | const BaseGameAppConfig = { | ||||||
|  |   fps: { | ||||||
|  |     position: { | ||||||
|  |       x: 20, | ||||||
|  |       y: 20 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } as const; | ||||||
|  |  | ||||||
|  | export default BaseGameAppConfig; | ||||||
							
								
								
									
										31
									
								
								src/games/lib/utils/Pool.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/games/lib/utils/Pool.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import { ObjectPoolFactory } from '@pixi-essentials/object-pool'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * - TODO | ||||||
|  |  */ | ||||||
|  | class Pool<T> { | ||||||
|  |   private _pool: ObjectPoolFactory<T>; | ||||||
|  |   // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||
|  |   constructor(ctor: new (...args: any[]) => T, reserve: number) { | ||||||
|  |     // Unsure why this needs the cast, probably because of the generic | ||||||
|  |     this._pool = ObjectPoolFactory.build(ctor) as ObjectPoolFactory<T>; | ||||||
|  |     this._pool.reserve(reserve); | ||||||
|  |     this._pool.startGC(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   allocate(): T { | ||||||
|  |     return this._pool.allocate(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   release(object: T): void { | ||||||
|  |     this._pool.release(object); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Pool; | ||||||
							
								
								
									
										223
									
								
								src/games/lib/utils/Rapier.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								src/games/lib/utils/Rapier.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,223 @@ | |||||||
|  | import StoredPromise from './StoredPromise'; | ||||||
|  | import Vector from './Vector'; | ||||||
|  | import Collidable from '../control/Collidable'; | ||||||
|  | import _rapier from '@dimforge/rapier2d-compat'; | ||||||
|  | import Graphics from '@/visual/Graphics'; | ||||||
|  | import Registry from '@/utils/Registry'; | ||||||
|  | import { PIXI, type Viewport } from '@/visual/pixi'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * - TODO | ||||||
|  |  * https://rapier.rs/docs/user_guides/javascript/colliders#overview | ||||||
|  |  */ | ||||||
|  | type colliderTypes = { | ||||||
|  |   ball: { | ||||||
|  |     radius: number | ||||||
|  |   }, | ||||||
|  |   cuboid: { | ||||||
|  |     halfHeight: number, | ||||||
|  |     halfWidth: number | ||||||
|  |   }, | ||||||
|  |   capsule: { | ||||||
|  |     halfHeight: number, | ||||||
|  |     radius: number | ||||||
|  |   }, | ||||||
|  |   triangle: { | ||||||
|  |     a: Vector, | ||||||
|  |     b: Vector, | ||||||
|  |     c: Vector | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const colliderGroups = { | ||||||
|  |   'wall': 0b0000_0000_0000_0001, | ||||||
|  |   'player': 0b0000_0000_0001_0000, | ||||||
|  |   'enemy': 0b0000_0000_0010_0000, | ||||||
|  |   'playerProjectile': 0b0000_0001_0000_0000, | ||||||
|  |   'enemyProjectile': 0b0000_0010_0000_0000, | ||||||
|  | } as const; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * - TODO | ||||||
|  |  */ | ||||||
|  | class Rapier { | ||||||
|  |   private _settingUp!: StoredPromise<void>; | ||||||
|  |   private _world!: _rapier.World; | ||||||
|  |   private _eventQueue!: _rapier.EventQueue; | ||||||
|  |   private _rigidBodyMap: Map<number, Collidable> = new Map(); | ||||||
|  |   private _colliderMap: Map<number, Collidable> = new Map(); | ||||||
|  |   private _debugLines: Graphics; | ||||||
|  |  | ||||||
|  |   constructor(gravity: Vector) { | ||||||
|  |     void this._setup(gravity); | ||||||
|  |     this._debugLines = new Graphics(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get rapier(): typeof _rapier { | ||||||
|  |     return _rapier; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get world(): _rapier.World { | ||||||
|  |     return this._world; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Needed as rapier2d is a webasm module that needs to be async imported directly | ||||||
|  |    */ | ||||||
|  |   private async _setup(gravity: Vector): Promise<void> { | ||||||
|  |     this._settingUp = new StoredPromise(); | ||||||
|  |     await this.rapier.init() | ||||||
|  |     // - TODO - Change integration params dt to ticker speed | ||||||
|  |     this._world = new this.rapier.World(gravity); | ||||||
|  |     this._eventQueue = new this.rapier.EventQueue(true); | ||||||
|  |  | ||||||
|  |     const viewport = Registry.fetch<Viewport>('Viewport'); | ||||||
|  |     viewport.addChild(this._debugLines); | ||||||
|  |  | ||||||
|  |     this._settingUp.resolve(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   async setup(): Promise<void> { | ||||||
|  |     await this._settingUp.promise; | ||||||
|  |  | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   createRigidBody(container: Collidable, type: keyof typeof _rapier.RigidBodyType, sleep: boolean = true, CCD: boolean = false): _rapier.RigidBody { | ||||||
|  |     const rigidBodyDesc = new this.rapier.RigidBodyDesc(this.rapier.RigidBodyType[type]) | ||||||
|  |       .setCanSleep(sleep) | ||||||
|  |       .setCcdEnabled(CCD); | ||||||
|  |  | ||||||
|  |     const rigidBody = this._world.createRigidBody(rigidBodyDesc); | ||||||
|  |     this._rigidBodyMap.set(rigidBody.handle, container); | ||||||
|  |     return rigidBody; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   createCollider<T extends keyof colliderTypes, U extends colliderTypes[T]>(container: Collidable, type: T, settings: U, rigidBody?: _rapier.RigidBody): _rapier.Collider { | ||||||
|  |     let colliderDesc: _rapier.ColliderDesc; | ||||||
|  |     if (type === 'ball') { | ||||||
|  |       const castSettings = (settings as colliderTypes['ball']); | ||||||
|  |       colliderDesc = new this.rapier.ColliderDesc(new this.rapier.Ball(castSettings.radius)); | ||||||
|  |     } else if (type === 'capsule') { | ||||||
|  |       const castSettings = (settings as colliderTypes['capsule']); | ||||||
|  |       colliderDesc = new this.rapier.ColliderDesc(new this.rapier.Capsule(castSettings.halfHeight, castSettings.radius)); | ||||||
|  |     } else if (type === 'cuboid') { | ||||||
|  |       const castSettings = (settings as colliderTypes['cuboid']); | ||||||
|  |       colliderDesc = new this.rapier.ColliderDesc(new this.rapier.Cuboid(castSettings.halfWidth, castSettings.halfHeight)); | ||||||
|  |     } else if (type === 'triangle') { | ||||||
|  |       const castSettings = (settings as colliderTypes['triangle']); | ||||||
|  |       colliderDesc = new this.rapier.ColliderDesc(new this.rapier.Triangle(castSettings.a, castSettings.b, castSettings.c)); | ||||||
|  |     } else { | ||||||
|  |       throw new Error(`Cannot create collider of type ${type}`); | ||||||
|  |     } | ||||||
|  |     colliderDesc.setActiveEvents(this.rapier.ActiveEvents.COLLISION_EVENTS); | ||||||
|  |     colliderDesc.setActiveCollisionTypes(this.rapier.ActiveCollisionTypes.DEFAULT); | ||||||
|  |  | ||||||
|  |     const collider = this._world.createCollider(colliderDesc, rigidBody); | ||||||
|  |     collider.setCollisionGroups(0); | ||||||
|  |     collider.setFriction(0); | ||||||
|  |  | ||||||
|  |     this._colliderMap.set(collider.handle, container); | ||||||
|  |  | ||||||
|  |     return collider; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   removeRigidBody(rigidBody: _rapier.RigidBody): void { | ||||||
|  |     if (rigidBody) { | ||||||
|  |       this._world.removeRigidBody(rigidBody); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   removeCollider(collider: _rapier.Collider): void { | ||||||
|  |     if (collider) { | ||||||
|  |       this._world.removeCollider(collider, true); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Add the collider to member groups.   | ||||||
|  |    * A member group is what groups we're in (i.e. so other groups can collide with us). | ||||||
|  |    * | ||||||
|  |    * @param collider - The collider to add the groups to | ||||||
|  |    * @param groups - The groups to add to | ||||||
|  |    */ | ||||||
|  |   addColliderMemberGroups(collider: _rapier.Collider, ...groups: (keyof typeof colliderGroups)[]): void { | ||||||
|  |     const bitmask = this._getColliderGroupsBitmask(groups) << 16; | ||||||
|  |     collider.setCollisionGroups(collider.collisionGroups() | bitmask); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Add the collider to filter groups.   | ||||||
|  |    * A filter group is what groups we can collide with. | ||||||
|  |    * | ||||||
|  |    * @param collider - The collider to add the groups to | ||||||
|  |    * @param groups - The groups to add to | ||||||
|  |    */ | ||||||
|  |   addColliderFilterGroups(collider: _rapier.Collider, ...groups: (keyof typeof colliderGroups)[]): void { | ||||||
|  |     const bitmask = this._getColliderGroupsBitmask(groups); | ||||||
|  |     collider.setCollisionGroups(collider.collisionGroups() | bitmask); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   private _getColliderGroupsBitmask(groups: (keyof typeof colliderGroups)[]): number { | ||||||
|  |     return groups.reduce((bitmask, group) => { | ||||||
|  |       return bitmask | colliderGroups[group]; | ||||||
|  |     }, 0); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    */ | ||||||
|  |   update(): void { | ||||||
|  |     this._world.step(this._eventQueue); | ||||||
|  |     this._eventQueue.drainCollisionEvents((handle1, handle2, colliding) => { | ||||||
|  |       const container1 = this._colliderMap.get(handle1); | ||||||
|  |       const container2 = this._colliderMap.get(handle2); | ||||||
|  |       if (container1 && container2) { | ||||||
|  |         container1.collide(container2, colliding); | ||||||
|  |         container2.collide(container1, colliding); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   debugRender(): void { | ||||||
|  |     const { vertices, colors } = this._world.debugRender(); | ||||||
|  |  | ||||||
|  |     this._debugLines.clear(); | ||||||
|  |  | ||||||
|  |     for (let i = 0; i < vertices.length / 4; i += 1) { | ||||||
|  |       let color = new PIXI.Color([ | ||||||
|  |         colors[i * 8], | ||||||
|  |         colors[i * 8 + 1], | ||||||
|  |         colors[i * 8 + 2], | ||||||
|  |       ]); | ||||||
|  |       this._debugLines.setStrokeStyle({ | ||||||
|  |         color: color, | ||||||
|  |         width: 2, | ||||||
|  |         alignment: 0.5, | ||||||
|  |       }) | ||||||
|  |       this._debugLines.moveTo(vertices[i * 4], vertices[i * 4 + 1]); | ||||||
|  |       this._debugLines.lineTo(vertices[i * 4 + 2], vertices[i * 4 + 3]); | ||||||
|  |       this._debugLines.stroke(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Rapier; | ||||||
							
								
								
									
										32
									
								
								src/games/lib/utils/Registry.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/games/lib/utils/Registry.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | /** | ||||||
|  |  * - TODO | ||||||
|  |  */ | ||||||
|  | class Registry { | ||||||
|  |   static _objects: Map<string, unknown> = new Map(); | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    * | ||||||
|  |    * @param name - TODO | ||||||
|  |    * @param obj - TODO | ||||||
|  |    */ | ||||||
|  |   static register<T>(name: string, obj: T): void { | ||||||
|  |     this._objects.set(name, obj); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * - TODO | ||||||
|  |    * | ||||||
|  |    * @param name - TODO | ||||||
|  |    */ | ||||||
|  |   static fetch<T>(name: string): T { | ||||||
|  |     const obj = this._objects.get(name) as T; | ||||||
|  |     if (obj) { | ||||||
|  |       return obj; | ||||||
|  |     } else { | ||||||
|  |       throw new Error(`Cannot find ${name}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Registry; | ||||||
							
								
								
									
										31
									
								
								src/games/lib/utils/StoredPromise.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/games/lib/utils/StoredPromise.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | class StoredPromise<T = void> { | ||||||
|  |   promise: null | Promise<T>; | ||||||
|  |   private _resolve: null | (( value: T | PromiseLike<T> ) => void); | ||||||
|  |   // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||
|  |   private _reject: null | (( reason?: any ) => void); | ||||||
|  |   constructor() { | ||||||
|  |     this.promise = new Promise<T>((resolve, reject) => { | ||||||
|  |       this._resolve = resolve; | ||||||
|  |       this._reject = reject; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   resolve(value: T | PromiseLike<T>): void { | ||||||
|  |     this._resolve?.(value); | ||||||
|  |     this.clear(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||
|  |   reject( reason?: any ): void { | ||||||
|  |     this._reject?.(reason); | ||||||
|  |     this.clear(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   clear(): void { | ||||||
|  |     this.promise = null; | ||||||
|  |     this._resolve = null; | ||||||
|  |     this._reject = null; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default StoredPromise; | ||||||
							
								
								
									
										75
									
								
								src/games/lib/utils/Ticker.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/games/lib/utils/Ticker.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | |||||||
|  | /** | ||||||
|  |  * A ticker class to call a callback on a set time\ | ||||||
|  |  * Is catch-up tick based so will call update multiple times | ||||||
|  |  */ | ||||||
|  | class Ticker { | ||||||
|  |   private _tickUpdateCb: (timeDelta: number) => Promise<void> = async () => {}; | ||||||
|  |   private _tickCompleteCb: (updated: boolean) => Promise<void> = async () => {}; | ||||||
|  |   private _voidUpdate: ( frameTime: number ) => void; | ||||||
|  |   private _tickCounter: number[] = []; | ||||||
|  |   private _tickTime = 0; | ||||||
|  |   private _lastFrameTime = 0; | ||||||
|  |   private _currentTotalDelta = 0; | ||||||
|  |   private _ticksPerSecond = 0; | ||||||
|  |  | ||||||
|  |   constructor() { | ||||||
|  |     this._voidUpdate = (frameTime: number): void => { | ||||||
|  |       void this._update(frameTime); | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get currentTicksPerSecond(): number { | ||||||
|  |     return this._ticksPerSecond; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Start the ticker to update every tickTime\ | ||||||
|  |    * Will use requestAnimationFrame and catch up if needed\ | ||||||
|  |    * Use the tick callback for logic updates and the complete callback for visual representations of that logic | ||||||
|  |    */ | ||||||
|  |   ticker(tickTime: number, tickCb: (timeDelta: number) => Promise<void>, completeCb: (updated: boolean) => Promise<void>): void { | ||||||
|  |     this._tickTime = tickTime; | ||||||
|  |     this._lastFrameTime = performance.now(); | ||||||
|  |     this._tickUpdateCb = tickCb; | ||||||
|  |     this._tickCompleteCb = completeCb; | ||||||
|  |     requestAnimationFrame(this._voidUpdate); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * The update loop on raf, call the update multiple times to catch up, then call raf again | ||||||
|  |    */ | ||||||
|  |   private async _update(currentFrameTime: number): Promise<void> { | ||||||
|  |     const delta = currentFrameTime - this._lastFrameTime; | ||||||
|  |     this._lastFrameTime = currentFrameTime; | ||||||
|  |     this._currentTotalDelta += delta; | ||||||
|  |  | ||||||
|  |     let updated = false; | ||||||
|  |     /** | ||||||
|  |      * - TODO - This min number probably needs to change, and reset the time differently | ||||||
|  |      * This way will reset on the next frame, do we want to allow a min frame for update/render purposes but run all the ticks? | ||||||
|  |      * Probably depends on the game. | ||||||
|  |      */ | ||||||
|  |     let ticks = Math.min(Math.floor(this._currentTotalDelta / this._tickTime), 100); | ||||||
|  |     this._currentTotalDelta = this._currentTotalDelta % this._tickTime; | ||||||
|  |     while (ticks > 0) { | ||||||
|  |       updated = true; | ||||||
|  |       ticks -= 1; | ||||||
|  |       await this._tickUpdateCb(this._tickTime); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (updated) { | ||||||
|  |       this._tickCounter.push(currentFrameTime); | ||||||
|  |       const minusOneSecond = currentFrameTime - 1000; | ||||||
|  |       while (this._tickCounter[0] < minusOneSecond) { | ||||||
|  |         this._tickCounter.shift(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       this._ticksPerSecond = this._tickCounter.length; | ||||||
|  |     } | ||||||
|  |     await this._tickCompleteCb(updated); | ||||||
|  |  | ||||||
|  |     requestAnimationFrame(this._voidUpdate); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Ticker; | ||||||
							
								
								
									
										7
									
								
								src/games/lib/utils/Vector.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/games/lib/utils/Vector.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | import { PIXI } from '@/visual/pixi'; | ||||||
|  |  | ||||||
|  | class Vector extends PIXI.Point { | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Vector; | ||||||
							
								
								
									
										9
									
								
								src/games/lib/utils/pixi-object-pool.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/games/lib/utils/pixi-object-pool.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | declare module '@pixi-essentials/object-pool' { | ||||||
|  |   class ObjectPoolFactory<T> { | ||||||
|  |     static build: (ctor: new (args: unknown[]) => T) => ObjectPoolFactory<T>; | ||||||
|  |     reserve: (reserve: number) => void; | ||||||
|  |     startGC: () => void; | ||||||
|  |     allocate: () => T; | ||||||
|  |     release: (obj: T) => void; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								src/games/lib/visual/Container.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/games/lib/visual/Container.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | import Vector from '@/utils/Vector'; | ||||||
|  | import { PIXI } from './pixi'; | ||||||
|  | import { VisualBase, VisualBaseSettings } from './VisualBase'; | ||||||
|  |  | ||||||
|  | type ContainerSettings = VisualBaseSettings; | ||||||
|  |  | ||||||
|  | class Container extends PIXI.Container { | ||||||
|  |   constructor(settings?: ContainerSettings) { | ||||||
|  |     super(); | ||||||
|  |     VisualBase.applySettings(this, settings); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Switch this container's parent to a new parent, keeping it's positioning | ||||||
|  |    */ | ||||||
|  |   addToNewParent(newParent: Container): void { | ||||||
|  |     const localPoint = newParent.toLocal(new Vector(0, 0), this); | ||||||
|  |     this.position.set(localPoint.x, localPoint.y); | ||||||
|  |     newParent.addChild(this); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Container; | ||||||
							
								
								
									
										13
									
								
								src/games/lib/visual/Graphics.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/games/lib/visual/Graphics.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | import { PIXI } from './pixi'; | ||||||
|  | import { VisualBase, VisualBaseSettings } from './VisualBase'; | ||||||
|  |  | ||||||
|  | type GraphicsSettings = VisualBaseSettings; | ||||||
|  |  | ||||||
|  | class Graphics extends PIXI.Graphics { | ||||||
|  |   constructor(settings?: GraphicsSettings ) { | ||||||
|  |     super(); | ||||||
|  |     VisualBase.applySettings(this, settings); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Graphics; | ||||||
							
								
								
									
										29
									
								
								src/games/lib/visual/Text.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/games/lib/visual/Text.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | import { assign } from 'radash'; | ||||||
|  | import { PIXI } from './pixi'; | ||||||
|  | import { VisualBase, VisualBaseSettings } from './VisualBase'; | ||||||
|  |  | ||||||
|  | type TextSettings = VisualBaseSettings & { | ||||||
|  |   text?: string | number, | ||||||
|  |   style?: string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class Text extends PIXI.Text { | ||||||
|  |   static styles: { [key: string]: (Partial<PIXI.TextStyle> | PIXI.TextStyle) } = { | ||||||
|  |   default: { | ||||||
|  |       fontSize: 16, | ||||||
|  |       fill: 0x000000, | ||||||
|  |       align: 'center', | ||||||
|  |        | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   constructor(settings?: TextSettings, style?: Partial<PIXI.TextStyle> | PIXI.TextStyle) { | ||||||
|  |     super({ | ||||||
|  |       text: settings?.text, | ||||||
|  |       style: assign(Text.styles[settings?.style ?? 'default'], style ?? {}) | ||||||
|  |     }) | ||||||
|  |     VisualBase.applySettings(this, settings); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Text; | ||||||
							
								
								
									
										93
									
								
								src/games/lib/visual/VisualBase.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/games/lib/visual/VisualBase.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | |||||||
|  | import { PIXI } from './pixi'; | ||||||
|  |  | ||||||
|  | type pointOrNumber = { | ||||||
|  |   x: number, | ||||||
|  |   y: number | ||||||
|  | } | number | undefined | PIXI.Point; | ||||||
|  |  | ||||||
|  | type VisualBaseSettings = { | ||||||
|  |   angle?: number, | ||||||
|  |   pivot?: pointOrNumber | ||||||
|  |   position?: { | ||||||
|  |     x: number, | ||||||
|  |     y: number | ||||||
|  |   }, | ||||||
|  |   rotation?: number, | ||||||
|  |   scale?: pointOrNumber | ||||||
|  |   skew?: pointOrNumber, | ||||||
|  |   width?: number, | ||||||
|  |   height?: number, | ||||||
|  |   alpha?: number, | ||||||
|  |   visible?: boolean, | ||||||
|  |   interactive?: boolean, | ||||||
|  |   zIndex?: number | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * - TODO | ||||||
|  |  */ | ||||||
|  | class VisualBase { | ||||||
|  |   static applySettings<T extends PIXI.Container>(applyTo: T, settings?: VisualBaseSettings): void { | ||||||
|  |     if (settings?.angle) { | ||||||
|  |       applyTo.angle = settings.angle; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (settings?.position) { | ||||||
|  |       applyTo?.position.set(settings.position.x, settings.position.y); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (settings?.rotation) { | ||||||
|  |       applyTo.rotation = settings.rotation; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (settings?.interactive) { | ||||||
|  |       applyTo.interactive = settings.interactive; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (settings?.zIndex) { | ||||||
|  |       applyTo.zIndex = settings.zIndex; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (settings?.alpha) { | ||||||
|  |       applyTo.alpha = settings.alpha; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (settings?.visible) { | ||||||
|  |       applyTo.visible = settings.visible; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (settings?.position) { | ||||||
|  |       applyTo?.position.set(settings.position.x, settings.position.y); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     applyPointPrimitiveOrObject('pivot', applyTo, settings?.pivot); | ||||||
|  |     applyPointPrimitiveOrObject('scale', applyTo, settings?.scale); | ||||||
|  |     applyPointPrimitiveOrObject('skew', applyTo, settings?.skew); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * - TODO | ||||||
|  |  * | ||||||
|  |  * @param prop - TODO | ||||||
|  |  * @param applyTo - TODO | ||||||
|  |  * @param setting  - TODO | ||||||
|  |  */ | ||||||
|  | function applyPointPrimitiveOrObject<T extends PIXI.Container>(prop: 'skew' | 'pivot' | 'scale', applyTo: T, setting: pointOrNumber): void { | ||||||
|  |   if (setting) { | ||||||
|  |     if (typeof setting === 'object') { | ||||||
|  |       applyTo[prop].set(setting.x, setting.y); | ||||||
|  |     } else { | ||||||
|  |       applyTo[prop].set(setting); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export { | ||||||
|  |   VisualBase | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export type { | ||||||
|  |   VisualBaseSettings | ||||||
|  | }; | ||||||
							
								
								
									
										11
									
								
								src/games/lib/visual/pixi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/games/lib/visual/pixi.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | // - TODO - Clean this up | ||||||
|  | import * as PIXI from 'pixi.js'; | ||||||
|  | import * as Events from '@pixi/events'; | ||||||
|  | import { Viewport } from 'pixi-viewport'; | ||||||
|  |  | ||||||
|  | import 'pixi.js/math-extras'; | ||||||
|  |  | ||||||
|  | // Filters | ||||||
|  | import * as Filters from 'pixi-filters'; | ||||||
|  |  | ||||||
|  | export { PIXI, Events, Viewport, Filters }; | ||||||
| @@ -27,7 +27,19 @@ | |||||||
|     "paths": { |     "paths": { | ||||||
|       "@/*": [ |       "@/*": [ | ||||||
|         "./src/*" |         "./src/*" | ||||||
|       ] |       ], | ||||||
|  |       "@/control/*": [ | ||||||
|  |         "./src/games/lib/control/*" | ||||||
|  |       ], | ||||||
|  |       "@/core/*": [ | ||||||
|  |         "./src/games/lib/core/*" | ||||||
|  |       ], | ||||||
|  |       "@/utils/*": [ | ||||||
|  |         "./src/games/lib/utils/*" | ||||||
|  |       ], | ||||||
|  |       "@/visual/*": [ | ||||||
|  |         "./src/games/lib/visual/*" | ||||||
|  |       ], | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "include": [ |   "include": [ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user