diff --git a/config/ConfigurationServer/.dockerignore b/config/ConfigurationServer/.dockerignore deleted file mode 100644 index 3729ff0..0000000 --- a/config/ConfigurationServer/.dockerignore +++ /dev/null @@ -1,25 +0,0 @@ -**/.classpath -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md \ No newline at end of file diff --git a/config/ConfigurationServer/.gitignore b/config/ConfigurationServer/.gitignore deleted file mode 100644 index bd7da14..0000000 --- a/config/ConfigurationServer/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -bin -obj -.vs -*.user diff --git a/config/ConfigurationServer/ConfigurationServer.csproj b/config/ConfigurationServer/ConfigurationServer.csproj deleted file mode 100644 index f5db55a..0000000 --- a/config/ConfigurationServer/ConfigurationServer.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - netcoreapp3.1 - Linux - . - - - - - - - - diff --git a/config/ConfigurationServer/ConfigurationServer.sln b/config/ConfigurationServer/ConfigurationServer.sln deleted file mode 100644 index 3715c7a..0000000 --- a/config/ConfigurationServer/ConfigurationServer.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30309.148 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfigurationServer", "ConfigurationServer.csproj", "{CD78C1FA-1A88-4299-961B-2B5424DBEEDF}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {CD78C1FA-1A88-4299-961B-2B5424DBEEDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CD78C1FA-1A88-4299-961B-2B5424DBEEDF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CD78C1FA-1A88-4299-961B-2B5424DBEEDF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CD78C1FA-1A88-4299-961B-2B5424DBEEDF}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {5CE906F0-BAD5-4BD3-B3D7-1D26E4D6385C} - EndGlobalSection -EndGlobal diff --git a/config/ConfigurationServer/Controllers/WeatherForecastController.cs b/config/ConfigurationServer/Controllers/WeatherForecastController.cs deleted file mode 100644 index 3ea0014..0000000 --- a/config/ConfigurationServer/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -namespace ConfigurationServer.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet] - public IEnumerable Get() - { - var rng = new Random(); - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateTime.Now.AddDays(index), - TemperatureC = rng.Next(-20, 55), - Summary = Summaries[rng.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} diff --git a/config/ConfigurationServer/Dockerfile b/config/ConfigurationServer/Dockerfile deleted file mode 100644 index 26db67a..0000000 --- a/config/ConfigurationServer/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. - -FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base -WORKDIR /app -EXPOSE 80 - -FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build -WORKDIR /src -COPY ["ConfigurationServer.csproj", ""] -RUN dotnet restore "./ConfigurationServer.csproj" -COPY . . -WORKDIR "/src/." -RUN dotnet build "ConfigurationServer.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "ConfigurationServer.csproj" -c Release -o /app/publish - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "ConfigurationServer.dll"] \ No newline at end of file diff --git a/config/ConfigurationServer/Program.cs b/config/ConfigurationServer/Program.cs deleted file mode 100644 index 7459de3..0000000 --- a/config/ConfigurationServer/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace ConfigurationServer -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} diff --git a/config/ConfigurationServer/Properties/launchSettings.json b/config/ConfigurationServer/Properties/launchSettings.json deleted file mode 100644 index e54db27..0000000 --- a/config/ConfigurationServer/Properties/launchSettings.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:63198", - "sslPort": 0 - } - }, - "$schema": "http://json.schemastore.org/launchsettings.json", - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "weatherforecast", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "ConfigurationServer": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "weatherforecast", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:5000" - }, - "Docker": { - "commandName": "Docker", - "launchBrowser": true, - "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/weatherforecast", - "publishAllPorts": true - } - } -} \ No newline at end of file diff --git a/config/ConfigurationServer/Startup.cs b/config/ConfigurationServer/Startup.cs deleted file mode 100644 index 5268c56..0000000 --- a/config/ConfigurationServer/Startup.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace ConfigurationServer -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - } -} diff --git a/config/ConfigurationServer/WeatherForecast.cs b/config/ConfigurationServer/WeatherForecast.cs deleted file mode 100644 index e4664f1..0000000 --- a/config/ConfigurationServer/WeatherForecast.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace ConfigurationServer -{ - public class WeatherForecast - { - public DateTime Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string Summary { get; set; } - } -} diff --git a/config/ConfigurationServer/appsettings.Development.json b/config/ConfigurationServer/appsettings.Development.json deleted file mode 100644 index 8983e0f..0000000 --- a/config/ConfigurationServer/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/config/ConfigurationServer/appsettings.json b/config/ConfigurationServer/appsettings.json deleted file mode 100644 index d9d9a9b..0000000 --- a/config/ConfigurationServer/appsettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} diff --git a/frontend/.firebaserc b/frontend/.firebaserc index 1c74fcc..2f95982 100644 --- a/frontend/.firebaserc +++ b/frontend/.firebaserc @@ -1,5 +1,5 @@ { "projects": { - "default": "decla-red" + "default": "decla-red } -} +} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 804df07..ed8c3a3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -60,5 +60,8 @@ "webpack": "^4.43.0", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.10.3" + }, + "dependencies": { + "firebase": "^7.22.0" } } diff --git a/frontend/src/index.html b/frontend/src/index.html index b9450ca..cb79086 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -12,7 +12,7 @@ - Decla.red + decla.red diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 2035f62..b148ed8 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -1,4 +1,5 @@ import { glMatrix } from 'gl-matrix'; +import { Configuration } from './scripts/config/configuration'; import { Game } from './scripts/game'; import { Random } from './scripts/helper/random'; import './styles/main.scss'; @@ -6,6 +7,8 @@ import './styles/main.scss'; glMatrix.setMatrixArrayType(Array); const main = async () => { + await Configuration.initialize(); + try { Random.seed = 42; await new Game().start(); diff --git a/frontend/src/scripts/config/configuration.ts b/frontend/src/scripts/config/configuration.ts new file mode 100644 index 0000000..38d07e1 --- /dev/null +++ b/frontend/src/scripts/config/configuration.ts @@ -0,0 +1,30 @@ +import * as firebase from 'firebase/app'; +import 'firebase/firebase-remote-config'; + +export class Configuration { + public static async initialize(): Promise { + const firebaseConfig = { + apiKey: 'AIzaSyBG85dp-AhaCW-qi_6mu77wDPSipzipIF4', + authDomain: 'decla-red.firebaseapp.com', + projectId: 'decla-red', + appId: '1:635208271441:web:c910843ae7e0549dadda70', + }; + + firebase.initializeApp(firebaseConfig); + + const remoteConfig = firebase.remoteConfig(); + remoteConfig.defaultConfig = { + online_servers: 'hi', + }; + + remoteConfig.settings = { + minimumFetchIntervalMillis: 3600 * 1000, + fetchTimeoutMillis: 15 * 1000, + } as any; + + await remoteConfig.ensureInitialized(); + await remoteConfig.fetchAndActivate(); + + console.log(remoteConfig.getValue('online_servers')); + } +} diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index 3001782..2d2434c 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -1,10 +1,16 @@ import { vec2 } from 'gl-matrix'; -import { CircleLight, compile, Flashlight, InvertedTunnel, Renderer } from 'sdf-2d'; +import { + Circle, + CircleLight, + compile, + Flashlight, + InvertedTunnel, + Renderer, +} from 'sdf-2d'; import { CommandBroadcaster } from './commands/command-broadcaster'; import { MoveToCommand } from './commands/move-to'; import { RenderCommand } from './commands/render'; import { StepCommand } from './commands/step'; -import { TeleportToCommand } from './commands/teleport-to'; import { DeltaTimeCalculator } from './helper/delta-time-calculator'; import { prettyPrint } from './helper/pretty-print'; import { rgb } from './helper/rgb'; @@ -12,6 +18,7 @@ import { IGame } from './i-game'; import { KeyboardListener } from './input/keyboard-listener'; import { MouseListener } from './input/mouse-listener'; import { TouchListener } from './input/touch-listener'; +import { GameObject } from './objects/game-object'; import { Objects } from './objects/objects'; import { Camera } from './objects/types/camera'; import { Character } from './objects/types/character'; @@ -49,17 +56,21 @@ export class Game implements IGame { ...InvertedTunnel.descriptor, shaderCombinationSteps: [0, 2, 4, 8, 16], }, - { - ...Flashlight.descriptor, - shaderCombinationSteps: [0, 1], - }, { ...BlobShape.descriptor, shaderCombinationSteps: [0, 1], }, + { + ...Circle.descriptor, + shaderCombinationSteps: [0, 2, 4, 8, 16], + }, { ...CircleLight.descriptor, - shaderCombinationSteps: [0, 1, 2, 4, 8, 16], + shaderCombinationSteps: [0, 1, 2, 4, 8], + }, + { + ...Flashlight.descriptor, + shaderCombinationSteps: [0, 1], }, ], { @@ -68,8 +79,6 @@ export class Game implements IGame { enableStopwatch: true, } ); - this.initializeScene(); - this.physics.start(); } public async start(): Promise { @@ -89,6 +98,8 @@ export class Game implements IGame { enableHighDpiRendering: false, lightCutoffDistance: settings.lightCutoffDistance, }); + this.initializeScene(); + this.physics.start(); requestAnimationFrame(this.gameLoop.bind(this)); } @@ -100,6 +111,10 @@ export class Game implements IGame { return this.physics.findIntersecting(box); } + public displayToWorldCoordinates(p: vec2): vec2 { + return this.renderer.displayToWorldCoordinates(p); + } + private initializeScene() { createDungeon(this.objects, this.physics); createDungeon(this.objects, this.physics); @@ -109,9 +124,6 @@ export class Game implements IGame { this.character = new Character(this.physics, this); this.objects.addObject(this.character); this.objects.addObject(this.camera); - let pos: any = localStorage.getItem('characterPosition'); - pos = pos ? JSON.parse(pos) : vec2.fromValues(0, 0); - this.character.sendCommand(new TeleportToCommand(pos)); } private deltaTimeCalculator = new DeltaTimeCalculator(); @@ -134,7 +146,14 @@ export class Game implements IGame { this.renderer.renderDrawables(); this.overlay.innerText = prettyPrint(this.renderer.insights); - localStorage.setItem('characterPosition', JSON.stringify(this.character.position)); requestAnimationFrame(this.gameLoop.bind(this)); } + + public addObject(o: GameObject) { + this.objects.addObject(o); + } + + public removeObject(o: GameObject) { + this.objects.removeObject(o); + } } diff --git a/frontend/src/scripts/i-game.ts b/frontend/src/scripts/i-game.ts index d7e1c40..13b5c82 100644 --- a/frontend/src/scripts/i-game.ts +++ b/frontend/src/scripts/i-game.ts @@ -1,6 +1,11 @@ +import { vec2 } from 'gl-matrix'; +import { GameObject } from './objects/game-object'; import { BoundingBoxBase } from './physics/bounds/bounding-box-base'; export interface IGame { readonly viewArea: BoundingBoxBase; findIntersecting(box: BoundingBoxBase): Array; + displayToWorldCoordinates(p: vec2): vec2; + addObject(o: GameObject): void; + removeObject(o: GameObject): void; } diff --git a/frontend/src/scripts/input/mouse-listener.ts b/frontend/src/scripts/input/mouse-listener.ts index 1120079..42eac13 100644 --- a/frontend/src/scripts/input/mouse-listener.ts +++ b/frontend/src/scripts/input/mouse-listener.ts @@ -1,6 +1,5 @@ import { vec2 } from 'gl-matrix'; import { CommandGenerator } from '../commands/command-generator'; -import { clamp01 } from '../helper/clamp'; import { CursorMoveCommand } from './commands/cursor-move-command'; import { PrimaryActionCommand } from './commands/primary-action'; import { SecondaryActionCommand } from './commands/secondary-action'; @@ -12,7 +11,7 @@ export class MouseListener extends CommandGenerator { private isMouseDown = false; - constructor(private target: Element) { + constructor(target: Element) { super(); target.addEventListener('mousedown', (event: MouseEvent) => { @@ -59,11 +58,6 @@ export class MouseListener extends CommandGenerator { } private positionFromEvent(event: MouseEvent): vec2 { - const bb = this.target.getBoundingClientRect(); - - return vec2.fromValues( - clamp01((event.clientX - bb.x) / bb.width), - clamp01(1 - (event.clientY - bb.y) / bb.height) - ); + return vec2.fromValues(event.clientX, event.clientY); } } diff --git a/frontend/src/scripts/input/touch-listener.ts b/frontend/src/scripts/input/touch-listener.ts index 99318f9..0d75936 100644 --- a/frontend/src/scripts/input/touch-listener.ts +++ b/frontend/src/scripts/input/touch-listener.ts @@ -1,6 +1,5 @@ import { vec2 } from 'gl-matrix'; import { CommandGenerator } from '../commands/command-generator'; -import { clamp01 } from '../helper/clamp'; import { PrimaryActionCommand } from './commands/primary-action'; import { SecondaryActionCommand } from './commands/secondary-action'; import { SwipeCommand } from './commands/swipe'; @@ -39,11 +38,6 @@ export class TouchListener extends CommandGenerator { } private positionFromEvent(event: TouchEvent): vec2 { - const bb = this.target.getBoundingClientRect(); - - return vec2.fromValues( - clamp01(1 - (event.touches[0].clientX - bb.x) / bb.width), - clamp01((event.touches[0].clientY - bb.y) / bb.height) - ); + return vec2.fromValues(event.touches[0].clientX, event.touches[0].clientY); } } diff --git a/frontend/src/scripts/objects/objects.ts b/frontend/src/scripts/objects/objects.ts index 2c681e2..7d1fe21 100644 --- a/frontend/src/scripts/objects/objects.ts +++ b/frontend/src/scripts/objects/objects.ts @@ -20,6 +20,16 @@ export class Objects implements CommandReceiver { public removeObject(o: GameObject) { this.objects.delete(o.id); + + for (const command of this.objectsGroupedByAbilities.keys()) { + if (o.reactsToCommand(command)) { + const array = this.objectsGroupedByAbilities.get(command); + array.splice( + array.findIndex((i) => i.id == o.id), + 1 + ); + } + } } public sendCommandToSingleObject(id: Id, e: Command) { diff --git a/frontend/src/scripts/objects/types/character.ts b/frontend/src/scripts/objects/types/character.ts index ded58b3..6d1c3ed 100644 --- a/frontend/src/scripts/objects/types/character.ts +++ b/frontend/src/scripts/objects/types/character.ts @@ -1,37 +1,48 @@ import { vec2 } from 'gl-matrix'; import { Flashlight } from 'sdf-2d'; -import { RenderCommand } from '../../graphics/commands/render'; +import { RenderCommand } from '../../commands/render'; +import { StepCommand } from '../../commands/step'; +import { TeleportToCommand } from '../../commands/teleport-to'; import { rgb } from '../../helper/rgb'; import { IGame } from '../../i-game'; +import { CursorMoveCommand } from '../../input/commands/cursor-move-command'; import { KeyDownCommand } from '../../input/commands/key-down'; import { KeyUpCommand } from '../../input/commands/key-up'; +import { PrimaryActionCommand } from '../../input/commands/primary-action'; import { SwipeCommand } from '../../input/commands/swipe'; import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base'; import { BoundingCircle } from '../../physics/bounds/bounding-circle'; -import { StepCommand } from '../../physics/commands/step'; -import { TeleportToCommand } from '../../physics/commands/teleport-to'; +import { PhysicsCircle } from '../../physics/bounds/physics-circle'; import { DynamicPhysicalObject } from '../../physics/dynamic-physical-object'; import { Physics } from '../../physics/physics'; -import { BlobShape } from '../../shapes/types/blob-shape'; +import { settings } from '../../settings'; +import { BlobShape } from '../../shapes/blob-shape'; +import { Projectile } from './projectile'; export class Character extends DynamicPhysicalObject { - protected head: BoundingCircle; - protected leftFoot: BoundingCircle; - protected rightFoot: BoundingCircle; + protected head: PhysicsCircle; + protected leftFoot: PhysicsCircle; + protected rightFoot: PhysicsCircle; private keysDown: Set = new Set(); - private light = new Flashlight( - vec2.fromValues(0, 0), + private flashlight = new Flashlight( + vec2.create(), rgb(1, 0.6, 0.45), - 0.5, - vec2.fromValues(-1, 0) + 0.15, + vec2.fromValues(1, 0), + 50 ); + private static walkForce = 0.005; + private static jumpForce = 10; + private shape = new BlobShape(); private boundingCircle = new BoundingCircle(this, vec2.create(), 50); - private center = vec2.create(); - private static speed = 1.5; + + private readonly headOffset = vec2.fromValues(0, 40); + private readonly leftFootOffset = vec2.fromValues(-20, -10); + private readonly rightFootOffset = vec2.fromValues(20, -10); constructor(physics: Physics, private game: IGame) { super(physics, true); @@ -41,13 +52,20 @@ export class Character extends DynamicPhysicalObject { this.addCommandExecutor(KeyDownCommand, (c) => this.keysDown.add(c.key)); this.addCommandExecutor(KeyUpCommand, (c) => this.keysDown.delete(c.key)); this.addCommandExecutor(StepCommand, this.stepHandler.bind(this)); + this.addCommandExecutor(CursorMoveCommand, this.directLight.bind(this)); + this.addCommandExecutor(PrimaryActionCommand, this.spawnProjectile.bind(this)); this.addCommandExecutor(SwipeCommand, (c) => { - this.tryMoving(vec2.multiply(vec2.create(), c.delta, this.game.viewArea.size)); + //this.tryMoving(vec2.multiply(vec2.create(), c.delta, this.game.viewArea.size)); }); - this.head = new BoundingCircle(this, this.center, 50); - this.leftFoot = new BoundingCircle(this, this.center, 50); - this.rightFoot = new BoundingCircle(this, this.center, 50); + this.head = new PhysicsCircle(physics, this, vec2.clone(this.headOffset), 50); + this.leftFoot = new PhysicsCircle(physics, this, vec2.clone(this.leftFootOffset), 20); + this.rightFoot = new PhysicsCircle( + physics, + this, + vec2.clone(this.rightFootOffset), + 20 + ); this.shape.setCircles([this.head, this.leftFoot, this.rightFoot]); @@ -60,11 +78,33 @@ export class Character extends DynamicPhysicalObject { private draw(c: RenderCommand) { c.renderer.addDrawable(this.shape); - c.renderer.addDrawable(this.light); + c.renderer.addDrawable(this.flashlight); + } + + private directLight(e: CursorMoveCommand) { + const pos = this.game.displayToWorldCoordinates(e.position); + vec2.sub(pos, pos, this.head.center); + this.flashlight.direction = pos; + } + + private spawnProjectile(e: PrimaryActionCommand) { + const pos = this.game.displayToWorldCoordinates(e.position); + const direction = vec2.sub(vec2.create(), pos, this.head.center); + vec2.normalize(direction, direction); + const start = vec2.add( + vec2.create(), + this.head.center, + vec2.scale(vec2.create(), direction, this.head.radius) + ); + + vec2.scale(direction, direction, 5); + + const projectile = new Projectile(this.game, this.physics, start, direction); + this.game.addObject(projectile); } public get position(): vec2 { - return this.center; + return this.head.center; } public getBoundingCircles(): Array { @@ -75,28 +115,74 @@ export class Character extends DynamicPhysicalObject { return this.head.boundingBox; } - private tryMoving(delta: vec2) { - this.physics.tryMovingDynamicCircle(this.head, delta); - } - private setPosition(value: vec2) { - //this.head.center = value; - this.light.center = vec2.add(vec2.create(), value, vec2.fromValues(50, -40)); + this.head.center = vec2.clone(value); + this.leftFoot.center = vec2.clone(value); + this.rightFoot.center = vec2.clone(value); } public stepHandler(c: StepCommand) { const deltaTime = c.deltaTimeInMiliseconds; - const up = ~~(this.keysDown.has('w') || this.keysDown.has('arrowup')); - const down = ~~(this.keysDown.has('s') || this.keysDown.has('arrowdown')); - const left = ~~(this.keysDown.has('a') || this.keysDown.has('arrowleft')); - const right = ~~(this.keysDown.has('d') || this.keysDown.has('arrowright')); - const movementVector = vec2.fromValues(right - left, up - down); - if (vec2.length(movementVector) > 0) { - vec2.normalize(movementVector, movementVector); - vec2.scale(movementVector, movementVector, Character.speed * deltaTime); + const isAirborne = this.leftFoot.isAirborne && this.rightFoot.isAirborne; + const up = + ~~( + !isAirborne && + (this.keysDown.has('w') || this.keysDown.has('arrowup') || this.keysDown.has(' ')) + ) * Character.jumpForce; + const down = ~~( + !isAirborne && + (this.keysDown.has('s') || this.keysDown.has('arrowdown')) + ); + const left = + ~~(this.keysDown.has('a') || this.keysDown.has('arrowleft')) * + Character.walkForce * + deltaTime; + const right = + ~~(this.keysDown.has('d') || this.keysDown.has('arrowright')) * + Character.walkForce * + deltaTime; - this.tryMoving(movementVector); - } + const movementForce = vec2.fromValues(right - left, up - down); + this.head.applyForce(movementForce, deltaTime); + this.head.applyForce(settings.gravitationalForce, deltaTime); + + const bodyCenter = vec2.sub(vec2.create(), this.head.center, this.headOffset); + + const leftFootPositon = vec2.add(vec2.create(), bodyCenter, this.leftFootOffset); + const rightFootPositon = vec2.add(vec2.create(), bodyCenter, this.rightFootOffset); + + const leftFootDelta = vec2.sub(vec2.create(), this.leftFoot.center, leftFootPositon); + const rightFootDelta = vec2.sub( + vec2.create(), + this.rightFoot.center, + rightFootPositon + ); + + vec2.scale(leftFootDelta, leftFootDelta, 0.0006); + vec2.scale(rightFootDelta, rightFootDelta, 0.0006); + + this.head.applyForce(leftFootDelta, deltaTime); + this.head.applyForce(rightFootDelta, deltaTime); + + vec2.scale(leftFootDelta, leftFootDelta, -0.5); + vec2.scale(rightFootDelta, rightFootDelta, -0.5); + + this.leftFoot.applyForce(movementForce, deltaTime); + this.rightFoot.applyForce(movementForce, deltaTime); + this.leftFoot.applyForce(leftFootDelta, deltaTime); + this.rightFoot.applyForce(rightFootDelta, deltaTime); + this.leftFoot.applyForce(settings.gravitationalForce, deltaTime); + this.rightFoot.applyForce(settings.gravitationalForce, deltaTime); + + this.head.step(deltaTime); + this.leftFoot.step(deltaTime); + this.rightFoot.step(deltaTime); + + this.flashlight.center = vec2.add( + vec2.create(), + this.head.center, + vec2.fromValues(0, 0) + ); } } diff --git a/frontend/src/scripts/objects/types/projectile.ts b/frontend/src/scripts/objects/types/projectile.ts new file mode 100644 index 0000000..6fbef3e --- /dev/null +++ b/frontend/src/scripts/objects/types/projectile.ts @@ -0,0 +1,54 @@ +import { vec2 } from 'gl-matrix'; +import { Circle } from 'sdf-2d'; +import { RenderCommand } from '../../commands/render'; +import { StepCommand } from '../../commands/step'; +import { IGame } from '../../i-game'; +import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base'; +import { BoundingCircle } from '../../physics/bounds/bounding-circle'; +import { PhysicsCircle } from '../../physics/bounds/physics-circle'; +import { DynamicPhysicalObject } from '../../physics/dynamic-physical-object'; +import { Physics } from '../../physics/physics'; +import { settings } from '../../settings'; + +export class Projectile extends DynamicPhysicalObject { + private shape: Circle; + private bounding: PhysicsCircle; + + constructor(private game: IGame, physics: Physics, position: vec2, velocity: vec2) { + super(physics, true); + + this.shape = new Circle(position, 20); + this.bounding = new PhysicsCircle(physics, this, position, 20); + + this.bounding.applyForce(velocity, 100); + + this.addCommandExecutor(RenderCommand, this.draw.bind(this)); + this.addCommandExecutor(StepCommand, this.step.bind(this)); + + this.addToPhysics(); + } + + private draw(c: RenderCommand) { + c.renderer.addDrawable(this.shape); + } + + private step(c: StepCommand) { + this.bounding.applyForce(settings.gravitationalForce, c.deltaTimeInMiliseconds); + if (this.bounding.step(c.deltaTimeInMiliseconds)) { + this.game.removeObject(this); + this.physics.removeDynamicBoundingBox(this.getBoundingBox()); + } + } + + public getBoundingCircles(): BoundingCircle[] { + return [this.bounding]; + } + + public getBoundingBox(): BoundingBoxBase { + return this.bounding.boundingBox; + } + + public distance(target: vec2): number { + return this.bounding.distance(target); + } +} diff --git a/frontend/src/scripts/objects/types/tunnel.ts b/frontend/src/scripts/objects/types/tunnel.ts index 28988a9..57bfd6f 100644 --- a/frontend/src/scripts/objects/types/tunnel.ts +++ b/frontend/src/scripts/objects/types/tunnel.ts @@ -7,6 +7,7 @@ import { Physics } from '../../physics/physics'; import { StaticPhysicalObject } from '../../physics/static-physical-object'; export class Tunnel extends StaticPhysicalObject { + public readonly isInverted = true; private shape: InvertedTunnel; constructor( diff --git a/frontend/src/scripts/objects/world/create-dungeon.ts b/frontend/src/scripts/objects/world/create-dungeon.ts index a278247..254c037 100644 --- a/frontend/src/scripts/objects/world/create-dungeon.ts +++ b/frontend/src/scripts/objects/world/create-dungeon.ts @@ -27,7 +27,7 @@ export const createDungeon = (objects: Objects, physics: Physics) => { objects.addObject(tunnel); - if (++tunnelsCountSinceLastLight > 3 && Random.getRandom() > 0.6) { + if (++tunnelsCountSinceLastLight > 3 && Random.getRandom() > 0.7) { objects.addObject( new Lamp( currentEnd, diff --git a/frontend/src/scripts/physics/bounds/bounding-circle.ts b/frontend/src/scripts/physics/bounds/bounding-circle.ts index 72a3288..9047620 100644 --- a/frontend/src/scripts/physics/bounds/bounding-circle.ts +++ b/frontend/src/scripts/physics/bounds/bounding-circle.ts @@ -6,7 +6,11 @@ import { BoundingBoxBase } from './bounding-box-base'; export class BoundingCircle { private _boundingBox: BoundingBox; - constructor(public readonly owner, private _center: vec2, private _radius: number) { + constructor( + public readonly owner: PhysicalObject, + private _center: vec2, + private _radius: number + ) { this._boundingBox = new BoundingBox(owner); this.recalculateBoundingBox(); } @@ -33,6 +37,10 @@ export class BoundingCircle { return vec2.distance(target, this.center) - this.radius; } + public distanceBetween(target: BoundingCircle): number { + return vec2.distance(target.center, this.center) - this.radius - target.radius; + } + public areIntersecting(other: PhysicalObject): boolean { return other.distance(this.center) < this.radius; } diff --git a/frontend/src/scripts/physics/bounds/physics-circle.ts b/frontend/src/scripts/physics/bounds/physics-circle.ts new file mode 100644 index 0000000..eddce28 --- /dev/null +++ b/frontend/src/scripts/physics/bounds/physics-circle.ts @@ -0,0 +1,73 @@ +import { vec2 } from 'gl-matrix'; +import { clamp } from '../../helper/clamp'; +import { settings } from '../../settings'; +import { PhysicalObject } from '../physical-object'; +import { Physics } from '../physics'; +import { BoundingCircle } from './bounding-circle'; + +export class PhysicsCircle extends BoundingCircle { + private velocity = vec2.create(); + private _isAirborne = true; + + public get isAirborne(): boolean { + return this._isAirborne; + } + + constructor( + private readonly physics: Physics, + owner: PhysicalObject, + center: vec2, + radius: number + ) { + super(owner, center, radius); + } + + public resetVelocity() { + this.velocity = vec2.create(); + } + + public applyForce(force: vec2, timeInMilliseconds: number) { + vec2.add( + this.velocity, + this.velocity, + vec2.scale(vec2.create(), force, timeInMilliseconds) + ); + + vec2.set( + this.velocity, + clamp(this.velocity.x, -settings.maxVelocityX, settings.maxVelocityX), + clamp(this.velocity.y, -settings.maxVelocityY, settings.maxVelocityY) + ); + } + + public step(timeInMilliseconds: number): boolean { + vec2.scale( + this.velocity, + this.velocity, + Math.pow(settings.velocityAttenuation, timeInMilliseconds) + ); + const distance = vec2.scale(vec2.create(), this.velocity, timeInMilliseconds); + const distanceLength = vec2.length(distance); + const stepCount = Math.ceil(distanceLength / settings.physicsMaxStep); + vec2.scale(distance, distance, 1 / stepCount); + + let wasHit = false; + + for (let i = 0; i < stepCount; i++) { + const { normal, tangent, hitSurface } = this.physics.tryMovingDynamicCircle( + this, + distance + ); + if (hitSurface) { + vec2.scale(this.velocity, tangent, vec2.dot(tangent, this.velocity)); + wasHit = true; + } + + this._isAirborne = !( + hitSurface && vec2.dot(normal, settings.gravitationalForce) < 0 + ); + } + + return wasHit; + } +} diff --git a/frontend/src/scripts/physics/containers/bounding-box-list.ts b/frontend/src/scripts/physics/containers/bounding-box-list.ts index 5da7203..eda5f4f 100644 --- a/frontend/src/scripts/physics/containers/bounding-box-list.ts +++ b/frontend/src/scripts/physics/containers/bounding-box-list.ts @@ -7,6 +7,13 @@ export class BoundingBoxList { this.boundingBoxes.push(box); } + public remove(box: BoundingBoxBase) { + this.boundingBoxes.splice( + this.boundingBoxes.findIndex((i) => i === box), + 1 + ); + } + public findIntersecting(box: BoundingBoxBase): Array { return this.boundingBoxes.filter((b) => b.intersects(box)); } diff --git a/frontend/src/scripts/physics/physical-object.ts b/frontend/src/scripts/physics/physical-object.ts index 7e4cb29..5409a8a 100644 --- a/frontend/src/scripts/physics/physical-object.ts +++ b/frontend/src/scripts/physics/physical-object.ts @@ -6,6 +6,7 @@ import { Physics } from './physics'; export abstract class PhysicalObject extends GameObject { public abstract getBoundingBox(): BoundingBoxBase; public abstract distance(target: vec2): number; + public readonly isInverted: boolean = false; constructor(protected physics: Physics, public readonly canCollide: boolean) { super(); diff --git a/frontend/src/scripts/physics/physics.ts b/frontend/src/scripts/physics/physics.ts index 729d93d..345b5a8 100644 --- a/frontend/src/scripts/physics/physics.ts +++ b/frontend/src/scripts/physics/physics.ts @@ -1,4 +1,6 @@ import { vec2 } from 'gl-matrix'; +import { rotate90Deg } from '../helper/rotate-90-deg'; +import { settings } from '../settings'; import { BoundingBoxBase } from './bounds/bounding-box-base'; import { BoundingCircle } from './bounds/bounding-circle'; import { ImmutableBoundingBox } from './bounds/immutable-bounding-box'; @@ -23,11 +25,19 @@ export class Physics { this.dynamicBoundingBoxes.insert(boundingBox); } + public removeDynamicBoundingBox(boundingBox: BoundingBoxBase) { + this.dynamicBoundingBoxes.remove(boundingBox); + } + public tryMovingDynamicCircle( circle: BoundingCircle, - delta: vec2, - invocationCount = 0 - ) { + delta: vec2 + ): { + realDelta: vec2; + hitSurface: boolean; + normal?: vec2; + tangent?: vec2; + } { circle.center = vec2.add(circle.center, circle.center, delta); const intersecting = this.findIntersecting(circle.boundingBox).filter( @@ -35,48 +45,65 @@ export class Physics { b.owner !== circle.owner && circle.areIntersecting(b.owner) && b.owner.canCollide ); - const pointCount = 16; - const points = circle.getPerimeterPoints(pointCount); + if (intersecting.length === 0) { + return { + realDelta: delta, + hitSurface: false, + }; + } - const distancesOfPoints = points.map((point) => ({ - point, - distances: intersecting.map((i) => i.owner.distance(point)).sort((a, b) => a - b), - })); + const points = circle.getPerimeterPoints(settings.hitDetectionCirclePointCount); + + const distancesOfPoints = points + .map((point) => ({ + point, + closest: intersecting + .map((i) => ({ + inverted: i.isInverted, + distance: i.owner.distance(point), + })) + .sort((a, b) => a.distance - b.distance)[0], + })) + .filter((i) => i.closest); const distancesOfIntersectingPoints = distancesOfPoints.filter( - (d) => d.distances[0] > 0 + (d) => + (d.closest.distance > 0 && d.closest.inverted) || + (d.closest.distance < 0 && !d.closest.inverted) ); if (distancesOfIntersectingPoints.length === 0) { - return; + return { + realDelta: delta, + hitSurface: false, + }; } - const distanceToLeastIntersectingForEachPoint = distancesOfIntersectingPoints.map( - (pointDistances) => ({ - point: pointDistances.point, - distance: pointDistances.distances[0], - }) - ); - - const deltas = distanceToLeastIntersectingForEachPoint.map((pointDistance) => { + const deltas = distancesOfIntersectingPoints.map((pointDistance) => { vec2.subtract(pointDistance.point, circle.center, pointDistance.point); vec2.normalize(pointDistance.point, pointDistance.point); - - vec2.scale(pointDistance.point, pointDistance.point, pointDistance.distance); + vec2.scale( + pointDistance.point, + pointDistance.point, + (pointDistance.closest.inverted ? 1 : -1) * pointDistance.closest.distance + ); return pointDistance.point; }); - const sumDelta = vec2.fromValues(0, 0); + const approxNormal = deltas.reduce( + (sum, current) => vec2.add(sum, sum, current), + vec2.create() + ); + vec2.scale(approxNormal, approxNormal, 1 / deltas.length); - deltas.forEach((d) => vec2.add(sumDelta, sumDelta, d)); + circle.center = vec2.add(circle.center, circle.center, approxNormal); - vec2.scale(sumDelta, sumDelta, 1 / deltas.length); - - if (invocationCount > 10) { - return; - } - - this.tryMovingDynamicCircle(circle, sumDelta, ++invocationCount); + return { + realDelta: delta, + hitSurface: true, + normal: vec2.normalize(approxNormal, approxNormal), + tangent: rotate90Deg(approxNormal), + }; } public findIntersecting(box: BoundingBoxBase): Array { diff --git a/frontend/src/scripts/settings.ts b/frontend/src/scripts/settings.ts index f2ab3c7..686a167 100644 --- a/frontend/src/scripts/settings.ts +++ b/frontend/src/scripts/settings.ts @@ -1,5 +1,12 @@ +import { vec2 } from 'gl-matrix'; + export const settings = { lightCutoffDistance: 600, - hitDetectionCirclePointCount: 8, + hitDetectionCirclePointCount: 32, hitDetectionMaxOverlap: 0.01, + physicsMaxStep: 5, + gravitationalForce: vec2.fromValues(0, -0.015), + maxVelocityX: 1.5, + maxVelocityY: 8, + velocityAttenuation: 0.99, }; diff --git a/frontend/src/scripts/shapes/blob-shape.ts b/frontend/src/scripts/shapes/blob-shape.ts index cb06cd4..e4224e5 100644 --- a/frontend/src/scripts/shapes/blob-shape.ts +++ b/frontend/src/scripts/shapes/blob-shape.ts @@ -11,13 +11,12 @@ export class BlobShape extends Drawable { uniform vec2 rightFootCenters[BLOB_COUNT]; uniform float headRadii[BLOB_COUNT]; uniform float footRadii[BLOB_COUNT]; - //uniform float ks[BLOB_COUNT]; - float smoothMin(float a, float b) + float blobSmoothMin(float a, float b) { - const float k = 80.0; - float res = exp2( -k*a ) + exp2( -k*b ); - return -log2( res )/k; + const float k = 300.0; + float res = exp2(-k * a) + exp2(-k * b); + return -log2(res) / k; } float circleDistance(vec2 circleCenter, float radius, vec2 target) { @@ -34,8 +33,31 @@ export class BlobShape extends Drawable { float rightFootDistance = circleDistance(rightFootCenters[i], footRadii[i], target); float res = min( - smoothMin(headDistance, leftFootDistance), - smoothMin(headDistance, rightFootDistance) + blobSmoothMin(headDistance, leftFootDistance), + blobSmoothMin(headDistance, rightFootDistance) + ); + + vec2 leftEyeOffset = vec2(-headRadii[i] * 0.35, headRadii[i] * 0.2); + vec2 rightEyeOffset = vec2(headRadii[i] * 0.35, headRadii[i] * 0.2); + + res = max( + res, + -circleDistance(headCenters[i] + leftEyeOffset, headRadii[i] * 0.25, target) + ); + + res = max( + res, + -circleDistance(headCenters[i] + rightEyeOffset, headRadii[i] * 0.25, target) + ); + + res = min( + res, + circleDistance(headCenters[i] + leftEyeOffset + vec2(0, -headRadii[i] * 0.175), headRadii[i] * 0.25, target) + ); + + res = min( + res, + circleDistance(headCenters[i] + rightEyeOffset + vec2(0, -headRadii[i] * 0.175), headRadii[i] * 0.25, target) ); minDistance = min(minDistance, res); @@ -57,6 +79,7 @@ export class BlobShape extends Drawable { shaderCombinationSteps: [0, 1, 10], empty: new BlobShape(), }; + protected head: BoundingCircle; protected leftFoot: BoundingCircle; protected rightFoot: BoundingCircle; @@ -79,10 +102,12 @@ export class BlobShape extends Drawable { } public minDistance(target: vec2): number { - return Math.min( - this.head.distance(target), - this.leftFoot.distance(target), - this.rightFoot.distance(target) + return ( + Math.min( + this.head.distance(target), + this.leftFoot.distance(target), + this.rightFoot.distance(target) + ) / 2 ); }