From c5d72e83b791137692a7cc3199b0c5011e0f628e Mon Sep 17 00:00:00 2001 From: phisn Date: Sun, 22 Sep 2024 00:58:04 +0200 Subject: [PATCH] Major web-game refactor for new core game --- .../web-game/src/game/model/interpolation.ts | 122 + packages/web-game/src/game/model/store.ts | 8 +- .../src/game/modules/module-camera.ts | 98 +- .../src/game/modules/module-hook-handler.ts | 16 +- .../game/modules/module-input/module-input.ts | 4 +- .../src/game/modules/module-input/mouse.ts | 17 +- .../game/modules/module-input/touch-alt.ts | 17 +- .../modules/module-input/touch-vertical.ts | 17 +- .../src/game/modules/module-input/touch.ts | 17 +- .../game/modules/module-lobby/module-lobby.ts | 85 +- .../module-particles/module-particles.ts | 93 +- .../module-particles/particles-simulation.ts | 6 +- .../game/modules/module-scene-agent/colors.ts | 20 - .../module-scene-agent/module-scene-agent.ts | 113 - .../module-scene-agent/objects/flag.ts | 49 - .../module-scene-agent/objects/rocket.ts | 50 - .../module-scene-agent/objects/test.svg | 1 - .../module-scene-agent/svg-loader.d.ts | 64 - .../modules/module-scene-agent/svg-loader.js | 2702 ----------------- .../game/modules/module-scene-agent/svg.ts | 26 - .../game/modules/module-scene/module-scene.ts | 166 - .../module-scene/mutatable-shape-geometry.ts | 73 - .../src/game/modules/module-ui/module-ui.ts | 12 +- .../modules/module-visual/module-scene.ts | 101 + .../mutatable-shape-geometry.ts | 0 .../objects/flag.ts | 29 +- .../objects/rocket.ts | 0 .../{module-scene => module-visual}/svg.ts | 0 packages/web-game/src/game/web-game.ts | 29 +- 29 files changed, 436 insertions(+), 3499 deletions(-) create mode 100644 packages/web-game/src/game/model/interpolation.ts delete mode 100644 packages/web-game/src/game/modules/module-scene-agent/colors.ts delete mode 100644 packages/web-game/src/game/modules/module-scene-agent/module-scene-agent.ts delete mode 100644 packages/web-game/src/game/modules/module-scene-agent/objects/flag.ts delete mode 100644 packages/web-game/src/game/modules/module-scene-agent/objects/rocket.ts delete mode 100644 packages/web-game/src/game/modules/module-scene-agent/objects/test.svg delete mode 100644 packages/web-game/src/game/modules/module-scene-agent/svg-loader.d.ts delete mode 100644 packages/web-game/src/game/modules/module-scene-agent/svg-loader.js delete mode 100644 packages/web-game/src/game/modules/module-scene-agent/svg.ts delete mode 100644 packages/web-game/src/game/modules/module-scene/module-scene.ts delete mode 100644 packages/web-game/src/game/modules/module-scene/mutatable-shape-geometry.ts create mode 100644 packages/web-game/src/game/modules/module-visual/module-scene.ts rename packages/web-game/src/game/modules/{module-scene-agent => module-visual}/mutatable-shape-geometry.ts (100%) rename packages/web-game/src/game/modules/{module-scene => module-visual}/objects/flag.ts (56%) rename packages/web-game/src/game/modules/{module-scene => module-visual}/objects/rocket.ts (100%) rename packages/web-game/src/game/modules/{module-scene => module-visual}/svg.ts (100%) diff --git a/packages/web-game/src/game/model/interpolation.ts b/packages/web-game/src/game/model/interpolation.ts new file mode 100644 index 00000000..88f472e2 --- /dev/null +++ b/packages/web-game/src/game/model/interpolation.ts @@ -0,0 +1,122 @@ +import { EntityWith } from "game/src/framework/entity" +import { Game } from "game/src/game" +import { GameComponents } from "game/src/model/store" + +export interface Interpolation { + x: number + y: number + rotation: number +} + +interface MappingEntry { + interpolation: Interpolation + entity: EntityWith + + previousX: number + previousY: number + previousRotation: number +} + +export class InterpolationStore { + private mapping: Map = new Map() + + constructor(game: Game) { + game.store.entities.listen( + ["body"], + entity => { + const body = entity.get("body") + + if (body.isFixed() === false) { + this.mapping.set(entity.id, { + interpolation: { + x: body.translation().x, + y: body.translation().y, + rotation: body.rotation(), + }, + entity, + + previousX: body.translation().x, + previousY: body.translation().y, + previousRotation: body.rotation(), + }) + } + }, + entity => { + this.mapping.delete(entity.id) + }, + ) + } + + get(id: number): Interpolation | undefined { + return this.mapping.get(id)?.interpolation + } + + *interpolations() { + for (const [id, { interpolation }] of this.mapping) { + yield [id, interpolation] as const + } + } + + reset(id: number) { + const entry = this.mapping.get(id) + + if (entry === undefined) { + return + } + + const { entity, interpolation } = entry + + interpolation.x = entity.get("body").translation().x + interpolation.y = entity.get("body").translation().y + interpolation.rotation = entity.get("body").rotation() + + entry.previousX = interpolation.x + entry.previousY = interpolation.y + entry.previousRotation = interpolation.rotation + } + + onUpdate(_delta: number, overstep: number) { + for (const entry of this.mapping.values()) { + const { entity, interpolation } = entry + + const translation = entity.get("body").translation() + const rotation = entity.get("body").rotation() + + interpolation.x = lerp(entry.previousX, translation.x, overstep) + interpolation.y = lerp(entry.previousY, translation.y, overstep) + interpolation.rotation = slerp(entry.previousRotation, rotation, overstep) + + entry.previousX = interpolation.x + entry.previousY = interpolation.y + entry.previousRotation = interpolation.rotation + } + } + + onLastFixedUpdate() { + for (const entry of this.mapping.values()) { + const { entity, interpolation } = entry + + const translation = entity.get("body").translation() + const rotation = entity.get("body").rotation() + + interpolation.x = translation.x + interpolation.y = translation.y + interpolation.rotation = rotation + + entry.previousX = translation.x + entry.previousY = translation.y + entry.previousRotation = rotation + } + } +} + +export function lerp(previous: number, next: number, t: number) { + return (1 - t) * previous + t * next +} + +export function slerp(previous: number, next: number, t: number) { + const difference = next - previous + const shortestAngle = (((difference % (2 * Math.PI)) + 3 * Math.PI) % (2 * Math.PI)) - Math.PI + + return previous + shortestAngle * t +} diff --git a/packages/web-game/src/game/model/store.ts b/packages/web-game/src/game/model/store.ts index 3a5cac8f..73f86fd0 100644 --- a/packages/web-game/src/game/model/store.ts +++ b/packages/web-game/src/game/model/store.ts @@ -1,15 +1,21 @@ import * as RAPIER from "@dimforge/rapier2d" import { Game } from "game/src/game" import { Scene, WebGLRenderer } from "three" +import { InterpolationStore } from "./interpolation" import { GameSettings } from "./settings" export class WebGameStore { public game: Game + public interpolation: InterpolationStore public renderer: WebGLRenderer public scene: Scene - constructor(settings: GameSettings, renderer: WebGLRenderer) { + constructor( + public settings: GameSettings, + renderer: WebGLRenderer, + ) { this.game = new Game(settings, { rapier: RAPIER }) + this.interpolation = new InterpolationStore(this.game) this.renderer = renderer this.scene = new Scene() } diff --git a/packages/web-game/src/game/modules/module-camera.ts b/packages/web-game/src/game/modules/module-camera.ts index d10388cb..5317d9bd 100644 --- a/packages/web-game/src/game/modules/module-camera.ts +++ b/packages/web-game/src/game/modules/module-camera.ts @@ -1,9 +1,7 @@ -import { LevelCapturedMessage } from "runtime/src/core/level-capture/level-captured-message" -import { RuntimeComponents } from "runtime/src/core/runtime-components" +import { levelComponents, LevelEntity } from "game/src/modules/module-level" +import { rocketComponents, RocketEntity } from "game/src/modules/module-rocket" import * as THREE from "three" -import { EntityWith, MessageCollector } from "../../../../_runtime-framework/src" -import { ExtendedComponents } from "../runtime-extension/extended-components" -import { ExtendedRuntime } from "../runtime-extension/new-extended-runtime" +import { WebGameStore } from "../model/store" type TransitionAnimation = | undefined @@ -31,7 +29,7 @@ export class ModuleCamera extends THREE.OrthographicCamera { private configAnimationSpeed: number private configZoom: number - private rocket: EntityWith + private getRocket: () => RocketEntity private cameraTargetSize: THREE.Vector2 = new THREE.Vector2() private levelConstraintPosition: THREE.Vector2 = new THREE.Vector2() @@ -41,25 +39,22 @@ export class ModuleCamera extends THREE.OrthographicCamera { private transitionAnimation: TransitionAnimation = undefined private startAnimation: StartAnimation = undefined - private levelCapturedCollector: MessageCollector - - constructor(private runtime: ExtendedRuntime) { + constructor(private store: WebGameStore) { super() this.position.z = 5 this.configAnimationSpeed = 1.0 / 1000.0 this.configZoom = 1920 * 0.04 - const [rocket] = runtime.factoryContext.store.find("rocket", "rigidBody", "interpolation") - this.rocket = rocket - this.position.x = rocket.components.rigidBody.translation().x - this.position.y = rocket.components.rigidBody.translation().y - - this.levelCapturedCollector = runtime.factoryContext.messageStore.collect("levelCaptured") + this.getRocket = store.game.store.entities.single(...rocketComponents) + const rocket = this.getRocket() + const body = rocket.get("body") + this.position.x = body.translation().x + this.position.y = body.translation().y - const firstLevel = runtime.factoryContext.store - .find("level") - .find(x => x.components.level.captured) + const firstLevel = store.game.store.entities + .multiple(...levelComponents) + .find(x => x.get("level").completed) if (!firstLevel) { throw new Error("No first level found") @@ -72,10 +67,26 @@ export class ModuleCamera extends THREE.OrthographicCamera { } this.updateLevelConstraintFromLevel(firstLevel) + + this.store.game.store.events.listen({ + captured: ({ level }) => { + this.updateLevelConstraintFromLevel(level) + + this.transitionAnimation = { + progress: 0, + startSize: this.size.clone(), + targetPosition: new THREE.Vector2(), + startPosition: this.position.clone(), + } + + this.updateCameraSize() + this.updateCameraPosition() + }, + }) } onCanvasResize(width: number, height: number) { - this.runtime.factoryContext.renderer.setSize(width, height, false) + this.store.renderer.setSize(width, height, false) const max = Math.max(width, height) @@ -86,24 +97,6 @@ export class ModuleCamera extends THREE.OrthographicCamera { this.updateCameraPosition() } - onFixedUpdate() { - const lastCaptured = [...this.levelCapturedCollector].at(-1) - - if (lastCaptured) { - this.updateLevelConstraintFromLevel(lastCaptured.level) - - this.transitionAnimation = { - progress: 0, - startSize: this.size.clone(), - targetPosition: new THREE.Vector2(), - startPosition: this.position.clone(), - } - - this.updateCameraSize() - this.updateCameraPosition() - } - } - onUpdate(delta: number) { this.updateCameraPosition() @@ -190,8 +183,8 @@ export class ModuleCamera extends THREE.OrthographicCamera { } updateViewport() { - const rendererWidth = this.runtime.factoryContext.renderer.domElement.clientWidth - const rendererHeight = this.runtime.factoryContext.renderer.domElement.clientHeight + const rendererWidth = this.store.renderer.domElement.clientWidth + const rendererHeight = this.store.renderer.domElement.clientHeight let viewportSizeX = this.size.x / this.cameraTargetSize.x let viewportSizeY = this.size.y / this.cameraTargetSize.y @@ -204,7 +197,7 @@ export class ModuleCamera extends THREE.OrthographicCamera { const viewportX = 0.5 * rendererWidth * (1 - viewportSizeX) const viewportY = 0.5 * rendererHeight * (1 - viewportSizeY) - this.runtime.factoryContext.renderer.setViewport( + this.store.renderer.setViewport( viewportX, viewportY, viewportSizeX * rendererWidth, @@ -236,10 +229,15 @@ export class ModuleCamera extends THREE.OrthographicCamera { } } - const rocketPositionInViewX = - this.rocket.components.interpolation.position.x - this.levelConstraintPosition.x - const rocketPositionInViewY = - this.rocket.components.interpolation.position.y - this.levelConstraintPosition.y + const rocket = this.getRocket() + const interpolation = this.store.interpolation.get(rocket.id) + + if (interpolation === undefined) { + throw new Error("Rocket interpolation not found") + } + + const rocketPositionInViewX = interpolation.x - this.levelConstraintPosition.x + const rocketPositionInViewY = interpolation.y - this.levelConstraintPosition.y const cameraPositionX = this.levelConstraintPosition.x + @@ -258,15 +256,17 @@ export class ModuleCamera extends THREE.OrthographicCamera { } } - private updateLevelConstraintFromLevel(entity: EntityWith) { + private updateLevelConstraintFromLevel(level: LevelEntity) { + const levelComponent = level.get("level") + this.levelConstraintSize = new THREE.Vector2( - entity.components.level.camera.bottomRight.x - entity.components.level.camera.topLeft.x, - entity.components.level.camera.topLeft.y - entity.components.level.camera.bottomRight.y, + levelComponent.cameraRect.right - levelComponent.cameraRect.left, + levelComponent.cameraRect.top - levelComponent.cameraRect.bottom, ) this.levelConstraintPosition = new THREE.Vector2( - entity.components.level.camera.topLeft.x, - entity.components.level.camera.bottomRight.y, + levelComponent.cameraRect.left, + levelComponent.cameraRect.bottom, ) } diff --git a/packages/web-game/src/game/modules/module-hook-handler.ts b/packages/web-game/src/game/modules/module-hook-handler.ts index 4b4bf935..df9efd97 100644 --- a/packages/web-game/src/game/modules/module-hook-handler.ts +++ b/packages/web-game/src/game/modules/module-hook-handler.ts @@ -1,17 +1,9 @@ -import { ExtendedRuntime } from "../runtime-extension/new-extended-runtime" +import { WebGameStore } from "../model/store" export class ModuleHookHandler { - constructor(private runtime: ExtendedRuntime) { - runtime.factoryContext.messageStore.listenTo("finished", () => { - /* - // protoc replaymodel to base64 bytes - const replaymodel = this.runtime.factoryContext.replayCapture.replay - const raw = ReplayModel.encode(replaymodel).finish() - const base64 = Buffer.from(raw).toString("base64") - console.log("ReplayModel: ", base64) - */ - - this.runtime.factoryContext.settings.hooks?.onFinished?.(runtime) + constructor(private runtime: WebGameStore) { + runtime.game.store.events.listen({ + death: () => {}, }) } } diff --git a/packages/web-game/src/game/modules/module-input/module-input.ts b/packages/web-game/src/game/modules/module-input/module-input.ts index 192783b8..cba9575a 100644 --- a/packages/web-game/src/game/modules/module-input/module-input.ts +++ b/packages/web-game/src/game/modules/module-input/module-input.ts @@ -1,4 +1,4 @@ -import { ExtendedRuntime } from "../../runtime-extension/new-extended-runtime" +import { WebGameStore } from "../../model/store" import { Keyboard } from "./keyboard" import { Mouse } from "./mouse" import { Touch } from "./touch" @@ -20,7 +20,7 @@ export class ModuleInput { // private precomputed: RuntimeSystemContext[] // private iter = -1 - constructor(runtime: ExtendedRuntime) { + constructor(runtime: WebGameStore) { this.keyboard = new Keyboard() this.mouse = new Mouse(runtime) this.touch = new Touch(runtime) diff --git a/packages/web-game/src/game/modules/module-input/mouse.ts b/packages/web-game/src/game/modules/module-input/mouse.ts index 9c7c2abe..a9dd31a9 100644 --- a/packages/web-game/src/game/modules/module-input/mouse.ts +++ b/packages/web-game/src/game/modules/module-input/mouse.ts @@ -1,4 +1,4 @@ -import { ExtendedRuntime } from "../../runtime-extension/new-extended-runtime" +import { WebGameStore } from "../../model/store" const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) @@ -14,24 +14,19 @@ export class Mouse { private ptrEvents = ["pointerdown", "pointerup", "pointermove", "pointercancel"] as const - constructor(private runtime: ExtendedRuntime) { + constructor(private runtime: WebGameStore) { this.onPointerEvent = this.onPointerEvent.bind(this) for (const ptrEvent of this.ptrEvents) { - runtime.factoryContext.renderer.domElement.addEventListener( - ptrEvent, - this.onPointerEvent, - { capture: true }, - ) + runtime.renderer.domElement.addEventListener(ptrEvent, this.onPointerEvent, { + capture: true, + }) } } dispose() { for (const ptrEvent of this.ptrEvents) { - this.runtime.factoryContext.renderer.domElement.removeEventListener( - ptrEvent, - this.onPointerEvent, - ) + this.runtime.renderer.domElement.removeEventListener(ptrEvent, this.onPointerEvent) } } diff --git a/packages/web-game/src/game/modules/module-input/touch-alt.ts b/packages/web-game/src/game/modules/module-input/touch-alt.ts index c7ca316e..00aa9dcd 100644 --- a/packages/web-game/src/game/modules/module-input/touch-alt.ts +++ b/packages/web-game/src/game/modules/module-input/touch-alt.ts @@ -1,4 +1,4 @@ -import { ExtendedRuntime } from "../../runtime-extension/new-extended-runtime" +import { WebGameStore } from "../../model/store" export class TouchAlt { private pointer?: { @@ -15,24 +15,19 @@ export class TouchAlt { private touchEvents = ["touchstart", "touchend", "touchmove", "touchcancel"] as const - constructor(private runtime: ExtendedRuntime) { + constructor(private runtime: WebGameStore) { this.onTouchEvent = this.onTouchEvent.bind(this) for (const ptrEvent of this.touchEvents) { - runtime.factoryContext.renderer.domElement.addEventListener( - ptrEvent, - this.onTouchEvent, - { capture: true }, - ) + runtime.renderer.domElement.addEventListener(ptrEvent, this.onTouchEvent, { + capture: true, + }) } } dispose() { for (const ptrEvent of this.touchEvents) { - this.runtime.factoryContext.renderer.domElement.removeEventListener( - ptrEvent, - this.onTouchEvent, - ) + this.runtime.renderer.domElement.removeEventListener(ptrEvent, this.onTouchEvent) } } diff --git a/packages/web-game/src/game/modules/module-input/touch-vertical.ts b/packages/web-game/src/game/modules/module-input/touch-vertical.ts index 3242c6ce..2815368c 100644 --- a/packages/web-game/src/game/modules/module-input/touch-vertical.ts +++ b/packages/web-game/src/game/modules/module-input/touch-vertical.ts @@ -1,4 +1,4 @@ -import { ExtendedRuntime } from "../../runtime-extension/new-extended-runtime" +import { WebGameStore } from "../../model/store" export class TouchVertical { private pointer?: { @@ -15,24 +15,19 @@ export class TouchVertical { private touchEvents = ["touchstart", "touchend", "touchmove", "touchcancel"] as const - constructor(private runtime: ExtendedRuntime) { + constructor(private runtime: WebGameStore) { this.onTouchEvent = this.onTouchEvent.bind(this) for (const ptrEvent of this.touchEvents) { - runtime.factoryContext.renderer.domElement.addEventListener( - ptrEvent, - this.onTouchEvent, - { capture: true }, - ) + runtime.renderer.domElement.addEventListener(ptrEvent, this.onTouchEvent, { + capture: true, + }) } } dispose() { for (const ptrEvent of this.touchEvents) { - this.runtime.factoryContext.renderer.domElement.removeEventListener( - ptrEvent, - this.onTouchEvent, - ) + this.runtime.renderer.domElement.removeEventListener(ptrEvent, this.onTouchEvent) } } diff --git a/packages/web-game/src/game/modules/module-input/touch.ts b/packages/web-game/src/game/modules/module-input/touch.ts index 1d9e739d..928babbe 100644 --- a/packages/web-game/src/game/modules/module-input/touch.ts +++ b/packages/web-game/src/game/modules/module-input/touch.ts @@ -1,4 +1,4 @@ -import { ExtendedRuntime } from "../../runtime-extension/new-extended-runtime" +import { WebGameStore } from "../../model/store" export class Touch { private thrustPointerId?: number @@ -16,24 +16,19 @@ export class Touch { private touchEvents = ["touchstart", "touchend", "touchmove", "touchcancel"] as const - constructor(private runtime: ExtendedRuntime) { + constructor(private runtime: WebGameStore) { this.onTouchEvent = this.onTouchEvent.bind(this) for (const ptrEvent of this.touchEvents) { - runtime.factoryContext.renderer.domElement.addEventListener( - ptrEvent, - this.onTouchEvent, - { capture: true }, - ) + runtime.renderer.domElement.addEventListener(ptrEvent, this.onTouchEvent, { + capture: true, + }) } } dispose() { for (const ptrEvent of this.touchEvents) { - this.runtime.factoryContext.renderer.domElement.removeEventListener( - ptrEvent, - this.onTouchEvent, - ) + this.runtime.renderer.domElement.removeEventListener(ptrEvent, this.onTouchEvent) } } diff --git a/packages/web-game/src/game/modules/module-lobby/module-lobby.ts b/packages/web-game/src/game/modules/module-lobby/module-lobby.ts index 893316e3..2f2563aa 100644 --- a/packages/web-game/src/game/modules/module-lobby/module-lobby.ts +++ b/packages/web-game/src/game/modules/module-lobby/module-lobby.ts @@ -1,16 +1,16 @@ +import { rocketComponents, RocketEntity } from "game/src/modules/module-rocket" import { Frame, FramePacket } from "shared/src/lobby-api/frame-packet" import { + messageFromServer, UPDATE_POSITIONS_EVERY_MS, UpdateFromClient, - messageFromServer, } from "shared/src/lobby-api/lobby-api" import { UserOther } from "shared/src/lobby-api/user-other" +import { lerp } from "three/src/math/MathUtils" import { Text } from "troika-three-text" -import { EntityWith } from "../../../../../_runtime-framework/src" -import { ExtendedComponents } from "../../runtime-extension/extended-components" -import { ExtendedRuntime } from "../../runtime-extension/new-extended-runtime" -import { lerp, slerp } from "../module-scene/module-scene" -import { Rocket } from "../module-scene/objects/rocket" +import { slerp } from "../../model/interpolation" +import { WebGameStore } from "../../model/store" +import { Rocket } from "../module-visual/objects/rocket" export class OtherUserGhost { private packets: FramePacket[] = [] @@ -22,7 +22,7 @@ export class OtherUserGhost { private previousFrame: Frame | undefined constructor( - private runtime: ExtendedRuntime, + private store: WebGameStore, private user: UserOther, ) { this.mesh = new Rocket(0.2) @@ -40,11 +40,11 @@ export class OtherUserGhost { this.mesh.add(text as any) - this.runtime.factoryContext.scene.add(this.mesh) + this.store.scene.add(this.mesh) } dispose() { - this.runtime.factoryContext.scene.remove(this.mesh) + this.store.scene.remove(this.mesh) } addPacket(packet: FramePacket) { @@ -107,15 +107,15 @@ export class OtherUserGhost { export class OtherUserGhosts { private ghosts: Map - constructor(private runtime: ExtendedRuntime) { + constructor(private store: WebGameStore) { this.ghosts = new Map() } addPackets(packets: FramePacket[]) { for (const packet of packets) { if ( - this.runtime.factoryContext.settings.instanceType === "play" && - this.runtime.factoryContext.settings?.lobby?.username === packet.username + this.store.settings.instanceType === "play" && + this.store.settings?.lobby?.username === packet.username ) { continue } @@ -132,8 +132,8 @@ export class OtherUserGhosts { addPlayer(user: UserOther) { if ( - this.runtime.factoryContext.settings.instanceType === "play" && - this.runtime.factoryContext.settings?.lobby?.username === user.username + this.store.settings.instanceType === "play" && + this.store.settings?.lobby?.username === user.username ) { return } @@ -142,13 +142,13 @@ export class OtherUserGhosts { return } - this.ghosts.set(user.username, new OtherUserGhost(this.runtime, user)) + this.ghosts.set(user.username, new OtherUserGhost(this.store, user)) } removePlayer(username: string) { if ( - this.runtime.factoryContext.settings.instanceType === "play" && - this.runtime.factoryContext.settings?.lobby?.username === username + this.store.settings.instanceType === "play" && + this.store.settings?.lobby?.username === username ) { return } @@ -157,7 +157,7 @@ export class OtherUserGhosts { if (ghost) { const user = ghost.getUser() - this.runtime.factoryContext.settings.hooks?.onUserLeft?.(user) + this.store.settings.hooks?.onUserLeft?.(user) ghost.dispose() } @@ -181,48 +181,48 @@ export class OtherUserGhosts { } export class ModuleLobby { - private packet: FramePacket - private rocket: EntityWith - private lastSend = 0 - private ws: WebSocket | undefined + private getRocket: () => RocketEntity + private disposed = false + private lastSend = 0 + private lastSetup: number + private otherPlayers: OtherUserGhosts + private packet: FramePacket private url: string + private ws: WebSocket | undefined - private lastSetup: number private setupEveryMs = 1000 * 30 - private otherPlayers: OtherUserGhosts - - constructor(private runtime: ExtendedRuntime) { - if (runtime.factoryContext.settings.instanceType !== "play") { + constructor(private store: WebGameStore) { + if (store.settings.instanceType !== "play") { throw new Error("ModuleLobby can only be used in play mode") } - if (runtime.factoryContext.settings.lobby === undefined) { + if (store.settings.lobby === undefined) { throw new Error("ModuleLobby requires a user token") } - const lobbyId = `${runtime.factoryContext.settings.worldname}-${runtime.factoryContext.settings.gamemode}` + const lobbyId = `${store.settings.worldname}-${store.settings.gamemode}` - const url = new URL(`wss://${runtime.factoryContext.settings.lobby.host}/lobby`) - url.searchParams.set("authorization", runtime.factoryContext.settings.lobby.token) + const url = new URL(`wss://${store.settings.lobby.host}/lobby`) + url.searchParams.set("authorization", store.settings.lobby.token) url.searchParams.set("id", lobbyId) this.url = url.toString() - this.otherPlayers = new OtherUserGhosts(runtime) + this.otherPlayers = new OtherUserGhosts(store) this.lastSetup = Date.now() this.packet = { - username: runtime.factoryContext.settings.lobby.username, + username: store.settings.lobby.username, frames: [], } - this.rocket = runtime.factoryContext.store.find("rocket", "rigidBody")[0] + this.getRocket = store.game.store.entities.single(...rocketComponents) this.lastSend = Date.now() - const token = runtime.factoryContext.settings.lobby.token + const token = store.settings.lobby.token this.setup(token) } @@ -238,12 +238,15 @@ export class ModuleLobby { return } + const rocket = this.getRocket() + const body = rocket.get("body") + this.otherPlayers.onFixedUpdate() this.packet.frames.push({ - x: this.rocket.components.rigidBody.translation().x, - y: this.rocket.components.rigidBody.translation().y, - rotation: this.rocket.components.rigidBody.rotation(), + x: body.translation().x, + y: body.translation().y, + rotation: body.rotation(), }) const now = Date.now() @@ -291,7 +294,7 @@ export class ModuleLobby { case "update": for (const user of data.usersConnected) { this.otherPlayers.addPlayer(user) - this.runtime.factoryContext.settings.hooks?.onUserJoined?.(user) + this.store.settings.hooks?.onUserJoined?.(user) } this.otherPlayers.addPackets(data.framePackets) @@ -307,7 +310,7 @@ export class ModuleLobby { this.otherPlayers.addPlayer(user) } - this.runtime.factoryContext.settings.hooks?.onConnected?.(data.users.length) + this.store.settings.hooks?.onConnected?.(data.users.length) break } @@ -320,7 +323,7 @@ export class ModuleLobby { this.ws.onclose = event => { console.warn("Lobby websocket closed with reason: ", event.reason, event.code) - this.runtime.factoryContext.settings.hooks?.onDisconnected?.() + this.store.settings.hooks?.onDisconnected?.() if (this.ws === previousWs) { this.rerunSetup(token) diff --git a/packages/web-game/src/game/modules/module-particles/module-particles.ts b/packages/web-game/src/game/modules/module-particles/module-particles.ts index 7b940957..f74fcc6e 100644 --- a/packages/web-game/src/game/modules/module-particles/module-particles.ts +++ b/packages/web-game/src/game/modules/module-particles/module-particles.ts @@ -1,14 +1,12 @@ +import { rocketComponents, RocketEntity } from "game/src/modules/module-rocket" import { EntityType } from "runtime/proto/world" -import { RocketDeathMessage } from "runtime/src/core/rocket/rocket-death-message" import { changeAnchor } from "runtime/src/model/world/change-anchor" import { entityRegistry } from "runtime/src/model/world/entity-registry" import { Color } from "three" -import { EntityWith, MessageCollector } from "../../../../../_runtime-framework/src" -import { ExtendedComponents } from "../../runtime-extension/extended-components" -import { ExtendedRuntime } from "../../runtime-extension/new-extended-runtime" +import { WebGameStore } from "../../model/store" import { - ParticleTemplate, downBias, + ParticleTemplate, randomBetween, randomBetweenDownBiased, } from "./particle-template" @@ -66,68 +64,71 @@ const PARTICLE_TEMPLATE_DEATH: ParticleTemplate = () => ({ export class ModuleParticles { private rocketTime = 0 - private rocket: EntityWith - - private rocketDeathCollector: MessageCollector + private getRocket: () => RocketEntity private simulation: ParticleSimulation - constructor(private runtime: ExtendedRuntime) { - const shapes = runtime.factoryContext.store - .find("shape") - .map(x => x.components.shape.vertices.map(x => x.position)) - - this.simulation = new ParticleSimulation(runtime, shapes) - - const [rocket] = runtime.factoryContext.store.find("rocket", "interpolation", "rigidBody") - this.rocket = rocket - - this.rocketDeathCollector = runtime.factoryContext.messageStore.collect("rocketDeath") + constructor(private store: WebGameStore) { + const shapes = store.game.store.entities + .multiple("shape") + .map(x => x.get("shape").vertices.map(x => x.position)) + + this.simulation = new ParticleSimulation(store, shapes) + this.getRocket = store.game.store.entities.single(...rocketComponents) + + store.game.store.events.listen({ + death: ({ normal, contactPoint }) => { + for (let i = 0; i < 50; ++i) { + const rotationFromNormal = Math.atan2(normal.y, normal.x) + + this.simulation.createParticle( + PARTICLE_TEMPLATE_DEATH, + contactPoint, + rotationFromNormal + Math.PI / 2, + { x: 0, y: 0 }, + ) + } + }, + }) } update(delta: number) { this.simulation.onUpdate(delta) - if (this.rocket.components.rocket.thrusting) { + const rocket = this.getRocket() + const rocketComponent = rocket.get("rocket") + + if (rocketComponent.thrust) { this.rocketTime += delta + const interpolation = this.store.interpolation.get(rocket.id) + const body = rocket.get("body") + + if (interpolation === undefined) { + throw new Error("Rocket interpolation not found") + } + + const spawnPosition = changeAnchor( + interpolation, + interpolation.rotation, + entityRegistry[EntityType.ROCKET], + { x: 0.5, y: 0.5 }, + { x: 0.5, y: 0.3 }, + ) + for (let i = 0; this.rocketTime >= 16 / 3; ++i) { this.rocketTime -= 16 / 3 - const spawnPosition = changeAnchor( - this.rocket.components.interpolation.position, - this.rocket.components.interpolation.rotation, - entityRegistry[EntityType.ROCKET], - { x: 0.5, y: 0.5 }, - { x: 0.5, y: 0.3 }, - ) - this.simulation.createParticle( PARTICLE_TEMPLATE_THRUST, spawnPosition, - this.rocket.components.interpolation.rotation, - this.rocket.components.rigidBody.linvel(), + interpolation.rotation, + body.linvel(), i, ) } } else { this.rocketTime = 0 } - - for (const message of this.rocketDeathCollector) { - for (let i = 0; i < 50; ++i) { - const normal = message.normal - const rotationFromNormal = Math.atan2(normal.y, normal.x) - - // console.log("death", message.position, message.contactPoint) - - this.simulation.createParticle( - PARTICLE_TEMPLATE_DEATH, - message.contactPoint, - rotationFromNormal + Math.PI / 2, - { x: 0, y: 0 }, - ) - } - } } } diff --git a/packages/web-game/src/game/modules/module-particles/particles-simulation.ts b/packages/web-game/src/game/modules/module-particles/particles-simulation.ts index ae9ec35a..cb083348 100644 --- a/packages/web-game/src/game/modules/module-particles/particles-simulation.ts +++ b/packages/web-game/src/game/modules/module-particles/particles-simulation.ts @@ -2,7 +2,7 @@ import { makeCCW, quickDecomp } from "poly-decomp-es" import { Point } from "runtime/src/model/point" import * as SAT from "sat" import * as THREE from "three" -import { ExtendedRuntime } from "../../runtime-extension/new-extended-runtime" +import { WebGameStore } from "../../model/store" import { Environment, aabbFromCircle, newEnvironment } from "./particle-environment" import { ParticleTemplate, @@ -34,14 +34,14 @@ export class ParticleSimulation { private instanceMatrix = new THREE.Matrix4() private maxInstances = 2048 - constructor(runtime: ExtendedRuntime, shapes: Point[][]) { + constructor(store: WebGameStore, shapes: Point[][]) { const geometry = new THREE.CircleGeometry(1, 32) const material = new THREE.MeshBasicMaterial({ color: 0xffffff }) this.particleMesh = new THREE.InstancedMesh(geometry, material, this.maxInstances) this.particleMesh.frustumCulled = false - runtime.factoryContext.scene.add(this.particleMesh) + store.scene.add(this.particleMesh) for (const shape of shapes) { const polygon = shape.map(p => [p.x, p.y] as [number, number]) diff --git a/packages/web-game/src/game/modules/module-scene-agent/colors.ts b/packages/web-game/src/game/modules/module-scene-agent/colors.ts deleted file mode 100644 index 0546e126..00000000 --- a/packages/web-game/src/game/modules/module-scene-agent/colors.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface AgentColors { - rocket: number - flagCaptureRegion: number - shape: number - outOfBounds: number -} - -export const agentColorsRGB: AgentColors = { - rocket: 0x00ff00, - flagCaptureRegion: 0x0000ff, - shape: 0xff0000, - outOfBounds: 0xff0000, -} - -export const agentColorsGrayScale: AgentColors = { - rocket: 0xdddddd, - flagCaptureRegion: 0x333333, - shape: 0xffffff, - outOfBounds: 0xffffff, -} diff --git a/packages/web-game/src/game/modules/module-scene-agent/module-scene-agent.ts b/packages/web-game/src/game/modules/module-scene-agent/module-scene-agent.ts deleted file mode 100644 index 9ffa19d9..00000000 --- a/packages/web-game/src/game/modules/module-scene-agent/module-scene-agent.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { RuntimeComponents } from "runtime/src/core/runtime-components" -import { RuntimeFactoryContext } from "runtime/src/core/runtime-factory-context" -import { RuntimeSystemContext } from "runtime/src/core/runtime-system-stack" -import { Runtime } from "runtime/src/runtime" -import * as THREE from "three" -import { EntityWith, SystemStack } from "../../../../../_runtime-framework/src" -import { AgentColors, agentColorsGrayScale, agentColorsRGB } from "./colors" -import { MutatableShapeGeometry } from "./mutatable-shape-geometry" -import { Flag } from "./objects/flag" -import { Rocket } from "./objects/rocket" - -export class ModuleSceneAgent { - private flags: Flag[] = [] - private rocket: Rocket - - private previousCurrentLevel?: EntityWith - - private upBound: THREE.Mesh - private downBound: THREE.Mesh - private leftBound: THREE.Mesh - private rightBound: THREE.Mesh - - constructor( - private scene: THREE.Scene, - private runtime: Runtime, - private grayScale: boolean, - ) { - const colors = grayScale ? agentColorsGrayScale : agentColorsRGB - - const [rocketEntity] = runtime.factoryContext.store.find("rocket", "rigidBody") - this.rocket = new Rocket(rocketEntity, colors) - this.scene.add(this.rocket) - - this.initShapes(runtime, colors) - - for (const levelEntity of runtime.factoryContext.store.find("level")) { - if (levelEntity.components.level.hideFlag) { - continue - } - - const flag = new Flag(levelEntity, colors) - scene.add(flag) - this.flags.push(flag) - } - - this.upBound = new THREE.Mesh( - new THREE.PlaneGeometry(100, 100), - new THREE.MeshBasicMaterial({ color: colors.outOfBounds }), - ) - - this.downBound = this.upBound.clone() - this.leftBound = this.upBound.clone() - this.rightBound = this.upBound.clone() - - this.scene.add(this.upBound) - this.scene.add(this.downBound) - this.scene.add(this.leftBound) - this.scene.add(this.rightBound) - - this.previousCurrentLevel = undefined - } - - private initShapes( - runtime: SystemStack, RuntimeSystemContext>, - colors: AgentColors, - ) { - const shapes = runtime.factoryContext.store.newSet("shape") - - for (const shape of shapes) { - const shapeGeometry = new MutatableShapeGeometry( - shape.components.shape.vertices.map(vertex => ({ - position: new THREE.Vector2(vertex.position.x, vertex.position.y), - color: colors.shape, - })), - ) - const shapeMaterial = new THREE.MeshBasicMaterial({ vertexColors: true }) - const shapeMesh = new THREE.Mesh(shapeGeometry, shapeMaterial) - shapeMesh.position.setZ(1) - - this.scene.add(shapeMesh) - } - } - - onUpdate() { - for (const flag of this.flags) { - flag.onUpdate() - } - - this.rocket.onUpdate() - - const currentLevel = this.rocket.entity.components.rocket!.currentLevel - - if (currentLevel !== this.previousCurrentLevel) { - const tl = currentLevel.components.level.boundsTL - const br = currentLevel.components.level.boundsBR - - const center = new THREE.Vector2((tl.x + br.x) / 2, (tl.y + br.y) / 2) - const width = Math.abs(br.x - tl.x) - const height = Math.abs(br.y - tl.y) - - this.upBound.position.set(center.x, center.y + height / 2 + 50, 0) - this.downBound.position.set(center.x, center.y - height / 2 - 50, 0) - this.leftBound.position.set(center.x - width / 2 - 50, center.y, 0) - this.rightBound.position.set(center.x + width / 2 + 50, center.y, 0) - - this.previousCurrentLevel = currentLevel - } - } - - getScene() { - return this.scene - } -} diff --git a/packages/web-game/src/game/modules/module-scene-agent/objects/flag.ts b/packages/web-game/src/game/modules/module-scene-agent/objects/flag.ts deleted file mode 100644 index e49c739d..00000000 --- a/packages/web-game/src/game/modules/module-scene-agent/objects/flag.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { RuntimeComponents } from "runtime/src/core/runtime-components" -import * as THREE from "three" -import { EntityWith } from "../../../../../../_runtime-framework/src" -import { AgentColors } from "../colors" - -/* -const prototypeRed = new Svg( - '', -) - -const prototypeGreen = new Svg( - '', -) -*/ - -export class Flag extends THREE.Object3D { - constructor( - private levelEntity: EntityWith, - colors: AgentColors, - ) { - super() - - const geometry = new THREE.BoxGeometry(1, 1, 1) - const materialRed = new THREE.MeshBasicMaterial({ color: colors.flagCaptureRegion }) - - const mesh = new THREE.Mesh(geometry, materialRed) - - this.add(mesh) - - this.position.set( - levelEntity.components.level.capturePosition.x, - levelEntity.components.level.capturePosition.y + - levelEntity.components.level.captureSize.y, - 0, - ) - - this.scale.set( - levelEntity.components.level.captureSize.x * 2, - levelEntity.components.level.captureSize.y * 2, - 1.0, - ) - } - - onUpdate() { - if (this.levelEntity.components.level.captured && this.visible) { - this.visible = false - } - } -} diff --git a/packages/web-game/src/game/modules/module-scene-agent/objects/rocket.ts b/packages/web-game/src/game/modules/module-scene-agent/objects/rocket.ts deleted file mode 100644 index bba7867a..00000000 --- a/packages/web-game/src/game/modules/module-scene-agent/objects/rocket.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { EntityType } from "runtime/proto/world" -import { RuntimeComponents } from "runtime/src/core/runtime-components" -import { entityRegistry } from "runtime/src/model/world/entity-registry" -import * as THREE from "three" -import { EntityWith } from "../../../../../../_runtime-framework/src" -import { AgentColors } from "../colors" -import { Svg } from "../svg" - -export class Rocket extends THREE.Object3D { - constructor( - public entity: EntityWith, - colors: AgentColors, - ) { - super() - - const colorStr = "#" + colors.rocket.toString(16).padStart(6, "0") - - // we do not use the original svg since its too sharp at the bottom - // and with lower resolution we do not paint any pixels there. therefore - // we just will the complete lower part - const svg = new Svg( - ``, - ) - - const scale = (0.15 * 1) / 25.0 - const rocketEntry = entityRegistry[EntityType.ROCKET] - - svg.scale.set(scale, scale, 1.0) - svg.rotation.set(0, 0, Math.PI) - svg.position.set(rocketEntry.width / 2, rocketEntry.height / 2, 1.0) - - this.add(svg) - - this.position.set( - entity.components.rigidBody.translation().x, - entity.components.rigidBody.translation().y, - 0, - ) - } - - onUpdate() { - this.position.set( - this.entity.components.rigidBody.translation().x, - this.entity.components.rigidBody.translation().y, - 0, - ) - - this.rotation.z = this.entity.components.rigidBody.rotation() - } -} diff --git a/packages/web-game/src/game/modules/module-scene-agent/objects/test.svg b/packages/web-game/src/game/modules/module-scene-agent/objects/test.svg deleted file mode 100644 index 09ca71c0..00000000 --- a/packages/web-game/src/game/modules/module-scene-agent/objects/test.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/web-game/src/game/modules/module-scene-agent/svg-loader.d.ts b/packages/web-game/src/game/modules/module-scene-agent/svg-loader.d.ts deleted file mode 100644 index a974aea2..00000000 --- a/packages/web-game/src/game/modules/module-scene-agent/svg-loader.d.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { BufferGeometry, Loader, LoadingManager, Shape, ShapePath, Vector2 } from "three" - -export interface SVGResultPaths extends ShapePath { - userData?: Record | undefined -} - -export interface SVGResult { - paths: SVGResultPaths[] - xml: XMLDocument -} - -export interface StrokeStyle { - strokeColor: string - strokeWidth: number - strokeLineJoin: string - strokeLineCap: string - strokeMiterLimit: number -} - -export class SVGLoader extends Loader { - constructor(manager?: LoadingManager) - - defaultDPI: number - defaultUnit: string - - parse(text: string): SVGResult - - static getStrokeStyle( - width?: number, - color?: string, - lineJoin?: string, - lineCap?: string, - miterLimit?: number, - ): StrokeStyle - - /** - * Generates a stroke with some witdh around the given path. - * @remarks The path can be open or closed (last point equals to first point) - * @param points Array of Vector2D (the path). Minimum 2 points. - * @param style Object with SVG properties as returned by SVGLoader.getStrokeStyle(), or SVGLoader.parse() in the path.userData.style object - * @param arcDivisions Arc divisions for round joins and endcaps. (Optional) - * @param minDistance Points closer to this distance will be merged. (Optional) - * @returns BufferGeometry with stroke triangles (In plane z = 0). UV coordinates are generated ('u' along path. 'v' across it, from left to right) - */ - static pointsToStroke( - points: Vector2[], - style: StrokeStyle, - arcDivisions?: number, - minDistance?: number, - ): BufferGeometry - - static pointsToStrokeWithBuffers( - points: Vector2[], - style: StrokeStyle, - arcDivisions?: number, - minDistance?: number, - vertices?: number[], - normals?: number[], - uvs?: number[], - vertexOffset?: number, - ): number - - static createShapes(shapePath: ShapePath): Shape[] -} diff --git a/packages/web-game/src/game/modules/module-scene-agent/svg-loader.js b/packages/web-game/src/game/modules/module-scene-agent/svg-loader.js deleted file mode 100644 index 74ff243c..00000000 --- a/packages/web-game/src/game/modules/module-scene-agent/svg-loader.js +++ /dev/null @@ -1,2702 +0,0 @@ -import { - Box2, - BufferGeometry, - FileLoader, - Float32BufferAttribute, - Loader, - Matrix3, - Path, - Shape, - ShapePath, - ShapeUtils, - SRGBColorSpace, - Vector2, - Vector3, -} from "three" - -const COLOR_SPACE_SVG = SRGBColorSpace - -class SVGLoader extends Loader { - constructor(manager) { - super(manager) - - // Default dots per inch - this.defaultDPI = 90 - - // Accepted units: 'mm', 'cm', 'in', 'pt', 'pc', 'px' - this.defaultUnit = "px" - } - - load(url, onLoad, onProgress, onError) { - const scope = this - - const loader = new FileLoader(scope.manager) - loader.setPath(scope.path) - loader.setRequestHeader(scope.requestHeader) - loader.setWithCredentials(scope.withCredentials) - loader.load( - url, - function (text) { - try { - onLoad(scope.parse(text)) - } catch (e) { - if (onError) { - onError(e) - } else { - console.error(e) - } - - scope.manager.itemError(url) - } - }, - onProgress, - onError, - ) - } - - parse(text) { - const scope = this - - function parseNode(node, style) { - if (node.nodeType !== 1) return - - const transform = getNodeTransform(node) - - let isDefsNode = false - - let path = null - - switch (node.nodeName) { - case "svg": - style = parseStyle(node, style) - break - - case "style": - parseCSSStylesheet(node) - break - - case "g": - style = parseStyle(node, style) - break - - case "path": - style = parseStyle(node, style) - if (node.hasAttribute("d")) path = parsePathNode(node) - break - - case "rect": - style = parseStyle(node, style) - path = parseRectNode(node) - break - - case "polygon": - style = parseStyle(node, style) - path = parsePolygonNode(node) - break - - case "polyline": - style = parseStyle(node, style) - path = parsePolylineNode(node) - break - - case "circle": - style = parseStyle(node, style) - path = parseCircleNode(node) - break - - case "ellipse": - style = parseStyle(node, style) - path = parseEllipseNode(node) - break - - case "line": - style = parseStyle(node, style) - path = parseLineNode(node) - break - - case "defs": - isDefsNode = true - break - - case "use": - style = parseStyle(node, style) - - const href = node.getAttributeNS("http://www.w3.org/1999/xlink", "href") || "" - const usedNodeId = href.substring(1) - const usedNode = node.viewportElement.getElementById(usedNodeId) - if (usedNode) { - parseNode(usedNode, style) - } else { - console.warn( - "SVGLoader: 'use node' references non-existent node id: " + usedNodeId, - ) - } - - break - - default: - // console.log( node ); - } - - if (path) { - if (style.fill !== undefined && style.fill !== "none") { - path.color.setStyle(style.fill, COLOR_SPACE_SVG) - } - - transformPath(path, currentTransform) - - paths.push(path) - - path.userData = { node: node, style: style } - } - - const childNodes = node.childNodes - - for (let i = 0; i < childNodes.length; i++) { - const node = childNodes[i] - - if (isDefsNode && node.nodeName !== "style" && node.nodeName !== "defs") { - // Ignore everything in defs except CSS style definitions - // and nested defs, because it is OK by the standard to have - //