Something something games
This commit is contained in:
		| @@ -7,6 +7,9 @@ const nextConfig = { | ||||
|   experimental: { | ||||
|     reactCompiler: true, | ||||
|     ppr: "incremental", | ||||
|     turbo: { | ||||
|        | ||||
|     } | ||||
|   }, | ||||
|   serverExternalPackages: ["typeorm", "better-sqlite3"], | ||||
|   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", | ||||
|   "private": true, | ||||
|   "scripts": { | ||||
|     "dev": "next dev", | ||||
|     "dev": "next dev --turbopack", | ||||
|     "build": "next build", | ||||
|     "build:analyse": "ANALYZE=true npm run build", | ||||
|     "start": "next start", | ||||
| @@ -11,51 +11,59 @@ | ||||
|     "lint:fix": "next lint --fix" | ||||
|   }, | ||||
|   "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", | ||||
|     "@mdx-js/loader": "^3.1.0", | ||||
|     "@mdx-js/react": "^3.1.0", | ||||
|     "@next/bundle-analyzer": "^15.1.0", | ||||
|     "@next/mdx": "^15.1.0", | ||||
|     "@tailwindcss/typography": "^0.5.15", | ||||
|     "@tanstack/react-query": "^5.62.7", | ||||
|     "@tanstack/react-virtual": "^3.11.1", | ||||
|     "@trpc/client": "^11.0.0-rc.660", | ||||
|     "@trpc/react-query": "^11.0.0-rc.660", | ||||
|     "@trpc/server": "^11.0.0-rc.660", | ||||
|     "@next/bundle-analyzer": "^15.1.7", | ||||
|     "@next/mdx": "^15.1.7", | ||||
|     "@pixi-essentials/object-pool": "^1.0.1", | ||||
|     "@pixi/events": "^7.4.2", | ||||
|     "@tailwindcss/postcss": "^4.0.8", | ||||
|     "@tailwindcss/typography": "^0.5.16", | ||||
|     "@tanstack/react-query": "^5.66.9", | ||||
|     "@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/mdx": "^2.0.13", | ||||
|     "@types/node": "^22.10.2", | ||||
|     "@types/react": "^19.0.1", | ||||
|     "@types/react-dom": "^19.0.2", | ||||
|     "@typescript-eslint/eslint-plugin": "^8.18.0", | ||||
|     "@types/node": "^22.13.5", | ||||
|     "@types/react": "^19.0.10", | ||||
|     "@types/react-dom": "^19.0.4", | ||||
|     "@typescript-eslint/eslint-plugin": "^8.14.1", | ||||
|     "autoprefixer": "^10.4.20", | ||||
|     "babel-plugin-react-compiler": "beta", | ||||
|     "better-sqlite3": "^11.7.0", | ||||
|     "better-sqlite3": "^11.8.1", | ||||
|     "client-only": "^0.0.1", | ||||
|     "drizzle-kit": "^0.30.1", | ||||
|     "drizzle-orm": "^0.38.2", | ||||
|     "eslint": "^9.17.0", | ||||
|     "eslint-config-next": "^15.1.0", | ||||
|     "exif-reader": "^2.0.1", | ||||
|     "framer-motion": "^11.14.4", | ||||
|     "glob": "^11.0.0", | ||||
|     "drizzle-kit": "^0.30.4", | ||||
|     "drizzle-orm": "^0.39.3", | ||||
|     "eslint": "^9.21.0", | ||||
|     "eslint-config-next": "^15.1.7", | ||||
|     "eventemitter3": "^5.0.1", | ||||
|     "exif-reader": "^2.0.2", | ||||
|     "framer-motion": "^12.4.7", | ||||
|     "glob": "^11.0.1", | ||||
|     "million": "^3.1.11", | ||||
|     "next": "15.1.1-canary.5", | ||||
|     "next": "15.2.0-canary.69", | ||||
|     "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", | ||||
|     "react": "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", | ||||
|     "server-only": "^0.0.1", | ||||
|     "sharp": "^0.33.5", | ||||
|     "superjson": "^2.2.2", | ||||
|     "tailwind-scrollbar": "^3.1.0", | ||||
|     "tailwindcss": "^3.4.16", | ||||
|     "typescript": "^5.7.2", | ||||
|     "tailwind-scrollbar": "^4.0.0", | ||||
|     "tailwindcss": "^4.0.8", | ||||
|     "typescript": "^5.7.3", | ||||
|     "yet-another-react-lightbox": "^3.21.7", | ||||
|     "zod": "^3.24.1" | ||||
|     "zod": "^3.24.2" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,3 @@ | ||||
| module.exports = { | ||||
|   plugins: { | ||||
|     tailwindcss: {}, | ||||
|     autoprefixer: {}, | ||||
|   }, | ||||
|   plugins: ["@tailwindcss/postcss"], | ||||
| }; | ||||
|   | ||||
							
								
								
									
										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; | ||||
| @tailwind components; | ||||
| @tailwind utilities; | ||||
| @import "tailwindcss"; | ||||
|  | ||||
| @layer utilities { | ||||
|   .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": { | ||||
|       "@/*": [ | ||||
|         "./src/*" | ||||
|       ] | ||||
|       ], | ||||
|       "@/control/*": [ | ||||
|         "./src/games/lib/control/*" | ||||
|       ], | ||||
|       "@/core/*": [ | ||||
|         "./src/games/lib/core/*" | ||||
|       ], | ||||
|       "@/utils/*": [ | ||||
|         "./src/games/lib/utils/*" | ||||
|       ], | ||||
|       "@/visual/*": [ | ||||
|         "./src/games/lib/visual/*" | ||||
|       ], | ||||
|     } | ||||
|   }, | ||||
|   "include": [ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user