diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index c259ab0..8275ed8 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -23,6 +23,7 @@ "unused-imports/no-unused-imports-ts": "error", "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/explicit-module-boundary-types": "off" + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-non-null-assertion": "off" } } diff --git a/frontend/src/index.html b/frontend/src/index.html index d899376..b9450ca 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -12,10 +12,10 @@ - Test + Decla.red - +
diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 2204ff0..2035f62 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -3,123 +3,9 @@ import { Game } from './scripts/game'; import { Random } from './scripts/helper/random'; import './styles/main.scss'; -glMatrix.setMatrixArrayType( - Array -); /* - } - } +glMatrix.setMatrixArrayType(Array); - zeroes.forEach((z) => { - const blueIndex = z.y * (width * 4) + z.x * 4 + 2; - imageData.data[blueIndex] = 255; - imageData.data[blueIndex - 1] = 255; - imageData.data[blueIndex - 2] = 255; - }); - - console.log(errors); - - ctx.putImageData(imageData, 0, 0); -};*/ //testSDF(); // extract as new image (data-uri) - -/* -const testSDF = () => { - Random.seed = 44; - - const objects: Array = [ - new TunnelShape(vec2.fromValues(20, 20), vec2.fromValues(40, 80), 10, 9), - new TunnelShape(vec2.fromValues(40, 80), vec2.fromValues(60, 20), 9, 15), - ]; - - const getPixelValue = (position: vec2) => { - /*const k = 0.15; - let res = Math.pow(2, -k * (vec2.distance(position, vec2.fromValues(20, 20)) - 20)); - res += Math.pow(2, -k * (vec2.distance(position, vec2.fromValues(40, 80)) - 30)); - res += Math.pow(2, -k * (vec2.distance(position, vec2.fromValues(60, 20)) - 20)); - res = -Math.log2(res) / k; - return -res; - - */ /*let min = 1000; - for (const t of objects) { - min = Math.min(min, t.distance(position)); - } - if (min < 0) { - // min = Math.min(min, vec2.distance(position, vec2.fromValues(40, 80 - 31.62)) - 31.62); - } - return -min;*/ /* -const tree = new BoundingBoxTree([ - new BoundingBox(300, 550, 150, 550, 'A'), - new BoundingBox(400, 800, 50, 200, 'B'), - new BoundingBox(450, 500, 175, 185, 'C'), - new BoundingBox(100, 200, 100, 500, 'D'), - new BoundingBox(750, 950, 450, 600, 'E'), - new BoundingBox(940, 1000, -2, 180, 'F'), - new BoundingBox(960, 1050, 50, 190, 'G'), - new BoundingBox(150, 900, 0, 575, 'H'), - new BoundingBox(-10000, 10000, -10000, 10000, 'I'), -]); - -tree.print(); -console.log(tree.findIntersecting(new BoundingBox(960, 1050, 50, 190, 'G'))); -*/ /* - return -vec2.distance(position, vec2.fromValues(40, 40)) + 30; - }; -/* - const width = 80; - const height = 100; - - const canvas = document.createElement('canvas'); - document.body.appendChild(canvas); - // set desired size of transparent image - canvas.width = width; - canvas.height = height; - - const ctx = canvas.getContext('2d'); - - const imageData = ctx.createImageData(width, height); - - const zeroes = []; - for (let x = 0; x < width; x++) { - console.log(x); - for (let y = 0; y < height; y++) { - const position = vec2.fromValues(x, y); - const redIndex = y * (width * 4) + x * 4; - const dist = getPixelValue(position); - if (Math.abs(dist) < 1.5) { - zeroes.push(position); - } - const blueIndex = y * (width * 4) + x * 4 + 2; - //imageData.data[blueIndex] = dist * 10; - - imageData.data[redIndex + 3] = 255; - } - } - - ctx.putImageData(imageData, 0, 0); - - console.log(zeroes); - const errors = []; - for (let x = 0; x < width; x++) { - console.log('verify ', x); - - for (let y = 0; y < height; y++) { - const position = vec2.fromValues(x, y); - const dist = getPixelValue(position); - - const realDistance = zeroes.map((z) => vec2.distance(z, position)).sort()[0]; - - if (Math.abs(realDistance - dist) < 3) { - const greenIndex = y * (width * 4) + x * 4 + 1; - imageData.data[greenIndex] = 255; - } else if (dist >= 0) { - const redIndex = y * (width * 4) + x * 4; - errors.push(realDistance - dist); - imageData.data[redIndex] = 255; - } - - /*if (zeroes.find((z) => vec2.distance(z, position) < dist) !== undefined) { - const blueIndex = y * (width * 4) + x * 4 + 2; - imageData.data[blueIndex] = 255; - }*/ const main = async () => { +const main = async () => { try { Random.seed = 42; await new Game().start(); diff --git a/frontend/src/scripts/game.ts b/frontend/src/scripts/game.ts index 7726d73..a3634b7 100644 --- a/frontend/src/scripts/game.ts +++ b/frontend/src/scripts/game.ts @@ -1,31 +1,30 @@ -import { vec3 } from 'gl-matrix'; -import { CircleLight, compile, Flashlight, Renderer } from 'sdf-2d'; +import { vec2 } from 'gl-matrix'; +import { CircleLight, compile, Flashlight, InvertedTunnel, Renderer } from 'sdf-2d'; import { CommandBroadcaster } from './commands/command-broadcaster'; import { RenderCommand } from './graphics/commands/render'; +import { DeltaTimeCalculator } from './helper/delta-time-calculator'; import { prettyPrint } from './helper/pretty-print'; +import { rgb } from './helper/rgb'; 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'; import { createDungeon } from './objects/world/create-dungeon'; +import { BoundingBoxBase } from './physics/bounds/bounding-box-base'; import { MoveToCommand } from './physics/commands/move-to'; import { StepCommand } from './physics/commands/step'; import { TeleportToCommand } from './physics/commands/teleport-to'; import { Physics } from './physics/physics'; -import { BoundingBoxBase } from './shapes/bounding-box-base'; -import { CircleShape } from './shapes/types/circle-shape'; -import { TunnelShape } from './shapes/types/tunnel-shape'; +import { settings } from './settings'; +import { BlobShape } from './shapes/types/blob-shape'; export class Game implements IGame { public readonly objects = new Objects(); public readonly physics = new Physics(); public readonly camera = new Camera(); - private previousTime?: DOMHighResTimeStamp = null; - private previousFpsValues: Array = []; private character: Character; private renderer: Renderer; private rendererPromise: Promise; @@ -34,8 +33,6 @@ export class Game implements IGame { constructor() { const canvas: HTMLCanvasElement = document.querySelector('canvas#main'); - document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); - new CommandBroadcaster( [ new KeyboardListener(document.body), @@ -48,16 +45,16 @@ export class Game implements IGame { this.rendererPromise = compile( canvas, [ - CircleShape.descriptor, - TunnelShape.descriptor, + InvertedTunnel.descriptor, Flashlight.descriptor, + BlobShape.descriptor, CircleLight.descriptor, ], - [ - vec3.fromValues(0, 0, 0), - vec3.fromValues(224 / 255, 96 / 255, 126 / 255), - vec3.fromValues(119 / 255, 173 / 255, 120 / 255), - ] + { + shadowTraceCount: 12, + paletteSize: 10, + enableStopwatch: true, + } ); this.initializeScene(); this.physics.start(); @@ -67,16 +64,22 @@ export class Game implements IGame { this.renderer = await this.rendererPromise; this.renderer.setRuntimeSettings({ isWorldInverted: true, - ambientLight: vec3.fromValues(0.35, 0.1, 0.45), - shadowLength: 300, + ambientLight: rgb(0.35, 0.1, 0.45), + colorPalette: [ + rgb(0.4, 1, 0.6), + rgb(1, 1, 0), + rgb(0.3, 1, 1), + rgb(0.3, 1, 1), + rgb(0.3, 1, 1), + rgb(0.3, 1, 1), + rgb(0.3, 1, 1), + ], + enableHighDpiRendering: false, + lightCutoffDistance: settings.lightCutoffDistance, }); requestAnimationFrame(this.gameLoop.bind(this)); } - public addObject(o: GameObject) { - this.objects.addObject(o); - } - public get viewArea(): BoundingBoxBase { return this.camera.viewArea; } @@ -86,51 +89,38 @@ export class Game implements IGame { } private initializeScene() { - const start = createDungeon(this.objects, this.physics); createDungeon(this.objects, this.physics); + //createDungeon(this.objects, this.physics); - this.character = new Character(this); - // this.physics.addDynamicBoundingBox(this.character.boundingBox); - this.addObject(this.character); - this.addObject(this.camera); - let pos: any = localStorage.getItem('character-position'); - pos = pos ? JSON.parse(pos) : start.from; + 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 handleVisibilityChange() { - if (!document.hidden) { - this.previousTime = null; - } - } + private deltaTimeCalculator = new DeltaTimeCalculator(); private gameLoop(time: DOMHighResTimeStamp) { - if (this.previousTime === null) { - this.previousTime = time; - } - - const deltaTime = time - this.previousTime; - this.previousTime = time; + const deltaTime = this.deltaTimeCalculator.getNextDeltaTime(time); this.objects.sendCommand(new StepCommand(deltaTime)); this.camera.sendCommand(new MoveToCommand(this.character.position)); - this.camera.sendCommand(new RenderCommand(this.renderer)); const shouldBeDrawn = this.physics .findIntersecting(this.camera.viewArea) - .map((b) => b.shape?.gameObject); + .map((b) => b.owner); for (const object of shouldBeDrawn) { object?.sendCommand(new RenderCommand(this.renderer)); } - this.character.sendCommand(new RenderCommand(this.renderer)); this.renderer.renderDrawables(); this.overlay.innerText = prettyPrint(this.renderer.insights); - - localStorage.setItem('character-position', JSON.stringify(this.character.position)); + localStorage.setItem('characterPosition', JSON.stringify(this.character.position)); requestAnimationFrame(this.gameLoop.bind(this)); } } diff --git a/frontend/src/scripts/helper/exponential-decay.ts b/frontend/src/scripts/helper/exponential-decay.ts deleted file mode 100644 index 7f321f5..0000000 --- a/frontend/src/scripts/helper/exponential-decay.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const exponentialDecay = ( - accumulator: number, - nextValue: number, - biasOfNextValue: number -) => accumulator * (1 - biasOfNextValue) + nextValue * biasOfNextValue; diff --git a/frontend/src/scripts/helper/get-combinations.ts b/frontend/src/scripts/helper/get-combinations.ts deleted file mode 100644 index 42c36d6..0000000 --- a/frontend/src/scripts/helper/get-combinations.ts +++ /dev/null @@ -1,29 +0,0 @@ -export const getCombinations = (values: Array>): Array> => { - if (!values.every((a) => a.length > 0)) { - return []; - } - - const result: Array> = []; - const counters = values.map((_) => 0); - - const increaseCounter = (i: number) => { - if (i >= counters.length) { - return false; - } - - counters[i]++; - - if (counters[i] >= values[i].length) { - counters[i] = 0; - return increaseCounter(i + 1); - } - - return true; - }; - - do { - result.push(values.map((v, i) => v[counters[i]])); - } while (increaseCounter(0)); - - return result; -}; diff --git a/frontend/src/scripts/helper/rgb.ts b/frontend/src/scripts/helper/rgb.ts new file mode 100644 index 0000000..a6ef20a --- /dev/null +++ b/frontend/src/scripts/helper/rgb.ts @@ -0,0 +1,3 @@ +import { vec3 } from 'gl-matrix'; + +export const rgb = (r: number, g: number, b: number): vec3 => vec3.fromValues(r, g, b); diff --git a/frontend/src/scripts/helper/rgb255.ts b/frontend/src/scripts/helper/rgb255.ts new file mode 100644 index 0000000..56c47ff --- /dev/null +++ b/frontend/src/scripts/helper/rgb255.ts @@ -0,0 +1,4 @@ +import { vec3 } from 'gl-matrix'; + +export const rgb255 = (r: number, g: number, b: number): vec3 => + vec3.fromValues(r / 255, g / 255, b / 255); diff --git a/frontend/src/scripts/helper/wait-while-false.ts b/frontend/src/scripts/helper/wait-while-false.ts deleted file mode 100644 index 1e0f708..0000000 --- a/frontend/src/scripts/helper/wait-while-false.ts +++ /dev/null @@ -1,29 +0,0 @@ -export const waitWhileFalse = (body: () => boolean): Promise => { - let resolveOnDone: () => void; - let rejectOnDone: (e: Error) => void; - - const onDone = new Promise((resolve, reject) => { - resolveOnDone = resolve; - rejectOnDone = reject; - }); - - const waiter = () => { - let success: boolean; - - try { - success = body(); - } catch (e) { - rejectOnDone(e); - return; - } - - if (success) { - resolveOnDone(); - } else { - requestAnimationFrame(waiter); - } - }; - - waiter(); - return onDone; -}; diff --git a/frontend/src/scripts/i-game.ts b/frontend/src/scripts/i-game.ts index 7b1a0da..d7e1c40 100644 --- a/frontend/src/scripts/i-game.ts +++ b/frontend/src/scripts/i-game.ts @@ -1,8 +1,6 @@ -import { GameObject } from './objects/game-object'; -import { BoundingBoxBase } from './shapes/bounding-box-base'; +import { BoundingBoxBase } from './physics/bounds/bounding-box-base'; export interface IGame { - addObject(o: GameObject): void; readonly viewArea: BoundingBoxBase; findIntersecting(box: BoundingBoxBase): Array; } diff --git a/frontend/src/scripts/objects/types/camera.ts b/frontend/src/scripts/objects/types/camera.ts index d46d196..5bd1c32 100644 --- a/frontend/src/scripts/objects/types/camera.ts +++ b/frontend/src/scripts/objects/types/camera.ts @@ -2,8 +2,8 @@ import { vec2 } from 'gl-matrix'; import { RenderCommand } from '../../graphics/commands/render'; import { CursorMoveCommand } from '../../input/commands/cursor-move-command'; import { ZoomCommand } from '../../input/commands/zoom'; +import { BoundingBox } from '../../physics/bounds/bounding-box'; import { MoveToCommand } from '../../physics/commands/move-to'; -import { BoundingBox } from '../../shapes/bounding-box'; import { GameObject } from '../game-object'; export class Camera extends GameObject { diff --git a/frontend/src/scripts/objects/types/character.ts b/frontend/src/scripts/objects/types/character.ts index 065dc1c..ded58b3 100644 --- a/frontend/src/scripts/objects/types/character.ts +++ b/frontend/src/scripts/objects/types/character.ts @@ -1,41 +1,61 @@ -import { vec2, vec3 } from 'gl-matrix'; +import { vec2 } from 'gl-matrix'; import { Flashlight } from 'sdf-2d'; import { RenderCommand } from '../../graphics/commands/render'; +import { rgb } from '../../helper/rgb'; import { IGame } from '../../i-game'; import { KeyDownCommand } from '../../input/commands/key-down'; import { KeyUpCommand } from '../../input/commands/key-up'; 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 { IShape } from '../../shapes/i-shape'; +import { DynamicPhysicalObject } from '../../physics/dynamic-physical-object'; +import { Physics } from '../../physics/physics'; import { BlobShape } from '../../shapes/types/blob-shape'; -import { GameObject } from '../game-object'; -export class Character extends GameObject { +export class Character extends DynamicPhysicalObject { + protected head: BoundingCircle; + protected leftFoot: BoundingCircle; + protected rightFoot: BoundingCircle; + private keysDown: Set = new Set(); + private light = new Flashlight( - vec2.create(), - vec3.fromValues(1, 0.6, 0.45), - 1.5, + vec2.fromValues(0, 0), + rgb(1, 0.6, 0.45), + 0.5, vec2.fromValues(-1, 0) ); - private shape = new BlobShape(vec2.create()); + + private shape = new BlobShape(); + private boundingCircle = new BoundingCircle(this, vec2.create(), 50); + private center = vec2.create(); private static speed = 1.5; - constructor(private game: IGame) { - super(); + constructor(physics: Physics, private game: IGame) { + super(physics, true); this.addCommandExecutor(RenderCommand, this.draw.bind(this)); this.addCommandExecutor(TeleportToCommand, (c) => this.setPosition(c.position)); 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(SwipeCommand, (c) => { 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.shape.setCircles([this.head, this.leftFoot, this.rightFoot]); + + this.addToPhysics(); } - public get position(): vec2 { - return this.shape.center; + public distance(target: vec2): number { + return this.shape.minDistance(target); } private draw(c: RenderCommand) { @@ -43,73 +63,24 @@ export class Character extends GameObject { c.renderer.addDrawable(this.light); } - private tryMoving(delta: vec2, isFirstIteration = true) { - const maxStep = 2; - if (vec2.length(delta) > maxStep) { - let steppedDelta = vec2.normalize(vec2.create(), delta); - vec2.scale(steppedDelta, steppedDelta, maxStep - 0.001); - - for (let i = 0; i <= vec2.length(delta) / maxStep - 1; i++) { - this.tryMoving(vec2.clone(steppedDelta), isFirstIteration); - } - - steppedDelta = vec2.normalize(vec2.create(), delta); - - vec2.scale(steppedDelta, steppedDelta, vec2.length(delta) % maxStep); - this.tryMoving(vec2.clone(steppedDelta), isFirstIteration); - - return; - } - - const nextShape = this.shape.clone(); - vec2.add(nextShape.center, nextShape.center, delta); - - const nextNearShapes = this.getNearShapesTo(nextShape); - - if (nextNearShapes.length && nextNearShapes[0].distance < 0) { - this.setPosition(nextShape.center); - } else { - if (!isFirstIteration) { - return; - } - - const currentNearShapes = this.getNearShapesTo(this.shape); - const intersecting = nextNearShapes - .filter( - (n) => - currentNearShapes.find((c) => c.shape === n.shape && c.distance <= 0) !== - undefined - ) - .sort((e) => Math.abs(e.distance)); - - if (intersecting.length < 1) { - return; - } - const normal = intersecting[0].shape.normal(this.shape.center); - - const maxDistance = intersecting.reduce((p, c) => (p.distance > c.distance ? p : c)) - .distance; - - vec2.add(delta, delta, vec2.scale(vec2.create(), normal, -maxDistance - 2)); - - this.tryMoving(delta, false); - } + public get position(): vec2 { + return this.center; } - private getNearShapesTo(shape: BlobShape): Array<{ shape: IShape; distance: number }> { - return this.game - .findIntersecting(shape.boundingBox) - .filter((b) => b.shape) - .map((b) => ({ - shape: b.shape, - // TODO: fix this - distance: b.shape.minDistance(shape.center) + shape.radius - 20, - })) - .sort((e) => e.distance); + public getBoundingCircles(): Array { + return [this.boundingCircle]; + } + + public getBoundingBox(): BoundingBoxBase { + return this.head.boundingBox; + } + + private tryMoving(delta: vec2) { + this.physics.tryMovingDynamicCircle(this.head, delta); } private setPosition(value: vec2) { - this.shape.position = value; + //this.head.center = value; this.light.center = vec2.add(vec2.create(), value, vec2.fromValues(50, -40)); } diff --git a/frontend/src/scripts/objects/types/lamp.ts b/frontend/src/scripts/objects/types/lamp.ts index 6be9793..1817f80 100644 --- a/frontend/src/scripts/objects/types/lamp.ts +++ b/frontend/src/scripts/objects/types/lamp.ts @@ -1,21 +1,40 @@ import { vec2, vec3 } from 'gl-matrix'; import { CircleLight } from 'sdf-2d'; import { RenderCommand } from '../../graphics/commands/render'; +import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base'; +import { ImmutableBoundingBox } from '../../physics/bounds/immutable-bounding-box'; import { MoveToCommand } from '../../physics/commands/move-to'; -import { GameObject } from '../game-object'; +import { PhysicalObject } from '../../physics/physical-object'; +import { Physics } from '../../physics/physics'; +import { settings } from '../../settings'; -export class Lamp extends GameObject { +export class Lamp extends PhysicalObject { private light: CircleLight; - constructor(center: vec2, color: vec3, lightness: number) { - super(); + constructor(center: vec2, color: vec3, lightness: number, physics: Physics) { + super(physics, false); this.light = new CircleLight(center, color, lightness); + this.addToPhysics(); this.addCommandExecutor(RenderCommand, this.draw.bind(this)); this.addCommandExecutor(MoveToCommand, this.moveTo.bind(this)); } + public distance(target: vec2): number { + return this.light.minDistance(target); + } + + public getBoundingBox(): BoundingBoxBase { + return new ImmutableBoundingBox( + this, + this.light.center.x - settings.lightCutoffDistance, + this.light.center.x + settings.lightCutoffDistance, + this.light.center.y - settings.lightCutoffDistance, + this.light.center.y + settings.lightCutoffDistance + ); + } + private draw(c: RenderCommand) { c.renderer.addDrawable(this.light); } diff --git a/frontend/src/scripts/objects/types/tunnel.ts b/frontend/src/scripts/objects/types/tunnel.ts index b5c417a..60f1758 100644 --- a/frontend/src/scripts/objects/types/tunnel.ts +++ b/frontend/src/scripts/objects/types/tunnel.ts @@ -1,26 +1,52 @@ import { vec2 } from 'gl-matrix'; import { RenderCommand } from '../../graphics/commands/render'; +import { BoundingBoxBase } from '../../physics/bounds/bounding-box-base'; +import { ImmutableBoundingBox } from '../../physics/bounds/immutable-bounding-box'; import { Physics } from '../../physics/physics'; +import { StaticPhysicalObject } from '../../physics/static-physical-object'; import { TunnelShape } from '../../shapes/types/tunnel-shape'; -import { GameObject } from '../game-object'; -export class Tunnel extends GameObject { +export class Tunnel extends StaticPhysicalObject { private shape: TunnelShape; constructor( physics: Physics, - public readonly from: vec2, + from: vec2, to: vec2, fromRadius: number, toRadius: number ) { - super(); + super(physics, true); + this.shape = new TunnelShape(from, to, fromRadius, toRadius); + this.addToPhysics(); - this.shape = new TunnelShape(from, to, fromRadius, toRadius, this); - physics.addStaticBoundingBox(this.shape.boundingBox); this.addCommandExecutor(RenderCommand, this.draw.bind(this)); } + public distance(target: vec2): number { + return this.shape.minDistance(target); + } + + public getBoundingBox(): BoundingBoxBase { + const xMin = Math.min( + this.shape.from.x - this.shape.fromRadius, + this.shape.to.x - this.shape.toRadius + ); + const yMin = Math.min( + this.shape.from.y - this.shape.fromRadius, + this.shape.to.y - this.shape.toRadius + ); + const xMax = Math.max( + this.shape.from.x + this.shape.fromRadius, + this.shape.to.x + this.shape.toRadius + ); + const yMax = Math.max( + this.shape.from.y + this.shape.fromRadius, + this.shape.to.y + this.shape.toRadius + ); + return new ImmutableBoundingBox(this, xMin, xMax, yMin, yMax, true); + } + private draw(c: RenderCommand) { c.renderer.addDrawable(this.shape); } diff --git a/frontend/src/scripts/objects/world/create-dungeon.ts b/frontend/src/scripts/objects/world/create-dungeon.ts index b1e36fc..a278247 100644 --- a/frontend/src/scripts/objects/world/create-dungeon.ts +++ b/frontend/src/scripts/objects/world/create-dungeon.ts @@ -1,14 +1,15 @@ -import { vec2 } from 'gl-matrix'; +import { vec2, vec3 } from 'gl-matrix'; import { Random } from '../../helper/random'; import { Physics } from '../../physics/physics'; import { Objects } from '../objects'; +import { Lamp } from '../types/lamp'; import { Tunnel } from '../types/tunnel'; -export const createDungeon = (objects: Objects, physics: Physics): Tunnel => { +export const createDungeon = (objects: Objects, physics: Physics) => { let previousRadius = 350; let previousEnd = vec2.create(); - let first: Tunnel; + let tunnelsCountSinceLastLight = 0; for (let i = 0; i < 500000; i += 500) { const deltaHeight = (Random.getRandom() - 0.5) * 2000; @@ -24,30 +25,24 @@ export const createDungeon = (objects: Objects, physics: Physics): Tunnel => { currentToRadius ); - if (!first) { - first = tunnel; - } - objects.addObject(tunnel); - /* if (deltaHeight > 0 && Random.getRandom() > 0.8) { + if (++tunnelsCountSinceLastLight > 3 && Random.getRandom() > 0.6) { objects.addObject( new Lamp( currentEnd, - Random.getRandom() * 20 + 30, - vec3.scale( + vec3.normalize( vec3.create(), - vec3.normalize(vec3.create(), vec3.fromValues(0.5, 0.1, 0.8)), - Random.getRandom() * 0.5 + 0.5 + vec3.fromValues(Random.getRandom(), 0, Random.getRandom()) ), - 1 + 0.5, + physics ) ); - } */ + tunnelsCountSinceLastLight = 0; + } previousEnd = currentEnd; previousRadius = currentToRadius; } - - return first; }; diff --git a/frontend/src/scripts/shapes/bounding-box-base.ts b/frontend/src/scripts/physics/bounds/bounding-box-base.ts similarity index 86% rename from frontend/src/scripts/shapes/bounding-box-base.ts rename to frontend/src/scripts/physics/bounds/bounding-box-base.ts index 8657388..d2c7bd7 100644 --- a/frontend/src/scripts/shapes/bounding-box-base.ts +++ b/frontend/src/scripts/physics/bounds/bounding-box-base.ts @@ -1,13 +1,14 @@ import { vec2 } from 'gl-matrix'; -import { IShape } from './i-shape'; +import { PhysicalObject } from '../physical-object'; export abstract class BoundingBoxBase { constructor( - public readonly shape: IShape, + public readonly owner: PhysicalObject, protected _xMin: number = 0, protected _xMax: number = 0, protected _yMin: number = 0, - protected _yMax: number = 0 + protected _yMax: number = 0, + public readonly isInverted = false ) {} public get 0(): number { diff --git a/frontend/src/scripts/shapes/bounding-box.ts b/frontend/src/scripts/physics/bounds/bounding-box.ts similarity index 60% rename from frontend/src/scripts/shapes/bounding-box.ts rename to frontend/src/scripts/physics/bounds/bounding-box.ts index 94d20c0..23bf4f7 100644 --- a/frontend/src/scripts/shapes/bounding-box.ts +++ b/frontend/src/scripts/physics/bounds/bounding-box.ts @@ -3,6 +3,38 @@ import { BoundingBoxBase } from './bounding-box-base'; import { ImmutableBoundingBox } from './immutable-bounding-box'; export class BoundingBox extends BoundingBoxBase { + public get xMin(): number { + return this._xMin; + } + + public set xMin(value: number) { + this._xMin = value; + } + + public set xMax(value: number) { + this._xMax = value; + } + + public get xMax(): number { + return this._xMax; + } + + public set yMin(value: number) { + this._yMin = value; + } + + public get yMin(): number { + return this._yMin; + } + + public set yMax(value: number) { + this._yMax = value; + } + + public get yMax(): number { + return this._yMax; + } + public get topLeft(): vec2 { return vec2.fromValues(this._xMin, this._yMax); } @@ -12,18 +44,18 @@ export class BoundingBox extends BoundingBoxBase { this._yMax = value.y; } - public get size(): vec2 { - return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin); - } - public set size(value: vec2) { this._xMax = this.xMin + value.x; this._yMin = this.yMax - value.y; } + public get size(): vec2 { + return vec2.fromValues(this._xMax - this._xMin, this._yMax - this._yMin); + } + public cloneAsImmutable(): ImmutableBoundingBox { return new ImmutableBoundingBox( - this.shape, + this.owner, this.xMin, this.xMax, this.yMin, diff --git a/frontend/src/scripts/physics/bounds/bounding-circle.ts b/frontend/src/scripts/physics/bounds/bounding-circle.ts new file mode 100644 index 0000000..72a3288 --- /dev/null +++ b/frontend/src/scripts/physics/bounds/bounding-circle.ts @@ -0,0 +1,67 @@ +import { vec2 } from 'gl-matrix'; +import { PhysicalObject } from '../physical-object'; +import { BoundingBox } from './bounding-box'; +import { BoundingBoxBase } from './bounding-box-base'; + +export class BoundingCircle { + private _boundingBox: BoundingBox; + + constructor(public readonly owner, private _center: vec2, private _radius: number) { + this._boundingBox = new BoundingBox(owner); + this.recalculateBoundingBox(); + } + + public get center(): vec2 { + return this._center; + } + + public set center(value: vec2) { + this._center = value; + this.recalculateBoundingBox(); + } + + public get radius(): number { + return this._radius; + } + + public set radius(value: number) { + this._radius = value; + this.recalculateBoundingBox(); + } + + public distance(target: vec2): number { + return vec2.distance(target, this.center) - this.radius; + } + + public areIntersecting(other: PhysicalObject): boolean { + return other.distance(this.center) < this.radius; + } + + public isInside(other: PhysicalObject): boolean { + return other.distance(this.center) < -this.radius; + } + + public getPerimeterPoints(count: number): Array { + const result: Array = []; + for (let i = 0; i < count; i++) { + result.push( + vec2.fromValues( + Math.cos((2 * Math.PI * i) / count) * this.radius + this.center.x, + Math.sin((2 * Math.PI * i) / count) * this.radius + this.center.y + ) + ); + } + return result; + } + + public get boundingBox(): BoundingBoxBase { + return this._boundingBox; + } + + private recalculateBoundingBox() { + this._boundingBox.xMin = this.center.x - this._radius; + this._boundingBox.xMax = this.center.x + this._radius; + this._boundingBox.yMin = this.center.y - this._radius; + this._boundingBox.yMax = this.center.y + this._radius; + } +} diff --git a/frontend/src/scripts/shapes/immutable-bounding-box.ts b/frontend/src/scripts/physics/bounds/immutable-bounding-box.ts similarity index 100% rename from frontend/src/scripts/shapes/immutable-bounding-box.ts rename to frontend/src/scripts/physics/bounds/immutable-bounding-box.ts diff --git a/frontend/src/scripts/physics/containers/bounding-box-list.ts b/frontend/src/scripts/physics/containers/bounding-box-list.ts index 7a2bc6e..5da7203 100644 --- a/frontend/src/scripts/physics/containers/bounding-box-list.ts +++ b/frontend/src/scripts/physics/containers/bounding-box-list.ts @@ -1,4 +1,4 @@ -import { BoundingBoxBase } from '../../shapes/bounding-box-base'; +import { BoundingBoxBase } from '../bounds/bounding-box-base'; export class BoundingBoxList { constructor(private boundingBoxes: Array = []) {} diff --git a/frontend/src/scripts/physics/containers/bounding-box-tree.ts b/frontend/src/scripts/physics/containers/bounding-box-tree.ts index 3cbebcb..d469be2 100644 --- a/frontend/src/scripts/physics/containers/bounding-box-tree.ts +++ b/frontend/src/scripts/physics/containers/bounding-box-tree.ts @@ -1,12 +1,10 @@ // source: https://github.com/ubilabs/kd-tree-javascript/blob/master/kdTree.js -import { ImmutableBoundingBox } from '../../shapes/immutable-bounding-box'; +import { ImmutableBoundingBox } from '../bounds/immutable-bounding-box'; class Node { public left?: Node = null; - public right?: Node = null; - constructor(public rectangle: ImmutableBoundingBox, public parent: Node) {} } diff --git a/frontend/src/scripts/physics/dynamic-physical-object.ts b/frontend/src/scripts/physics/dynamic-physical-object.ts new file mode 100644 index 0000000..b697b47 --- /dev/null +++ b/frontend/src/scripts/physics/dynamic-physical-object.ts @@ -0,0 +1,10 @@ +import { BoundingCircle } from './bounds/bounding-circle'; +import { PhysicalObject } from './physical-object'; + +export abstract class DynamicPhysicalObject extends PhysicalObject { + public abstract getBoundingCircles(): Array; + + protected addToPhysics() { + super.addToPhysics(true); + } +} diff --git a/frontend/src/scripts/physics/physical-object.ts b/frontend/src/scripts/physics/physical-object.ts new file mode 100644 index 0000000..7e4cb29 --- /dev/null +++ b/frontend/src/scripts/physics/physical-object.ts @@ -0,0 +1,21 @@ +import { vec2 } from 'gl-matrix'; +import { GameObject } from '../objects/game-object'; +import { BoundingBoxBase } from './bounds/bounding-box-base'; +import { Physics } from './physics'; + +export abstract class PhysicalObject extends GameObject { + public abstract getBoundingBox(): BoundingBoxBase; + public abstract distance(target: vec2): number; + + constructor(protected physics: Physics, public readonly canCollide: boolean) { + super(); + } + + protected addToPhysics(isDynamic = false) { + if (isDynamic) { + this.physics.addDynamicBoundingBox(this.getBoundingBox()); + } else { + this.physics.addStaticBoundingBox(this.getBoundingBox()); + } + } +} diff --git a/frontend/src/scripts/physics/physics.ts b/frontend/src/scripts/physics/physics.ts index 07d1797..729d93d 100644 --- a/frontend/src/scripts/physics/physics.ts +++ b/frontend/src/scripts/physics/physics.ts @@ -1,15 +1,14 @@ -import { BoundingBoxTree } from './containers/bounding-box-tree'; +import { vec2 } from 'gl-matrix'; +import { BoundingBoxBase } from './bounds/bounding-box-base'; +import { BoundingCircle } from './bounds/bounding-circle'; +import { ImmutableBoundingBox } from './bounds/immutable-bounding-box'; import { BoundingBoxList } from './containers/bounding-box-list'; -import { ImmutableBoundingBox } from '../shapes/immutable-bounding-box'; -import { BoundingBoxBase } from '../shapes/bounding-box-base'; +import { BoundingBoxTree } from './containers/bounding-box-tree'; export class Physics { private isTreeInitialized = false; - private staticBoundingBoxesWaitList = []; - private staticBoundingBoxes = new BoundingBoxTree(); - private dynamicBoundingBoxes = new BoundingBoxList(); public addStaticBoundingBox(boundingBox: ImmutableBoundingBox) { @@ -24,6 +23,62 @@ export class Physics { this.dynamicBoundingBoxes.insert(boundingBox); } + public tryMovingDynamicCircle( + circle: BoundingCircle, + delta: vec2, + invocationCount = 0 + ) { + circle.center = vec2.add(circle.center, circle.center, delta); + + const intersecting = this.findIntersecting(circle.boundingBox).filter( + (b) => + b.owner !== circle.owner && circle.areIntersecting(b.owner) && b.owner.canCollide + ); + + const pointCount = 16; + const points = circle.getPerimeterPoints(pointCount); + + const distancesOfPoints = points.map((point) => ({ + point, + distances: intersecting.map((i) => i.owner.distance(point)).sort((a, b) => a - b), + })); + + const distancesOfIntersectingPoints = distancesOfPoints.filter( + (d) => d.distances[0] > 0 + ); + + if (distancesOfIntersectingPoints.length === 0) { + return; + } + + const distanceToLeastIntersectingForEachPoint = distancesOfIntersectingPoints.map( + (pointDistances) => ({ + point: pointDistances.point, + distance: pointDistances.distances[0], + }) + ); + + const deltas = distanceToLeastIntersectingForEachPoint.map((pointDistance) => { + vec2.subtract(pointDistance.point, circle.center, pointDistance.point); + vec2.normalize(pointDistance.point, pointDistance.point); + + vec2.scale(pointDistance.point, pointDistance.point, pointDistance.distance); + return pointDistance.point; + }); + + const sumDelta = vec2.fromValues(0, 0); + + deltas.forEach((d) => vec2.add(sumDelta, sumDelta, d)); + + vec2.scale(sumDelta, sumDelta, 1 / deltas.length); + + if (invocationCount > 10) { + return; + } + + this.tryMovingDynamicCircle(circle, sumDelta, ++invocationCount); + } + public findIntersecting(box: BoundingBoxBase): Array { return [ ...this.staticBoundingBoxes.findIntersecting(box), diff --git a/frontend/src/scripts/physics/static-physical-object.ts b/frontend/src/scripts/physics/static-physical-object.ts new file mode 100644 index 0000000..cf27574 --- /dev/null +++ b/frontend/src/scripts/physics/static-physical-object.ts @@ -0,0 +1,7 @@ +import { PhysicalObject } from './physical-object'; + +export abstract class StaticPhysicalObject extends PhysicalObject { + protected addToPhysics() { + super.addToPhysics(false); + } +} diff --git a/frontend/src/scripts/settings.ts b/frontend/src/scripts/settings.ts new file mode 100644 index 0000000..15c28bb --- /dev/null +++ b/frontend/src/scripts/settings.ts @@ -0,0 +1,3 @@ +export const settings = { + lightCutoffDistance: 600, +}; diff --git a/frontend/src/scripts/shapes/i-shape.ts b/frontend/src/scripts/shapes/i-shape.ts deleted file mode 100644 index a2b48d1..0000000 --- a/frontend/src/scripts/shapes/i-shape.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { vec2 } from 'gl-matrix'; -import { GameObject } from '../objects/game-object'; -import { BoundingBox } from './bounding-box'; - -export interface IShape { - readonly boundingBox: BoundingBox; - readonly gameObject?: GameObject; - - minDistance(target: vec2): number; - normal(from: vec2): vec2; - clone(): IShape; -} diff --git a/frontend/src/scripts/shapes/types/blob-shape.ts b/frontend/src/scripts/shapes/types/blob-shape.ts index d88a8df..365e74e 100644 --- a/frontend/src/scripts/shapes/types/blob-shape.ts +++ b/frontend/src/scripts/shapes/types/blob-shape.ts @@ -1,110 +1,89 @@ import { mat2d, vec2 } from 'gl-matrix'; -import { Circle, Drawable, DrawableDescriptor } from 'sdf-2d'; -import { GameObject } from '../../objects/game-object'; -import { BoundingBox } from '../bounding-box'; -import { IShape } from '../i-shape'; -import { CircleShape } from './circle-shape'; +import { Drawable, DrawableDescriptor } from 'sdf-2d'; +import { BoundingCircle } from '../../physics/bounds/bounding-circle'; -export class BlobShape extends Drawable implements IShape { +export class BlobShape extends Drawable { public static descriptor: DrawableDescriptor = { sdf: { shader: ` - uniform struct { - vec2 headCenter; - vec2 leftFootCenter; - vec2 rightFootCenter; - float headRadius; - float footRadius; - float k; - }[BLOB_COUNT] blobs; + uniform vec2 headCenters[BLOB_COUNT]; + uniform vec2 leftFootCenters[BLOB_COUNT]; + 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) - { - const float k = 80.0; - float res = exp2( -k*a ) + exp2( -k*b ); - return -log2( res )/k; + float smoothMin(float a, float b) + { + const float k = 80.0; + float res = exp2( -k*a ) + exp2( -k*b ); + return -log2( res )/k; + } + + float circleDistance(vec2 circleCenter, float radius, vec2 target) { + return distance(target, circleCenter) - radius; + } + + float blobMinDistance(vec2 target, out float colorIndex) { + float minDistance = 1000.0; + colorIndex = 3.0; + + for (int i = 0; i < BLOB_COUNT; i++) { + float headDistance = circleDistance(headCenters[i], headRadii[i], target); + float leftFootDistance = circleDistance(leftFootCenters[i], footRadii[i], target); + float rightFootDistance = circleDistance(rightFootCenters[i], footRadii[i], target); + + float res = min( + smoothMin(headDistance, leftFootDistance), + smoothMin(headDistance, rightFootDistance) + ); + + minDistance = min(minDistance, res); } - float circleDistance(vec2 circleCenter, float radius) { - return distance(position, circleCenter) - radius; - } - - void blobMinDistance(inout float minDistance, inout float color) { - for (int i = 0; i < BLOB_COUNT; i++) { - float headDistance = circleDistance(blobs[i].headCenter, blobs[i].headRadius); - float leftFootDistance = circleDistance(blobs[i].leftFootCenter, blobs[i].footRadius); - float rightFootDistance = circleDistance(blobs[i].rightFootCenter, blobs[i].footRadius); - - float res = min( - smoothMin(headDistance, leftFootDistance), - smoothMin(headDistance, rightFootDistance) - ); - - minDistance = min(minDistance, res); - color = mix(1.0, color, step(distanceNdcPixelSize + SURFACE_OFFSET, res)); - } - } - `, + return minDistance; + } + `, distanceFunctionName: 'blobMinDistance', }, - uniformName: 'blobs', + propertyUniformMapping: { + footRadius: 'footRadii', + headRadius: 'headRadii', + rightFootCenter: 'rightFootCenters', + leftFootCenter: 'leftFootCenters', + headCenter: 'headCenters', + }, uniformCountMacroName: 'BLOB_COUNT', - shaderCombinationSteps: [1], - empty: new BlobShape(vec2.fromValues(0, 0)), + shaderCombinationSteps: [0, 1, 10], + empty: new BlobShape(), }; + protected head: BoundingCircle; + protected leftFoot: BoundingCircle; + protected rightFoot: BoundingCircle; - public readonly boundingCircleRadius = 100; - - protected readonly headRadius = 40; - protected readonly footRadius = 15; - - private readonly headOffset = vec2.fromValues(0, -15); - private readonly leftFootOffset = vec2.fromValues(-12, -60); - private readonly rightFootOffset = vec2.fromValues(12, -60); - - public readonly isInverted = false; - protected boundingCircle = new CircleShape(vec2.create(), this.boundingCircleRadius); - protected head = new Circle(vec2.create(), this.headRadius); - protected leftFoot = new Circle(vec2.create(), this.footRadius); - protected rightFoot = new Circle(vec2.create(), this.footRadius); - - public constructor(center: vec2, public readonly gameObject: GameObject = null) { + public constructor() { super(); - this.position = center; + + const circle = new BoundingCircle(null, vec2.create(), 200); + this.setCircles([circle, circle, circle]); } - public set position(value: vec2) { - vec2.copy(this.boundingCircle.center, value); - vec2.add(this.head.center, value, this.headOffset); - vec2.add(this.leftFoot.center, value, this.leftFootOffset); - vec2.add(this.rightFoot.center, value, this.rightFootOffset); - } - - public get center(): vec2 { - return this.boundingCircle.center; - } - - public get radius(): number { - return this.boundingCircle.radius; + public setCircles([head, leftFoot, rightFoot]: [ + BoundingCircle, + BoundingCircle, + BoundingCircle + ]) { + this.head = head; + this.leftFoot = leftFoot; + this.rightFoot = rightFoot; } public minDistance(target: vec2): number { - return this.boundingCircle.minDistance(target); - } - public normal(from: vec2): vec2 { - return this.boundingCircle.normal(from); - } - - public get boundingBox(): BoundingBox { - return this.boundingCircle.boundingBox; - } - - public isInside(target: vec2): boolean { - return this.minDistance(target) < 0; - } - - public clone(): BlobShape { - return new BlobShape(this.boundingCircle.center, this.gameObject); + return Math.min( + this.head.distance(target), + this.leftFoot.distance(target), + this.rightFoot.distance(target) + ); } protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { @@ -120,8 +99,8 @@ export class BlobShape extends Drawable implements IShape { this.rightFoot.center, transform2d ), - headRadius: this.headRadius * transform1d, - footRadius: this.footRadius * transform1d, + headRadius: this.head.radius * transform1d, + footRadius: this.leftFoot.radius * transform1d, }; } } diff --git a/frontend/src/scripts/shapes/types/circle-shape.ts b/frontend/src/scripts/shapes/types/circle-shape.ts index d9fbd34..158b468 100644 --- a/frontend/src/scripts/shapes/types/circle-shape.ts +++ b/frontend/src/scripts/shapes/types/circle-shape.ts @@ -1,12 +1,8 @@ import { vec2 } from 'gl-matrix'; import { Circle } from 'sdf-2d'; import { GameObject } from '../../objects/game-object'; -import { BoundingBox } from '../bounding-box'; -import { IShape } from '../i-shape'; - -export class CircleShape extends Circle implements IShape { - public readonly isInverted = false; +export class CircleShape extends Circle { public constructor( center = vec2.create(), radius = 0, @@ -15,34 +11,6 @@ export class CircleShape extends Circle implements IShape { super(center, radius); } - public distance(target: vec2): number { - return vec2.distance(this.center, target) - this.radius; - } - - public normal(from: vec2): vec2 { - const diff = vec2.subtract(vec2.create(), from, this.center); - return vec2.normalize(diff, diff); - } - - public get boundingBox(): BoundingBox { - return new BoundingBox( - this, - this.center.x - this.radius, - this.center.x + this.radius, - this.center.y - this.radius, - this.center.y + this.radius - ); - } - - public isInside(target: vec2): boolean { - return this.distance(target) < 0; - } - - public areIntersecting(other: CircleShape): boolean { - const distance = vec2.distance(this.center, other.center); - return distance < this.radius + other.radius; - } - public clone(): CircleShape { return new CircleShape(vec2.clone(this.center), this.radius, this.gameObject); } diff --git a/frontend/src/scripts/shapes/types/tunnel-shape.ts b/frontend/src/scripts/shapes/types/tunnel-shape.ts index df46f08..8e3b977 100644 --- a/frontend/src/scripts/shapes/types/tunnel-shape.ts +++ b/frontend/src/scripts/shapes/types/tunnel-shape.ts @@ -1,81 +1,17 @@ import { vec2 } from 'gl-matrix'; import { InvertedTunnel } from 'sdf-2d'; -import { clamp01 } from '../../helper/clamp'; -import { rotate90Deg } from '../../helper/rotate-90-deg'; -import { GameObject } from '../../objects/game-object'; -import { BoundingBox } from '../bounding-box'; -import { IShape } from '../i-shape'; -export class TunnelShape extends InvertedTunnel implements IShape { - constructor( - readonly from: vec2, - readonly to: vec2, - readonly fromRadius: number, - readonly toRadius: number, - public readonly gameObject: GameObject = null - ) { +export class TunnelShape extends InvertedTunnel { + constructor(from: vec2, to: vec2, fromRadius: number, toRadius: number) { super(from, to, fromRadius, toRadius); } - public get boundingBox(): BoundingBox { - const xMin = Math.min(this.from.x - this.fromRadius, this.to.x - this.toRadius); - const yMin = Math.min(this.from.y - this.fromRadius, this.to.y - this.toRadius); - const xMax = Math.max(this.from.x + this.fromRadius, this.to.x + this.toRadius); - const yMax = Math.max(this.from.y + this.fromRadius, this.to.y + this.toRadius); - - return new BoundingBox(this, xMin, xMax, yMin, yMax); - } - - public normal(target: vec2): vec2 { - const targetFromDelta = vec2.subtract(vec2.create(), target, this.from); - - const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from); - - const h = clamp01( - vec2.dot(targetFromDelta, toFromDelta) / vec2.dot(toFromDelta, toFromDelta) - ); - - let diff = vec2.create(); - - if (h == 1) { - vec2.subtract(diff, target, this.to); - } else if (h == 0) { - vec2.subtract(diff, target, this.from); - } else { - const side = Math.sign( - toFromDelta.x * targetFromDelta.y - toFromDelta.y * targetFromDelta.x - ); - - const normal = rotate90Deg(toFromDelta); - vec2.normalize(normal, normal); - - const translatedFrom = vec2.add( - vec2.create(), - this.from, - vec2.scale(vec2.create(), normal, side * this.fromRadius) - ); - - const translatedTo = vec2.add( - vec2.create(), - this.to, - vec2.scale(vec2.create(), normal, side * this.toRadius) - ); - - diff = rotate90Deg(vec2.subtract(vec2.create(), translatedTo, translatedFrom)); - - vec2.scale(diff, diff, side); - } - - return vec2.normalize(diff, diff); - } - public clone(): TunnelShape { return new TunnelShape( vec2.clone(this.from), vec2.clone(this.to), this.fromRadius, - this.toRadius, - this.gameObject + this.toRadius ); } }