diff --git a/client/build.mjs b/client/build.mjs index 524b2e30..ea9132d6 100644 --- a/client/build.mjs +++ b/client/build.mjs @@ -2,6 +2,8 @@ import { fileURLToPath } from 'url'; import { dirname, resolve } from 'path'; import esbuild from 'esbuild'; +import { YAMLPlugin } from 'esbuild-yaml'; + const __filename = fileURLToPath( import.meta.url ); const __dirname = dirname( __filename ); @@ -12,6 +14,9 @@ esbuild bundle: true, minify: false, outdir: '../docs', + plugins: [ + YAMLPlugin() + ], target: 'es2018', alias: { '@': resolve( __dirname, 'src/app' ), diff --git a/client/dev.js b/client/dev.js index fdb5d187..62eca6aa 100644 --- a/client/dev.js +++ b/client/dev.js @@ -13,7 +13,7 @@ run('build:esbuild'); chokidar.watch(['src', '../game'], { }).on('all', (event, path) => { if (event === 'change') { console.log(event, path); - if (path.endsWith('.js') || path.endsWith('.ts')) { + if (path.endsWith('.js') || path.endsWith('.ts') || path.endsWith('.yml')) { run('build:esbuild'); } } diff --git a/client/package-lock.json b/client/package-lock.json index 796af0a8..0a04d75b 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -14,8 +14,9 @@ "cross-env": "^7.0.3", "detect-gpu": "^5.0.57", "esbuild": "0.24.0", + "esbuild-yaml": "^3.0.4", "npm-run-all": "^4.1.5", - "postprocessing": "^6.36.5", + "postprocessing": "^6.37.8", "pug": "^3.0.3", "pug-stylus": "^0.0.5", "stylus": "^0.64.0", @@ -1825,6 +1826,22 @@ "@esbuild/win32-x64": "0.24.0" } }, + "node_modules/esbuild-yaml": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/esbuild-yaml/-/esbuild-yaml-3.0.4.tgz", + "integrity": "sha512-PQFVI+CL7gTEmvJk5jjMcltqGEDPSQVrTM2wCz9HC44pdRBZWmNjkLR44N8IO8XZeoxoSk2/r7B2H1LMM3E8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.8.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "esbuild": ">=0.19.0" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -3673,13 +3690,13 @@ } }, "node_modules/postprocessing": { - "version": "6.36.5", - "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.36.5.tgz", - "integrity": "sha512-/KiVAZaEpmigRofZf2pXOTwtzli3GdzvvdrDjU8wU79ScXQfzwuLoPDglatNGsBctMuJ29SCEZBe0LUZBT3s+A==", + "version": "6.37.8", + "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.37.8.tgz", + "integrity": "sha512-qTFUKS51z/fuw2U+irz4/TiKJ/0oI70cNtvQG1WxlPKvBdJUfS1CcFswJd5ATY3slotWfvkDDZAsj1X0fU8BOQ==", "dev": true, "license": "Zlib", "peerDependencies": { - "three": ">= 0.157.0 < 0.172.0" + "three": ">= 0.157.0 < 0.181.0" } }, "node_modules/promise": { @@ -4976,6 +4993,19 @@ } } }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yuka": { "version": "0.7.8", "resolved": "https://registry.npmjs.org/yuka/-/yuka-0.7.8.tgz", diff --git a/client/package.json b/client/package.json index 18167d45..3c06eee9 100644 --- a/client/package.json +++ b/client/package.json @@ -26,8 +26,9 @@ "cross-env": "^7.0.3", "detect-gpu": "^5.0.57", "esbuild": "0.24.0", + "esbuild-yaml": "^3.0.4", "npm-run-all": "^4.1.5", - "postprocessing": "^6.36.5", + "postprocessing": "^6.37.8", "pug": "^3.0.3", "pug-stylus": "^0.0.5", "stylus": "^0.64.0", diff --git a/client/src/app/main.js b/client/src/app/main.js index 9c3053e1..7e0cc293 100644 --- a/client/src/app/main.js +++ b/client/src/app/main.js @@ -3,6 +3,7 @@ */ import l from '@/helpers/l.js'; import Config from "@/config.js"; +import Routes from "@/routes.js"; import Scenograph from "@/scenograph.js"; import UI from "@/ui.js"; @@ -12,7 +13,12 @@ import UI from "@/ui.js"; l.config = new Config(); /** - * Scenograph controls the current scene + * Routes activate game modes and screens. + */ +l.routes = new Routes(); + +/** + * Scenograph controls the current 3D scene */ l.scenograph = new Scenograph(); @@ -25,7 +31,7 @@ l.init = function () { /** * Load up the overworld by default. */ - l.current_scene = l.scenograph.load( "Overworld" ); + l.current_scene = l.scenograph.director.load( "Overworld" ); l.scenograph.init(); } diff --git a/client/src/app/routes.js b/client/src/app/routes.js new file mode 100644 index 00000000..05d35ce4 --- /dev/null +++ b/client/src/app/routes.js @@ -0,0 +1,28 @@ +/** + * @name Routes + * @description Provides an interface to load and unload game modes and screens. + * @namespace l.routes + * @memberof l + * @global + */ + +/** + * Internal libs and helpers. + */ +import l from '@/helpers/l.js'; + +import hangarRoute from '@/routes/hangar.js'; +import multiPlayerRoute from '@/routes/multiplayer.js'; +import singlePlayerRoute from '@/routes/singleplayer.js'; + +export default class routes { + + singlePlayer; + + constructor() { + this.hangar = hangarRoute; + this.multiPlayer = multiPlayerRoute; + this.singlePlayer = singlePlayerRoute; + } + +} diff --git a/client/src/app/routes/hangar.js b/client/src/app/routes/hangar.js new file mode 100644 index 00000000..9bf37e1f --- /dev/null +++ b/client/src/app/routes/hangar.js @@ -0,0 +1,66 @@ +/** + * @name Hangar scene + * @description Displays a player hangar + * @namespace l.routes.hangar + * @memberof l.routes + * @global + */ + +/** + * Internal libs and helpers. + */ +import l from '@/helpers/l.js'; + +export default class hangarRoute { + + // Scenograph instance of a structure that contains a hangar. + targetStructure = false; + + constructor() { + console.log( 'Hangar launched' ); + + // Start controls. + l.scenograph.controls.activate(); + + // Set client mode. + l.mode = 'hangar'; + + this.targetStructure = l.scenograph.objects.structures.platform.instances[0]; + + l.scenograph.actors.player = l.scenograph.actors.get('Player Two'); + l.scenograph.actors.player.setMode('person'); + + this.loadHangar(); + + } + + loadHangar() { + l.current_scene.scene.add(l.scenograph.objects.structures.hangar.mesh); + //this.targetStructure.visible = false; + l.scenograph.objects.structures.hangar.mesh.visible = true; + console.log(this.targetStructure.userData.config.hangars[0].position); + l.scenograph.objects.structures.hangar.mesh.position.x = this.targetStructure.userData.config.hangars[0].position.x; + l.scenograph.objects.structures.hangar.mesh.position.y = this.targetStructure.userData.config.hangars[0].position.y; + l.scenograph.objects.structures.hangar.mesh.position.z = this.targetStructure.userData.config.hangars[0].position.z; + + l.scenograph.actors.player.vehicle.mesh.userData.object.position.x = this.targetStructure.userData.config.hangars[0].position.x; + l.scenograph.actors.player.vehicle.mesh.userData.object.position.z = - 2.5 + this.targetStructure.userData.config.hangars[0].position.z; + l.scenograph.actors.player.vehicle.mesh.userData.object.position.y = l.scenograph.objects.structures.hangar.mesh.position.y - 7.5; + + l.scenograph.actors.player.actorInstance.object.position.x = this.targetStructure.userData.config.hangars[0].position.x; + l.scenograph.actors.player.actorInstance.object.position.z = 10 + this.targetStructure.userData.config.hangars[0].position.z; + l.scenograph.actors.player.actorInstance.object.position.y = l.scenograph.objects.structures.hangar.mesh.position.y - 2.5 + ; + + l.scenograph.cameras.active.position.copy(l.scenograph.actors.player.actorInstance.object.position); + + if ( l.scenograph.controls.orbit ) { + l.scenograph.cameras.orbit.updateProjectionMatrix(); + l.scenograph.controls.orbit.update(); + l.scenograph.controls.orbitTarget.x = l.scenograph.objects.structures.hangar.mesh.position.x; + l.scenograph.controls.orbitTarget.y = l.scenograph.objects.structures.hangar.mesh.position.y; + l.scenograph.controls.orbitTarget.z = l.scenograph.objects.structures.hangar.mesh.position.z; + } + } + +} diff --git a/client/src/app/routes/multiplayer.js b/client/src/app/routes/multiplayer.js new file mode 100644 index 00000000..e0e76d27 --- /dev/null +++ b/client/src/app/routes/multiplayer.js @@ -0,0 +1,34 @@ +/** + * @name Multi Player + * @description Managees the game client's multi player mode. + * @namespace l.routes.multiplayer + * @memberof l.routes + * @global + */ + +/** + * Internal libs and helpers. + */ +import l from '@/helpers/l.js'; + +export default class multiPlayerRoute { + + constructor() { + console.log( 'Multi player launched' ); + + // Start controls. + l.scenograph.controls.activate(); + + // Start overlays. + l.scenograph.overlays.activate(); + + let serverLocation = l.env == 'Dev' ? 'lcl.langenium.com:8090' : 'test.langenium.com:42069'; + + l.scenograph.modes.multiplayer.connect( '//' + serverLocation ); + + // Set client mode. + l.mode = 'multi_player'; + + } + +} diff --git a/client/src/app/routes/singleplayer.js b/client/src/app/routes/singleplayer.js new file mode 100644 index 00000000..ef3e383f --- /dev/null +++ b/client/src/app/routes/singleplayer.js @@ -0,0 +1,32 @@ +/** + * @name Single Player + * @description Managees the game client's single player mode. + * @namespace l.routes.singleplayer + * @memberof l.routes + * @global + */ + +/** + * Internal libs and helpers. + */ +import l from '@/helpers/l.js'; + +export default class singlePlayerRoute { + + constructor() { + console.log( 'Single player launched' ); + + // Start controls. + l.scenograph.controls.activate(); + + // Start overlays. + l.scenograph.overlays.activate(); + + // Set client mode. + l.mode = 'single_player'; + + l.scenograph.actors.map.get('Player One').setMode('vehicle'); + + } + +} diff --git a/client/src/app/scenograph.js b/client/src/app/scenograph.js index a7090dd1..dd4fe024 100644 --- a/client/src/app/scenograph.js +++ b/client/src/app/scenograph.js @@ -20,22 +20,21 @@ import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; import l from "@/helpers/l.js"; import { calculateAdjustedGapSize } from '@/helpers/math.js'; +import Actors from "@/scenograph/actors.js"; import Cameras from "@/scenograph/cameras.js"; import Controls from "@/scenograph/controls.js"; +import Director from "@/scenograph/director.js"; import Effects from "@/scenograph/effects"; import Events from "./scenograph/events"; import Materials from "@/scenograph/materials.js"; +import Objects from "@/scenograph/objects"; import Overlays from "@/scenograph/overlays.js"; + import Debugging from '@/scenograph/modes/debugging.js'; import Fast from '@/scenograph/modes/fast.js'; import Multiplayer from "@/scenograph/modes/multiplayer.js"; -/** - * Scenes - */ -import Overworld from '@/scenograph/scenes/overworld.js'; - /** * Scene controllers */ @@ -46,6 +45,8 @@ import { export default class Scenograph { + actors; + cameras; /** @@ -59,8 +60,12 @@ export default class Scenograph { modes; + objects; + overlays; + sceneManager; + /** * @instance YUKA.EntityManager; */ @@ -79,6 +84,11 @@ export default class Scenograph { this.modes = {}; + /** + * Cameras. + */ + this.actors = new Actors(); + /** * Cameras. */ @@ -104,11 +114,21 @@ export default class Scenograph { */ this.materials = new Materials(); + /** + * Objects. + */ + this.objects = new Objects(); + /** * Overlays. */ this.overlays = new Overlays(); + /** + * Scene Manager. + */ + this.director = new Director(); + /** * Setup the different game modes (controllers) */ @@ -136,14 +156,6 @@ export default class Scenograph { } - load( sceneName ) { - let scene = false; - - if ( sceneName == 'Overworld' ) { - scene = new Overworld(); - } - return scene; - } /** * Game 3D initialiser, called by l when it's finished loading. @@ -174,6 +186,9 @@ export default class Scenograph { // Reusable raycaster for tracking what the user tried to hit. l.current_scene.raycaster = new THREE.Raycaster(); + // Load all object classes. + await this.objects.init(); + // Scene Setup. l.current_scene.setup(); @@ -222,6 +237,7 @@ export default class Scenograph { const delta = l.scenograph.time.update().getDelta(); if ( l.current_scene.started ) { + if ( l.current_scene.animation_queue.length > 0 ) { for ( var i = 0; diff --git a/client/src/app/scenograph/actors.js b/client/src/app/scenograph/actors.js new file mode 100644 index 00000000..20bca219 --- /dev/null +++ b/client/src/app/scenograph/actors.js @@ -0,0 +1,42 @@ +/** + * @name Actors + * @description Provides an interface to manage actors. + * @namespace l.scenograph.actors + * @memberof l.scenograph + * @global + * + * @todo: #31 consider removal now that world instance manages and updates actors. + */ + +/** + * Internal libs and helpers. + */ +import l from '@/helpers/l.js'; +import Player from '@/scenograph/actors/player.js'; + +export default class Actors { + + map; + player; + + constructor() { + this.map = new Map(); + } + + async registerActor(actorInstance) { + if ( actorInstance.config.class == 'player' ) { + let player = new Player( actorInstance ); + await player.load(); + this.map.set(actorInstance.config.name, player); + } + } + + get(name) { + return this.map.get(name); + } + + getAll() { + return this.map.values(); + } + +} diff --git a/client/src/app/scenograph/actors/player.js b/client/src/app/scenograph/actors/player.js new file mode 100644 index 00000000..f3a8e113 --- /dev/null +++ b/client/src/app/scenograph/actors/player.js @@ -0,0 +1,76 @@ +/** + * @name Player + * @description Provides an interface to the player actor in the game. + * @memberof l.scenograph.actors + * @global + */ + +/** + * Vendor libs and base class. + */ +import * as THREE from "three"; + +/** + * Internal libs and helpers. + */ +import l from '@/helpers/l.js'; + +export default class Player { + + // Whether the player is in their vehicle or not. + mode; + + ready; + + // Character model. + person; + + // Vehicle model. + vehicle; + + constructor( actorInstance ) { + + this.actorInstance = actorInstance; + + this.ready = false; + + this.person = false; + + this.vehicle = false; + + this.setMode(); + } + + setMode( mode = 'vehicle' ) { + if ( mode === 'vehicle' ) { + this.mode = 'vehicle'; + } + else { + this.mode = 'person'; + } + } + + async load() { + + // Setup aircraft, used for the intro sequence. + this.vehicle = new l.scenograph.objects.vehicles.valiant(); + await this.vehicle.load(); + l.current_scene.scene.add( + this.vehicle.mesh + ); + l.current_scene.animation_queue.push( + delta => this.vehicle.animate(delta) + ); + + // Setup person, used for the hangar scene. + this.person = new l.scenograph.objects.vehicles.person(this.actorInstance); + l.current_scene.scene.add( + this.person.mesh + ); + l.current_scene.animation_queue.push( + delta => this.person.animate(delta) + ); + + } + +} diff --git a/client/src/app/scenograph/cameras.js b/client/src/app/scenograph/cameras.js index 75230374..37914f8f 100644 --- a/client/src/app/scenograph/cameras.js +++ b/client/src/app/scenograph/cameras.js @@ -99,60 +99,4 @@ export default class Cameras { return camera; } - updatePlayer( rY, tY, tZ ) { - var radian = ( Math.PI / 180 ); - - l.current_scene.objects.player.camera_distance = l.current_scene.objects.player.default_camera_distance + ( l.current_scene.room_depth / 2 ); - if ( l.current_scene.objects.player.airSpeed < 0 ) { - l.current_scene.objects.player.camera_distance -= l.current_scene.objects.player.airSpeed * 4; - } - - let xDiff = l.current_scene.objects.player.mesh.position.x; - let zDiff = l.current_scene.objects.player.mesh.position.z; - - l.scenograph.cameras.player.position.x = xDiff + l.current_scene.objects.player.camera_distance * Math.sin( l.current_scene.objects.player.mesh.rotation.y ); - l.scenograph.cameras.player.position.z = zDiff + l.current_scene.objects.player.camera_distance * Math.cos( l.current_scene.objects.player.mesh.rotation.y ); - - if ( rY != 0 ) { - - l.scenograph.cameras.player.rotation.y += rY; - } - else { - // Check there is y difference and the rotation pad isn't being pressed. - if ( - l.scenograph.cameras.player.rotation.y != l.current_scene.objects.player.mesh.rotation.y && - ( l.scenograph.controls.touch && !l.scenograph.controls.touch.controls.rotationPad.mouseDown ) - ) { - - // Get the difference in y rotation betwen the camera and ship - let yDiff = l.current_scene.objects.player.mesh.rotation.y - l.scenograph.cameras.player.rotation.y; - - // Check the y difference is larger than 1/100th of a radian - if ( - Math.abs( yDiff ) > radian / 100 - ) { - // Add 1/60th of the difference in rotation, as FPS currently capped to 60. - l.scenograph.cameras.player.rotation.y += ( l.current_scene.objects.player.mesh.rotation.y - l.scenograph.cameras.player.rotation.y ) * 1 / 60; - } - else { - l.scenograph.cameras.player.rotation.y = l.current_scene.objects.player.mesh.rotation.y; - } - - } - - } - - let xDiff2 = tZ * Math.sin( l.current_scene.objects.player.mesh.rotation.y ), - zDiff2 = tZ * Math.cos( l.current_scene.objects.player.mesh.rotation.y ); - - if ( l.current_scene.objects.player.mesh.position.y + tY >= 1 ) { - l.scenograph.cameras.player.position.y += tY; - } - - l.scenograph.cameras.player.position.x += xDiff2; - l.scenograph.cameras.player.position.z += zDiff2; - - l.scenograph.cameras.player.updateProjectionMatrix(); - } - } diff --git a/client/src/app/scenograph/controls.js b/client/src/app/scenograph/controls.js index b6d1f26c..b25b1a77 100644 --- a/client/src/app/scenograph/controls.js +++ b/client/src/app/scenograph/controls.js @@ -137,7 +137,7 @@ export default class Controls { this.orbit = false; // Reset camera y position after disengaging orbit controls. - // l.scenograph.cameras.player.position.copy( l.current_scene.objects.player.mesh.position ); + // l.scenograph.cameras.player.position.copy( l.scenograph.actors.player.vehicle.mesh.position ); // l.scenograph.cameras.player.position.y += 10.775 / 4; } } diff --git a/client/src/app/scenograph/controls/touch/weapons.js b/client/src/app/scenograph/controls/touch/weapons.js index 8701765e..3147dc6c 100644 --- a/client/src/app/scenograph/controls/touch/weapons.js +++ b/client/src/app/scenograph/controls/touch/weapons.js @@ -67,9 +67,9 @@ export default class WeaponControls { let timeRemaining = 0; - if ( parseInt(l.current_scene.stats.currentTime) < parseInt(l.current_scene.objects.player.mesh.userData.actor.weapons.last) + parseInt(l.current_scene.objects.player.mesh.userData.actor.weapons.timeout) ) { - timeRemaining = parseInt(l.current_scene.objects.player.mesh.userData.actor.weapons.timeout) - ( - parseInt(l.current_scene.stats.currentTime) - parseInt(l.current_scene.objects.player.mesh.userData.actor.weapons.last) + if ( parseInt(l.current_scene.stats.currentTime) < parseInt(l.scenograph.actors.player.vehicle.mesh.userData.actor.weapons.last) + parseInt(l.scenograph.actors.player.vehicle.mesh.userData.actor.weapons.timeout) ) { + timeRemaining = parseInt(l.scenograph.actors.player.vehicle.mesh.userData.actor.weapons.timeout) - ( + parseInt(l.current_scene.stats.currentTime) - parseInt(l.scenograph.actors.player.vehicle.mesh.userData.actor.weapons.last) ); } diff --git a/client/src/app/scenograph/director.js b/client/src/app/scenograph/director.js new file mode 100644 index 00000000..73a01759 --- /dev/null +++ b/client/src/app/scenograph/director.js @@ -0,0 +1,429 @@ +/** + * Director. + * + * Scene Management class. + */ + +/** + * Vendor libs and base class. + */ +import * as THREE from "three"; + +/** + * Internal libs and helpers + */ +import l from '@/helpers/l.js'; + +/** + * World Simulation + */ +import World from '#/game/src/world'; + +/** + * Scene controllers + */ +import { setupTriggers, updateTriggers } from "@/scenograph/triggers"; +import { + setupTweens, + updateTweens, + startTweening, +} from "@/scenograph/tweens"; + + + +export default class Director { + constructor() { + + /** + * Animation queue. + */ + this.animation_queue = []; + + /** + * Primary scene camera + * + * @memberof THREE.Camera + */ + this.camera = false; + + /** + * Effects composers and their layers. + * + * @memberof Object { postprocessing.EffectComposer } + */ + this.effects = { + particles: false, + postprocessing: false + }; + + /** + * Fast mode (bloom off, no shadows) + * + * @memberof Boolean + */ + this.fast = true; + + + /** + * Game world simulation. + */ + this.world = false; + + /** + * Reusable loaders for assets. + */ + this.loaders = { + gltf: false, + object: false, + texture: false, + stats: { + fonts: { + target: 0, // @todo: Check if this affects double loads, shouldn't with caching. + loaded: 0 + }, + gtlf: { + target: 0, // @todo: Check if this affects double loads, shouldn't with caching. + loaded: 0 + }, + screens: { + target: 0, + loaded: 0 + }, + svg: { + target: 1, + loaded: 0 + }, + textures: { + target: 9, + loaded: 0 + } + }, + + /** + * UI controller + */ + ui: false + }; + + /** + * Reusable materials for assets + */ + this.materials = false; + + /** + * Camera is being moved by tweening. + * + * @memberof Boolean + */ + this.moving = false; + + /** + * Current position of the users pointer. + * + * @memberof THREE.Vector2 + */ + this.pointer = false; + + /** + * Raycaster that projects into the scene from the users pointer and picks up collisions for interaction. + * + * @memberof THREE.Raycaster + */ + this.raycaster = false; + + /** + * Ready to begin. + */ + + this.ready = false; + + /** + * Renderers that create the scene. + * + * @memberof Object { THREE.Renderer , ... } + */ + this.renderers = { + webgl: false + }; + + /** + * Settings that controls the scene. + * + * @memberof Object + */ + this.settings = { + adjusted_gap: false, // calculated value + game_controls: false, // show in-game control overlays. + gap: 1.3, // depth(z axis) gap between desks + light: { + fast: { + desk: { + normal: 0.015, active: 0.05 + }, + neonSign: { + normal: 0.35, active: 0.05 + } + }, + highP: { + desk: { + normal: 0.015, active: 0.035 + }, + neonSign: { + normal: 0.1, active: 0.05 + } + } + + }, + room_depth: false, // calculated value + scale: 11, // do not change, braeks css screen sizes + startPosZ: - 10 // updated responsive eugene levy + }; + + + /** + * Currently selected object. + * + * @memberof THREE.Object3d + */ + this.selected = false; + + /** + * Skip the intro sequence for this scene. + */ + this.skipintro = false; + + /** + * If the main sequence has begun. + * + * @memberof Boolean + */ + this.started = false; + + /** + * Custom array of game performance stats. + */ + this.stats = { + /** + * Frames Per Second (FPS) + * + * @memberof Integer + */ + currentTime: performance.now(), + fps: 0, + frameCount: 0, + lastTime: performance.now(), + }; + + /** + * All scene triggers. + * + * @memberof Object + */ + this.triggers = {}; + + /** + * All scene tweens. + * + * @memberof Object + */ + this.tweens = {}; + } + + // Load world instance from game classes. + load( sceneName ) { + this.world = new World( sceneName ); + + return this; + } + + // Load the objects in world instance to the current scene. + async setup() { + this.setupSceneDefaults(); + + this.loadInstance(); + + this.finishSetup(); + } + + async loadInstance() { + + await this.world.config.objects.forEach( async object_config => { + if ( object_config.model == 'extractor' ) { + l.scenograph.director.loadObject( + object_config, + await l.scenograph.objects.structures.extractor.get() + ); + } + if ( object_config.model == 'platform' ) { + l.scenograph.director.loadObject( + object_config, + await l.scenograph.objects.structures.platform.get() + ); + } + if ( object_config.model == 'refinery' ) { + l.scenograph.director.loadObject( + object_config, + await l.scenograph.objects.structures.refinery.get() + ); + } + } ); + + this.world.config.actors.forEach( async object_config => { + if ( object_config.class == 'cargoShip' ) { + l.scenograph.director.loadObject( + object_config, + await l.scenograph.objects.vehicles.cargoShip.get() + ); + } + if ( object_config.class == 'pirate' ) { + l.scenograph.director.loadObject( + object_config, + await l.scenograph.objects.vehicles.raven.get() + ); + } + + } ); + + this.world.instance.actors.forEach(async actorInstance => { + if ( actorInstance.config.class == 'player' ) { + await l.scenograph.actors.registerActor( actorInstance ); + } + }); + + } + + async loadObject( config, object ) { + object.position.x = config.position.x; + object.position.y = config.position.y; + object.position.z = config.position.z; + + if ( config.rotation ) { + object.rotation.x = config.rotation.x; + object.rotation.y = config.rotation.y; + object.rotation.z = config.rotation.z; + } + + object.name = config.name; + object.userData.config = config; + + // @todo: add to current_scene array relevant to object class. + + l.current_scene.scene.add( + object + ); + } + + /** + * @todo: Make this dynamic and not hard codo + */ + async temp_addPlayer() { + + } + + async setupSceneDefaults() { + + /** + * Tracked meshes and mesh groups that compose the scene. + * + * @memberof Object + */ + l.current_scene.objects = {}; + + /** + * The main scene container. + * + * @memberof THREE.Scene + */ + l.current_scene.scene = new THREE.Scene(); + l.current_scene.scene.visible = false; + + l.scenograph.effects.init(); + + l.current_scene.objects.demoShip = new l.scenograph.objects.vehicles.valiant(); + await l.current_scene.objects.demoShip.load(); + l.current_scene.scene.add( + l.current_scene.objects.demoShip.mesh + ); + l.current_scene.animation_queue.push( + delta => l.current_scene.objects.demoShip.animate(delta) + ); + l.current_scene.tweens.shipEnterY = l.current_scene.objects.demoShip.shipEnterY(); + l.current_scene.tweens.shipEnterZ = l.current_scene.objects.demoShip.shipEnterZ(); + + l.current_scene.objects.door = await l.scenograph.objects.preloader.createDoor(); + l.current_scene.objects.door.position.set( + -l.scenograph.objects.preloader.doorWidth / 2, + -5 + l.scenograph.objects.preloader.doorHeight / 2, + -15 + l.current_scene.room_depth / 2 + ); + l.current_scene.scene.add( l.current_scene.objects.door ); + + // Setup skybox + l.current_scene.objects.sky = new l.scenograph.objects.environment.sky(); + l.current_scene.scene.add( + l.current_scene.objects.sky.mesh + ); + l.current_scene.animation_queue.push( + l.current_scene.objects.sky.animate + ); + + // Setup ocean + //l.current_scene.objects.ocean = new Ocean( extractors.extractorLocations ); + l.current_scene.objects.ocean = new l.scenograph.objects.environment.ocean( + l.scenograph.objects.structures.extractor.extractorLocations + ); + l.current_scene.scene.add( + l.current_scene.objects.ocean.water + ); + l.current_scene.animation_queue.push( + l.current_scene.objects.ocean.animate + ); + + // Adjust ambient light intensity + l.current_scene.objects.ambientLight = new THREE.AmbientLight( + l.config.settings.fast ? 0x555555 : 0x444444 + ); // Dim ambient light color + l.current_scene.objects.ambientLight.name = 'Main Light'; + l.current_scene.objects.ambientLight.intensity = Math.PI; + l.current_scene.scene.add( + l.current_scene.objects.ambientLight + ); + + l.current_scene.objects.screens_loaded = 0; + l.current_scene.objects.room = await l.scenograph.objects.preloader.createOfficeRoom(); + l.current_scene.scene.add( l.current_scene.objects.room ); + + + // Setup triggers + setupTriggers(); + + // Setup Tweens. + setupTweens(); + + + } + + finishSetup() { + // Add objects to animation queue. + l.current_scene.animation_queue.push( + l.scenograph.objects.animate + ); + // Check if we've finished loading. + let bootWaiter = setInterval( () => { + if ( + // Check door sign is loaded up. + l.current_scene.objects.door_sign + ) { + l.current_scene.ready = true; + clearTimeout( bootWaiter ); + + // Start tweens. + startTweening(); + + requestAnimationFrame( l.scenograph.animate ); + } + }, 100 ); + } + + +} diff --git a/client/src/app/scenograph/modes/multiplayer.js b/client/src/app/scenograph/modes/multiplayer.js index 0adacb9b..6706ed1f 100644 --- a/client/src/app/scenograph/modes/multiplayer.js +++ b/client/src/app/scenograph/modes/multiplayer.js @@ -94,7 +94,7 @@ export default class Multiplayer { // l.current_scene.animation_queue.push( // newPlayer.animate // ); - l.current_scene.objects.players.push( newPlayer ); + l.scenograph.actors.player.vehicles.push( newPlayer ); } @@ -102,7 +102,7 @@ export default class Multiplayer { * Remove a disconnected remote player from the client session. */ async remove_player( data ) { - l.current_scene.objects.players.forEach( ( ship ) => { + l.scenograph.actors.player.vehicles.forEach( ( ship ) => { if ( ship.socket_id == data.socket_id ) { l.current_scene.scene.remove( ship.mesh ); } @@ -115,18 +115,18 @@ export default class Multiplayer { move_ship( data ) { if ( data.socket_id == l.scenograph.modes.multiplayer.socket.id ) { // Update stored ship state, don't punch out as functions aren't transmitted. - l.current_scene.objects.player.airSpeed = data.airSpeed; - l.current_scene.objects.player.altitude = data.altitude; - l.current_scene.objects.player.heading = data.heading; - l.current_scene.objects.player.horizon = data.horizon; - l.current_scene.objects.player.position.x = data.position.x; - l.current_scene.objects.player.position.y = data.position.y; - l.current_scene.objects.player.position.z = data.position.z; - l.current_scene.objects.player.rotation = data.rotation; - l.current_scene.objects.player.verticalSpeed = data.verticalSpeed; + l.scenograph.actors.player.vehicle.mesh.userData.object.airSpeed = data.airSpeed; + l.scenograph.actors.player.vehicle.mesh.userData.object.altitude = data.altitude; + l.scenograph.actors.player.vehicle.heading = data.heading; + l.scenograph.actors.player.vehicle.horizon = data.horizon; + l.scenograph.actors.player.vehicle.position.x = data.position.x; + l.scenograph.actors.player.vehicle.position.y = data.position.y; + l.scenograph.actors.player.vehicle.position.z = data.position.z; + l.scenograph.actors.player.vehicle.rotation = data.rotation; + l.scenograph.actors.player.vehicle.mesh.userData.object.verticalSpeed = data.verticalSpeed; } else { - l.current_scene.objects.players.forEach( ( ship ) => { + l.scenograph.actors.player.vehicles.forEach( ( ship ) => { if ( ship.socket_id == data.socket_id ) { // Update stored ship state, don't punch out as functions aren't transmitted. diff --git a/client/src/app/scenograph/objects.js b/client/src/app/scenograph/objects.js new file mode 100644 index 00000000..2e462af5 --- /dev/null +++ b/client/src/app/scenograph/objects.js @@ -0,0 +1,118 @@ +/** + * Objects. + * + * Composes scene meshes from gltf and procedural code. + */ + +/** + * Vendor libs and base class. + */ +import * as THREE from "three"; + +/** + * Internal libs and helpers + */ +import l from '@/helpers/l.js'; + + +/** + * Objects + */ + +// Environment +import Ocean from "@/scenograph/objects/environment/ocean"; +import Sky from "@/scenograph/objects/environment/sky"; +import Sky2 from "@/scenograph/objects/environment/sky2"; + +/** + * Preloader objects + */ +import { createDoor, createOfficeRoom, doorHeight, doorWidth } from "@/scenograph/objects/structures/office_room"; + +// Projectiles +import Missile from "@/scenograph/objects/projectiles/missile"; + +// Structures +import Extractor from "@/scenograph/objects/structures/extractor"; +import Hangar from "@/scenograph/objects/structures/hangar"; +import Platform from "@/scenograph/objects/structures/platform"; +import Refinery from "@/scenograph/objects/structures/refinery"; + +// Vehicles +import CargoShip from "@/scenograph/objects/vehicles/cargo_ship"; +import Person from "@/scenograph/objects/vehicles/person"; +import Raven from "@/scenograph/objects/vehicles/raven"; +import Valiant from "@/scenograph/objects/vehicles/valiant"; + +export default class Objects { + + environment = false; + preloader = false; + projectiles = false; + structures = false; + vehicles = false; + + constructor() { + this.environment = { + ocean: Ocean, + sky: Sky, + }; + this.preloader = { + createDoor: createDoor, + createOfficeRoom: createOfficeRoom, + doorHeight: doorHeight, + doorWidth: doorWidth, + }; + this.projectiles = { + missile: new Missile() + }; + this.structures = { + extractor: new Extractor(), + hangar: new Hangar(), + platform: new Platform(), + refinery: new Refinery(), + }; + this.vehicles = { + cargoShip: new CargoShip(), + person: Person, + raven: new Raven(), + valiant: Valiant, + }; + + } + + /** + * @todo: Conditionally switching off loading some objects on scenes where not needed. + */ + async init () { + await this.structures.extractor.load(); + await this.structures.hangar.load(); + await this.structures.platform.load(); + await this.structures.refinery.load(); + await this.projectiles.missile.load(); + await this.vehicles.cargoShip.load(); + await this.vehicles.raven.load(); + console.log("Objects loaded"); + } + + /** + * Animate hook. + * + * This method is called within the main animation loop and + * therefore must only reference global objects or properties. + * + * @method animate + * @memberof Objects + * @global + * @note All references within this method should be globally accessible. + **/ + animate( currentTime ) { + l.scenograph.objects.structures.extractor.animate( currentTime ); + l.scenograph.objects.structures.platform.animate( currentTime ); + l.scenograph.objects.structures.refinery.animate( currentTime ); + l.scenograph.objects.projectiles.missile.animate( currentTime ); + l.scenograph.objects.vehicles.cargoShip.animate( currentTime ); + l.scenograph.objects.vehicles.raven.animate( currentTime ); + } + +} diff --git a/client/src/app/scenograph/objects/projectiles/missile.js b/client/src/app/scenograph/objects/projectiles/missile.js index 03062430..2cb9ae0b 100644 --- a/client/src/app/scenograph/objects/projectiles/missile.js +++ b/client/src/app/scenograph/objects/projectiles/missile.js @@ -74,10 +74,10 @@ export default class Missile { async targetLost( targetMeshUuid ) { - l.current_scene.objects.projectiles.missile.active.forEach( ( missile, index ) => { + l.scenograph.objects.projectiles.missile.active.forEach( ( missile, index ) => { // Detonate early if missile target was lost. if ( missile.userData.destMesh.uuid == targetMeshUuid ) { - l.current_scene.objects.projectiles.missile.animateMissileEnd( missile, index ); + l.scenograph.objects.projectiles.missile.animateMissileEnd( missile, index ); } } ); } @@ -159,7 +159,7 @@ export default class Missile { l.current_scene.scene.add( mesh ); - l.current_scene.objects.projectiles.missile.explosions.push( mesh ); + l.scenograph.objects.projectiles.missile.explosions.push( mesh ); } /** @@ -171,7 +171,7 @@ export default class Missile { * @param {*} destCoords Destination coordinates for flight path */ async fireMissile( originMesh, originCoords, destMesh, destCoords ) { - let newMissile = l.current_scene.objects.projectiles.missile.mesh.clone(); + let newMissile = l.scenograph.objects.projectiles.missile.mesh.clone(); // Attach metas. newMissile.userData.created = l.current_scene.stats.currentTime; @@ -192,7 +192,7 @@ export default class Missile { l.current_scene.scene.add(newMissile); // Add missile to the active missiles array (for animation etc) - l.current_scene.objects.projectiles.missile.active.push( newMissile ); + l.scenograph.objects.projectiles.missile.active.push( newMissile ); // Add a trail to the missile. newMissile.userData.trail = l.current_scene.effects.trail.createTrail( newMissile, 0, 0, -1.5 ); @@ -233,7 +233,7 @@ export default class Missile { const destroyedTargets = new Set(); // @todo: v7: This has to be simulated on the server somehow.. - l.current_scene.objects.projectiles.missile.active.forEach( ( missile, index ) => { + l.scenograph.objects.projectiles.missile.active.forEach( ( missile, index ) => { const [ damage, targetDestroyed ] = missile.userData.object.hitCalculation(); if ( targetDestroyed ) { @@ -247,22 +247,22 @@ export default class Missile { ( parseFloat( l.current_scene.stats.currentTime ) >= parseFloat( missile.userData.created ) + 10000 ) || ( damage ) ) { - l.current_scene.objects.projectiles.missile.animateMissileEnd( missile, index ); + l.scenograph.objects.projectiles.missile.animateMissileEnd( missile, index ); } // Otherwise keep flying forward. else { - l.current_scene.objects.projectiles.missile.animateMissileFlight( missile ); + l.scenograph.objects.projectiles.missile.animateMissileFlight( missile ); } } ); // Destroy all missiles headed toward the target. - l.current_scene.objects.projectiles.missile.active = l.current_scene.objects.projectiles.missile.active.filter( missile => { + l.scenograph.objects.projectiles.missile.active = l.scenograph.objects.projectiles.missile.active.filter( missile => { const remove = destroyedTargets.has( missile.userData.destMesh.uuid ); - if (remove) l.current_scene.objects.projectiles.missile.targetLost( missile.userData.destMesh.uuid ); + if (remove) l.scenograph.objects.projectiles.missile.targetLost( missile.userData.destMesh.uuid ); return !remove; }); - l.current_scene.objects.projectiles.missile.explosions.forEach( ( explosion, index ) => { + l.scenograph.objects.projectiles.missile.explosions.forEach( ( explosion, index ) => { // Check if it's been 2 seconds since the explosion started, remove if so if ( parseFloat( l.current_scene.stats.currentTime ) >= parseFloat( explosion.userData.created ) + 2000 ) { // Remove the explosion from the scene. @@ -284,7 +284,7 @@ export default class Missile { explosion = null; // Remove from the tracking array. - l.current_scene.objects.projectiles.missile.explosions.splice( index, 1 ); + l.scenograph.objects.projectiles.missile.explosions.splice( index, 1 ); } else { explosion.lookAt( l.scenograph.cameras.active.position ); @@ -310,11 +310,11 @@ export default class Missile { } animateMissileEnd( missile, activeIndex ) { - l.current_scene.objects.projectiles.missile.active.splice( activeIndex, 1 ); + l.scenograph.objects.projectiles.missile.active.splice( activeIndex, 1 ); l.current_scene.scene.remove( missile ); missile.userData.trail.destroyMesh(); missile.userData.trail.deactivate(); - l.current_scene.objects.projectiles.missile.loadExplosion( missile.userData.destMesh.position ); + l.scenograph.objects.projectiles.missile.loadExplosion( missile.userData.destMesh.position ); } } diff --git a/client/src/app/scenograph/objects/structures/extractors.js b/client/src/app/scenograph/objects/structures/extractor.js similarity index 91% rename from client/src/app/scenograph/objects/structures/extractors.js rename to client/src/app/scenograph/objects/structures/extractor.js index 8fb0695c..ebe3d070 100644 --- a/client/src/app/scenograph/objects/structures/extractors.js +++ b/client/src/app/scenograph/objects/structures/extractor.js @@ -16,6 +16,10 @@ import { proceduralBuilding, proceduralMetalMaterial2 } from '@/scenograph/mater export default class Extractor { + instances; + + locations; + // Array of three.vector3's defining X/Z coordinates and radius of where extractors are in the ocean. extractorLocations; @@ -26,10 +30,12 @@ export default class Extractor { size; constructor() { + this.instances = []; + this.locations = []; this.ready = false; this.size = 150; - // Setup extractors + // // Setup extractors this.extractorLocations = [ //new THREE.Vector3( 0, 0, this.size * 10 ), // Test extractor. new THREE.Vector3( 0, -70000, this.size * 10 ), @@ -39,27 +45,6 @@ export default class Extractor { ]; } - async getAll() { - let extractors = []; - - await this.load(); - - this.extractorLocations.forEach( async ( extractor_location, i ) => { - let extractor = this.mesh.clone(); - - extractor.rotation.y = Math.PI / 8; - extractor.position.x = extractor_location.x; - extractor.position.y = -7450; - extractor.position.z = extractor_location.y; - - extractor.name = 'Extractor #' + ( i + 1 ); - - extractors.push( extractor ); - } ); - - return extractors; - } - async load() { //const material = new THREE.MeshBasicMaterial( {color: 0xff0000, transparent: true, opacity: 1.0, side: THREE.DoubleSide} ); @@ -128,6 +113,20 @@ export default class Extractor { } + async get() { + let extractor = this.mesh.clone(); + + extractor.rotation.y = Math.PI / 8; + + let i = this.instances.length; + + extractor.name = 'Extractor #' + ( i + 1 ); + + this.instances.push( extractor ); + + return extractor; + } + getPhatTank() { const geometry = new THREE.SphereGeometry( 3, 32, 16 ); // const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } ); @@ -199,7 +198,7 @@ export default class Extractor { **/ animate( currentTime ) { - l.current_scene.objects.extractors.forEach( ( extractor, i ) => { + l.scenograph.objects.structures.extractor.instances.forEach( ( extractor, i ) => { let inner = extractor.getObjectByName( 'inner' ); let outer = extractor.getObjectByName( 'outer' ); inner.material.uniforms.time.value += 0.0000025; diff --git a/client/src/app/scenograph/objects/structures/hangar.js b/client/src/app/scenograph/objects/structures/hangar.js new file mode 100644 index 00000000..82a698ce --- /dev/null +++ b/client/src/app/scenograph/objects/structures/hangar.js @@ -0,0 +1,206 @@ +/** + * Hangar (Player quarters) + */ + +/** + * Vendor libs + */ +import * as THREE from 'three'; +import { ADDITION, HOLLOW_SUBTRACTION, Operation, Evaluator } from 'three-bvh-csg'; + +/** + * Internal libs and helpers. + */ +import l from '@/helpers/l.js'; +import { proceduralBuilding, proceduralMetalMaterial2 } from '@/scenograph/materials.js'; + +import HangarObject from '#/game/src/objects/structures/hangar'; + +export default class Hangar { + + // Modified materials for indoor scenes + materials; + + // Game object containing layout definition and collision detection handling. + objectClass; + + // THREE.Mesh + mesh; + + // The scale of the mesh. + size; + + constructor() { + + this.size = 5; + + this.materials = {}; + + this.ready = false; + + this.objectClass = new HangarObject(); + + } + + /** + * Load hangar mesh + * @returns + */ + async loadMesh( ) { + let hangarConfig, corridorConfig, quartersConfig; + + this.objectClass.design.components.forEach(component => { + if ( component.name === 'Main Bay' ) { + hangarConfig = component; + } + if ( component.name === 'Quarters' ) { + quartersConfig = component; + } + if ( component.name === 'Corridor' ) { + corridorConfig = component; + } + }); + + /** + * Main hangar mesh + */ + let hangarMesh = await this.loadHangarMesh( hangarConfig ); + + /** + * Corridor mesh + */ + let corridorMesh = await this.loadCorridorMesh( corridorConfig ); + corridorMesh.operation = ADDITION; + hangarMesh.add( corridorMesh ); + + /** + * Player quarters mesh. + */ + let quartersMesh = await this.loadQuartersMesh( quartersConfig ); + quartersMesh.operation = ADDITION; + hangarMesh.add( quartersMesh ); + + // Constructive Solid Geometry (csg) Evaluator. + let csgEvaluator; + csgEvaluator = new Evaluator(); + csgEvaluator.useGroups = true; + let result = csgEvaluator.evaluateHierarchy( hangarMesh ); + + let mesh = new THREE.Object3D(); + mesh.add( result ); + mesh.userData.targetable = false; + mesh.userData.objectClass = 'hangar'; + mesh.scale.setScalar( 2.5 ); + + return mesh; + } + + async load() { + + // Setup materials. + await this.loadMaterials(); + + this.mesh = await this.loadMesh(); + + } + + /** + * Setup Hangar materials + */ + async loadMaterials() { + this.materials.clear = new THREE.MeshBasicMaterial( { color: 0xffFFFF, transparent: true, visible: false, side: THREE.DoubleSide } ); + this.materials.metal = proceduralBuilding( { + uniforms: { + time: { value: 0.0 }, + scale: { value: .025 }, // Scale + lacunarity: { value: 2.0 }, // Lacunarity + randomness: { value: 1.0 }, // Randomness + emitColour1: { value: new THREE.Vector4( 0.0, 0.0, 0.0, 0.25 ) }, // Emission gradient colour 1 + emitColour2: { value: new THREE.Vector4( 0.158, 1., 1., .9 ) }, // Emission gradient colour 2 + shadowFactor: { value: 0.03 }, + shadowOffset: { value: 0.1 }, + } + } ); + this.materials.metal.transparent = true; + this.materials.metal.side = THREE.DoubleSide; + // Clone of the material to customize scaling. + this.materials.hangar = this.materials.metal.clone(); + this.materials.hangar.uniforms.scale.value = 0.25; + } + + /** + * Creates the Hangar's main mesh. + */ + async loadHangarMesh( config ) { + /** + * Hangar mesh + */ + let hangarMesh = new Operation( new THREE.BoxGeometry( config.width, config.height, config.depth, 2, 2, 2 ), [ + this.materials.hangar, + this.materials.hangar, + this.materials.hangar, + this.materials.hangar, + this.materials.hangar, + this.materials.clear.clone() + ] ); + + hangarMesh.updateMatrixWorld(); + + return hangarMesh; + } + + /** + * Creates the corridor mesh. + */ + async loadCorridorMesh( config ) { + + let corridorMesh = new Operation( new THREE.BoxGeometry( config.width, config.height, config.depth ), this.materials.metal ); + + corridorMesh.position.x = config.position.x; + corridorMesh.position.y = config.position.y; + corridorMesh.position.z = config.position.z; + corridorMesh.rotation.x = config.rotation.x; + corridorMesh.rotation.y = config.rotation.y; + corridorMesh.rotation.z = config.rotation.z; + + corridorMesh.updateMatrixWorld(); + + return corridorMesh; + } + + /** + * Creates the player quarters mesh. + */ + async loadQuartersMesh( config ) { + let quartersMesh = new Operation( new THREE.BoxGeometry( config.width, config.height, config.depth, 2, 2, 2 ), this.materials.metal ); + + quartersMesh.position.x = config.position.x; + quartersMesh.position.y = config.position.y; + quartersMesh.position.z = config.position.z; + quartersMesh.rotation.x = config.rotation.x; + quartersMesh.rotation.y = config.rotation.y; + quartersMesh.rotation.z = config.rotation.z; + quartersMesh.material.uniforms.scale.value = 0.8; + + quartersMesh.updateMatrixWorld(); + quartersMesh.name = 'inner'; + + return quartersMesh; + } + + /** + * Animate hook. + * + * This method is called within the main animation loop and + * therefore must only reference global objects or properties. + * + * @method animate + * @memberof Hangar + * @global + * @note All references within this method should be globally accessible. + **/ + animate( currentTime ) { + l.current_scene.objects.hangar.materials.metal.uniforms.time.value += 0.0000025; + } + +} diff --git a/client/src/app/scenograph/objects/structures/platform.js b/client/src/app/scenograph/objects/structures/platform.js index 01ad77e1..77f5cdf3 100644 --- a/client/src/app/scenograph/objects/structures/platform.js +++ b/client/src/app/scenograph/objects/structures/platform.js @@ -13,6 +13,8 @@ import { proceduralBuilding, proceduralMetalMaterial2, proceduralSolarPanel } fr export default class Platform { + instances; + // THREE.Mesh mesh; @@ -20,6 +22,7 @@ export default class Platform { model; constructor() { + this.instances = []; this.ready = false; } @@ -111,6 +114,14 @@ export default class Platform { this.ready = true; } + async get() { + let platform = this.mesh.clone(); + + this.instances.push( platform ); + + return platform; + } + /** * Animate hook. * diff --git a/client/src/app/scenograph/objects/structures/refineries.js b/client/src/app/scenograph/objects/structures/refinery.js similarity index 87% rename from client/src/app/scenograph/objects/structures/refineries.js rename to client/src/app/scenograph/objects/structures/refinery.js index 16082e1c..d33fe24b 100644 --- a/client/src/app/scenograph/objects/structures/refineries.js +++ b/client/src/app/scenograph/objects/structures/refinery.js @@ -10,7 +10,7 @@ import l from '@/helpers/l.js'; import { proceduralMetalMaterial2, proceduralBuilding } from '@/scenograph/materials.js'; import { Brush, Evaluator, INTERSECTION } from 'three-bvh-csg'; -export default class Refineries { +export default class Refinery { // Array of three.vector3's defining X/Z coordinates and radius of where extractors are in the ocean. locations; @@ -22,6 +22,7 @@ export default class Refineries { size; constructor() { + this.instances = []; this.ready = false; this.size = 1000; @@ -33,25 +34,16 @@ export default class Refineries { ]; } - async getAll() { - let meshes = []; + async get() { + let refinery = this.mesh.clone(); - await this.load(); + let i = this.instances.length; - this.locations.forEach( async ( location, i ) => { - let mesh = this.mesh.clone(); + refinery.name = 'Extractor #' + ( i + 1 ); - mesh.position.x = location.x; - mesh.position.y = this.size / 6.5; - mesh.position.z = location.y; - mesh.name = 'Refinery #' + ( i + 1 ); - mesh.userData.targetable = true; - mesh.userData.objectClass = 'refinery'; + this.instances.push( refinery ); - meshes.push( mesh ); - } ); - - return meshes; + return refinery; } async load() { @@ -141,7 +133,7 @@ export default class Refineries { **/ animate( delta ) { - l.current_scene.objects.refineries.forEach( ( refinery, i ) => { + l.scenograph.objects.structures.refinery.instances.forEach( ( refinery, i ) => { let inner = refinery.getObjectByName( 'inner' ); inner.material.uniforms.time.value += 0.00005; } ); diff --git a/client/src/app/scenograph/objects/vehicles/cargo_ships.js b/client/src/app/scenograph/objects/vehicles/cargo_ship.js similarity index 80% rename from client/src/app/scenograph/objects/vehicles/cargo_ships.js rename to client/src/app/scenograph/objects/vehicles/cargo_ship.js index d5029756..e2bcaf4c 100644 --- a/client/src/app/scenograph/objects/vehicles/cargo_ships.js +++ b/client/src/app/scenograph/objects/vehicles/cargo_ship.js @@ -16,14 +16,11 @@ import { proceduralMetalMaterial } from '@/scenograph/materials.js'; import { SUBTRACTION, Brush, Evaluator } from 'three-bvh-csg'; import cargoShip from '../../../../../../game/src/actors/cargoShip'; -export default class CargoShips { +export default class CargoShip { // THREE.Mesh clones instances; - // Array of three.vector3's defining X/Z coordinates and radius of where extractors are in the ocean. - locations; - // THREE.Mesh mesh; @@ -36,28 +33,10 @@ export default class CargoShips { // The scale of the mesh. size; - // Locations the cargo ships will randomly select and travel to. - targets; - constructor() { this.instances = []; this.ready = false; this.size = 1000; - - // Cargo ship start locations - this.locations = [ - //new THREE.Vector3( 0, -500, this.size * 10 ), // Test ship - new THREE.Vector3( -35000, -2000, this.size * 10 ), - new THREE.Vector3( -36000, -1500, this.size * 10 ), - new THREE.Vector3( -34000, -1500, this.size * 10 ), - ]; - - // Cargo ship destinations, use the current scene's extractor positions. - this.destinations = []; - l.current_scene.objects.extractors.forEach( (extractor) => { - this.destinations.push( new THREE.Vector3( extractor.position.x, 0, extractor.position.z ) ); - }); - } /** @@ -69,14 +48,15 @@ export default class CargoShips { let path = new YUKA.Path(); path.loop = true; - // Add the union platform - const platform_location = l.current_scene.objects.platform.mesh.position; - path.add( new YUKA.Vector3( platform_location.x, 0, platform_location.z ) ); + // Add the union platforms + l.scenograph.objects.structures.platform.instances.forEach( (platform) => { + path.add( new YUKA.Vector3( platform.position.x, 0, platform.position.z ) ); + }); - this.destinations.forEach( ( destination ) => { - path.add( new YUKA.Vector3( destination.x, 0, destination.z ) ); + // Add the extractors. + l.scenograph.objects.structures.extractor.instances.forEach( (extractor) => { + path.add( new YUKA.Vector3( extractor.position.x, 0, extractor.position.z ) ); }); - path.add( new YUKA.Vector3( platform_location.x, 0, platform_location.z ) ); return path; } @@ -136,6 +116,35 @@ export default class CargoShips { } + async get() { + let mesh = this.mesh.clone(); + mesh.userData.path = this.getPath(); + + let i = this.instances.length; + + // Bump each starting point for the cargo ships + for ( let j = 0; j < i; j++) { + mesh.userData.path.advance(); + } + + mesh.position.copy( mesh.userData.path.current() ); + mesh.name = 'Cargo Ship #' + ( i + 1 ); + + mesh.userData.objectClass = 'cargoShip'; + mesh.userData.targetable = true; + mesh.userData.size = this.size; + mesh.userData.actor = new cargoShip( mesh, l.current_scene.scene ); + + l.scenograph.entityManager.add( mesh.userData.actor.entity ); + + mesh.matrixAutoUpdate = false; + + this.instances.push( mesh ); + + return mesh; + } + + // Note: has to be loaded after extractors! async load() { //const material = new THREE.MeshBasicMaterial( {color: 0xff0000, transparent: true, opacity: 1.0, side: THREE.DoubleSide} ); @@ -205,7 +214,7 @@ export default class CargoShips { animate( delta ) { if ( l.current_scene.settings.game_controls ) { - l.current_scene.objects.cargo_ships.forEach( ( cargo_ship ) => { + l.scenograph.objects.vehicles.cargoShip.instances.forEach( ( cargo_ship ) => { cargo_ship.userData.actor.animate( delta ); diff --git a/client/src/app/scenograph/objects/vehicles/person.js b/client/src/app/scenograph/objects/vehicles/person.js new file mode 100644 index 00000000..d013233e --- /dev/null +++ b/client/src/app/scenograph/objects/vehicles/person.js @@ -0,0 +1,182 @@ +/** + * Person object. + * + * Rig to be used like a vehicle when in first person mode. + * + * @todo: Add some limbs or a body model to see. + */ +import * as THREE from 'three'; + +/** + * Internal libs and helpers. + */ +import l from '@/helpers/l.js'; + +export default class Person { + + constructor(actorInstance) { + // Set internal game accessor to the game world actor instance. + this.game = actorInstance; + + this.default_camera_distance = l.scenograph.width < l.scenograph.height ? -5 : -2.5; + + this.camera_distance = 0; + + this.mesh = new THREE.Object3D(); + } + + // Internal helper to manage state changes to the person's character model. + updateControls() { + let mappings = { + forward : 'W', + back : 'S', + jump : ' ', + crouch : 'shift', + turnLeft : 'A', + turnRight : 'D', + } + let changing = false; + for ( const [ controlName, keyMapping ] of Object.entries( mappings ) ) { + if ( l.scenograph.controls.keyboard.pressed( keyMapping ) ) { + this.game.actor.controls[ controlName ] = true; + changing = true; + } + else { + + this.game.actor.controls[ controlName ] = false; + + if ( l.scenograph.controls.touch ) { + // Check if any touchpad controls are being pressed + if ( + l.scenograph.controls.touch.controls.moveUp || + l.scenograph.controls.touch.controls.moveDown || + l.scenograph.controls.touch.controls.moveForward || + l.scenograph.controls.touch.controls.moveBackward || + l.scenograph.controls.touch.controls.moveLeft || + l.scenograph.controls.touch.controls.moveRight + ) { + changing = true; + if ( l.scenograph.controls.touch.controls.moveForward ) { + this.game.actor.controls.forward = true; + } + if ( l.scenograph.controls.touch.controls.moveBackward ) { + this.game.actor.controls.back = true; + } + if ( l.scenograph.controls.touch.controls.moveUp ) { + this.game.actor.controls.jump = true; + } + if ( l.scenograph.controls.touch.controls.moveDown ) { + this.game.actor.controls.crouch = true; + } + if ( l.scenograph.controls.touch.controls.moveLeft ) { + this.game.actor.controls.turnLeft = true; + } + if ( l.scenograph.controls.touch.controls.moveRight ) { + this.game.actor.controls.turnRight = true; + } + } + + } + } + } + this.game.actor.controls.changing = changing; + + } + + // Synchronise mesh with game world object. + sync() { + this.mesh.position.x = this.game.object.position.x; + this.mesh.position.y = this.game.object.position.y; + this.mesh.position.z = this.game.object.position.z; + + this.mesh.rotation.x = this.game.object.rotation.x; + this.mesh.rotation.y = this.game.object.rotation.y; + this.mesh.rotation.z = this.game.object.rotation.z; + } + + updateCamera( rY, tY, tZ ) { + var radian = ( Math.PI / 180 ); + + l.scenograph.actors.player.person.camera_distance = l.scenograph.actors.player.person.default_camera_distance + ( l.current_scene.room_depth / 20 ); + if ( l.scenograph.actors.player.person.airSpeed < 0 ) { + l.scenograph.actors.player.person.camera_distance -= l.scenograph.actors.player.person.airSpeed * 4; + } + + let xDiff = l.scenograph.actors.player.person.mesh.position.x; + let zDiff = l.scenograph.actors.player.person.mesh.position.z; + + l.scenograph.cameras.player.position.x = xDiff + l.scenograph.actors.player.person.camera_distance * Math.sin( l.scenograph.actors.player.person.mesh.rotation.y ); + l.scenograph.cameras.player.position.z = zDiff + l.scenograph.actors.player.person.camera_distance * Math.cos( l.scenograph.actors.player.person.mesh.rotation.y ); + + // if ( rY != 0 ) { + + // l.scenograph.cameras.player.rotation.y += rY; + // } + // else { + // // Check there is y difference and the rotation pad isn't being pressed. + // if ( + // l.scenograph.cameras.player.rotation.y != l.scenograph.actors.player.person.mesh.rotation.y && + // ( l.scenograph.controls.touch && !l.scenograph.controls.touch.controls.rotationPad.mouseDown ) + // ) { + + // // Get the difference in y rotation betwen the camera and ship + // let yDiff = l.scenograph.actors.player.person.mesh.rotation.y - l.scenograph.cameras.player.rotation.y; + + // // Check the y difference is larger than 1/100th of a radian + // if ( + // Math.abs( yDiff ) > radian / 100 + // ) { + // // Add 1/60th of the difference in rotation, as FPS currently capped to 60. + // l.scenograph.cameras.player.rotation.y += ( l.scenograph.actors.player.person.mesh.rotation.y - l.scenograph.cameras.player.rotation.y ) * 1 / 60; + // } + // else { + // l.scenograph.cameras.player.rotation.y = l.scenograph.actors.player.person.mesh.rotation.y; + // } + + // } + + // } + + // let xDiff2 = tZ * Math.sin( l.scenograph.actors.player.person.mesh.rotation.y ), + // zDiff2 = tZ * Math.cos( l.scenograph.actors.player.person.mesh.rotation.y ); + + // if ( l.scenograph.actors.player.person.mesh.position.y + tY >= 1 ) { + // l.scenograph.cameras.player.position.y += tY; + // } + + // l.scenograph.cameras.player.position.x += xDiff2; + // l.scenograph.cameras.player.position.z += zDiff2; + + l.scenograph.cameras.player.updateProjectionMatrix(); + } + + + /** + * Animate hook. + * + * This method is called within a delta in the main animation + * which means it supports "this" references to itself. + * + * @method animate + * @memberof Person + * @local + * @note All references within this method should be globally accessible. + **/ + animate( delta ) { + if ( l.current_scene.settings.game_controls && l.scenograph.actors.player.mode == 'person' && this.game.object) { + + // Detect keyboard input and pass it to the ship state model. + this.updateControls(); + + // Sync the mesh to game world state. + this.sync(); + + // Update the persons camera + this.updateCamera(this.game.object.rY, this.game.object.tY, this.game.object.tZ); + l.scenograph.cameras.player.rotation.y = this.game.object.rotation.y; + + } + } + + +} diff --git a/client/src/app/scenograph/objects/vehicles/raven.js b/client/src/app/scenograph/objects/vehicles/raven.js index 892db1a7..e1d12771 100644 --- a/client/src/app/scenograph/objects/vehicles/raven.js +++ b/client/src/app/scenograph/objects/vehicles/raven.js @@ -10,15 +10,18 @@ import * as THREE from 'three'; */ import l from '@/helpers/l.js'; import { brightenMaterial, proceduralMetalMaterial } from '@/scenograph/materials.js'; -import Pirate from '#/game/src/actors/pirate'; -import RavenBase from '#/game/src/objects/aircraft/raven'; +import PirateActor from '#/game/src/actors/pirate'; +import RavenObject from '#/game/src/objects/aircraft/raven'; -export default class Raven extends RavenBase { +export default class Raven { // An actor containing AI behaviours. actor; + // THREE.Mesh clones + instances; + // Ship Model (gltf) model; @@ -32,7 +35,8 @@ export default class Raven extends RavenBase { state; constructor() { - super(); + + this.instances = []; this.default_camera_distance = l.scenograph.width < l.scenograph.height ? -70 : -35; this.trail_position_y = 1.2; this.trail_position_z = 1.5; @@ -111,19 +115,27 @@ export default class Raven extends RavenBase { // this.mesh.scale.set(100,100,100); this.mesh.matrixAutoUpdate = false; - // @todo: Uncouple from the pirate actor when vehicle selection is introduced. - this.mesh.userData.actor = new Pirate( this.mesh, l.current_scene.scene ); - l.scenograph.entityManager.add( this.mesh.userData.actor.entity ); - this.mesh.userData.object = this; - this.mesh.userData.object.standing = -1; + } + + async get() { + let mesh = this.mesh.clone(); + + mesh.userData.object = new RavenObject( mesh ); + mesh.userData.object.standing = -1; // Set the object start position based on the path. // @todo: pluck it dynamically from path. - this.mesh.userData.object.startPosition.x = -2000; - this.mesh.userData.object.startPosition.y = this.mesh.position.y; - this.mesh.userData.object.startPosition.z = -1000; + mesh.userData.object.startPosition.x = -2000; + mesh.userData.object.startPosition.y = this.mesh.position.y; + mesh.userData.object.startPosition.z = -1000; + + mesh.userData.actor = new PirateActor( mesh, l.current_scene.scene ); + l.scenograph.entityManager.add( mesh.userData.actor.entity ); + this.instances.push( mesh ); + + return mesh; } /** @@ -138,8 +150,11 @@ export default class Raven extends RavenBase { * @note All references within this method should be globally accessible. **/ animate( delta ) { + if ( l.current_scene.settings.game_controls ) { - l.current_scene.objects.bot.mesh.userData.actor.animate( delta ); + l.scenograph.objects.vehicles.raven.instances.forEach( raven => { + raven.userData.actor.animate( delta ); + } ); } } diff --git a/client/src/app/scenograph/objects/vehicles/valiant.js b/client/src/app/scenograph/objects/vehicles/valiant.js index c735e83d..4179b4a2 100644 --- a/client/src/app/scenograph/objects/vehicles/valiant.js +++ b/client/src/app/scenograph/objects/vehicles/valiant.js @@ -1,8 +1,8 @@ /** * Valiant Aircraft - * + * * Provides a Valiant aircraft that can be added and updated in the game world. - * + * * @todo: Uncouple player actor from this ship class. */ @@ -14,9 +14,9 @@ import * as THREE from 'three'; import l from '@/helpers/l.js'; import { brightenMaterial, proceduralMetalMaterial } from '@/scenograph/materials.js'; import Player from '#/game/src/actors/player'; -import ValiantBase from '#/game/src/objects/aircraft/valiant'; +import ValiantObject from '#/game/src/objects/aircraft/valiant'; -export default class Valiant extends ValiantBase { +export default class Valiant { // Camera distance. camera_distance; @@ -46,7 +46,6 @@ export default class Valiant extends ValiantBase { trail; constructor() { - super(); this.default_camera_distance = -35; this.trail_position_y = 1.2; this.trail_position_z = 1.5; @@ -106,6 +105,7 @@ export default class Valiant extends ValiantBase { this.mesh.name = 'Player Ship'; this.mesh.position.z = l.current_scene.room_depth; this.mesh.rotation.order = 'YXZ'; + this.mesh.scale.setScalar(2); this.mesh.userData.targetable = true; this.mesh.userData.objectClass = 'player'; @@ -118,14 +118,11 @@ export default class Valiant extends ValiantBase { this.mixer.clipAction( this.model.animations[ 0 ] ).play(); this.mixer.clipAction( this.model.animations[ 1 ] ).play(); - l.current_scene.tweens.shipEnterY = this.shipEnterY(); - l.current_scene.tweens.shipEnterZ = this.shipEnterZ(); - //l.current_scene.effects.particles.createShipThruster(this, 1.5, { x: 0, y: 1.2, z: 1.5 }); this.trail = l.current_scene.effects.trail.createTrail( this.mesh, 0, this.trail_position_y, this.trail_position_z ); - this.mesh.userData.object = this; + this.mesh.userData.object = new ValiantObject( this.mesh ); } createThrusterMesh( options ) { @@ -272,7 +269,7 @@ export default class Valiant extends ValiantBase { .to( target, l.config.settings.skipintro ? 0 : 2000 ) // Move to (300, 200) in 1 second. .easing( TWEEN.Easing.Circular.Out ) // Use an easing function to make the animation smooth. .onUpdate( () => { - l.current_scene.objects.player.mesh.position.y = coords.y; + l.current_scene.objects.demoShip.mesh.position.y = coords.y; } ) .onComplete( () => { //console.log('ready'); @@ -290,7 +287,7 @@ export default class Valiant extends ValiantBase { // Called after tween.js updates 'coords'. // Move 'box' to the position described by 'coords' with a CSS translation. - l.current_scene.objects.player.mesh.position.z = coords.x; + l.current_scene.objects.demoShip.mesh.position.z = coords.x; } ) .onComplete( () => { @@ -310,11 +307,11 @@ export default class Valiant extends ValiantBase { } // Set the ship as ready. - l.current_scene.objects.player.ready = true; - l.current_scene.objects.player.camera_distance = l.current_scene.objects.player.default_camera_distance + ( l.current_scene.room_depth / 2 ); - l.current_scene.objects.player.position.x = l.current_scene.objects.player.mesh.position.x; - l.current_scene.objects.player.position.y = l.current_scene.objects.player.mesh.position.y; - l.current_scene.objects.player.position.z = l.current_scene.objects.player.mesh.position.z; + l.current_scene.objects.demoShip.ready = true; + l.current_scene.objects.demoShip.camera_distance = l.current_scene.objects.demoShip.default_camera_distance + ( l.current_scene.room_depth / 2 ); + l.current_scene.objects.demoShip.mesh.userData.object.position.x = l.current_scene.objects.demoShip.mesh.position.x; + l.current_scene.objects.demoShip.mesh.userData.object.position.y = l.current_scene.objects.demoShip.mesh.position.y; + l.current_scene.objects.demoShip.mesh.userData.object.position.z = l.current_scene.objects.demoShip.mesh.position.z; } ); } @@ -331,12 +328,12 @@ export default class Valiant extends ValiantBase { let changing = false; for ( const [ controlName, keyMapping ] of Object.entries( mappings ) ) { if ( l.scenograph.controls.keyboard.pressed( keyMapping ) ) { - l.current_scene.objects.player.controls[ controlName ] = true; + this.mesh.userData.object.controls[ controlName ] = true; changing = true; } else { - l.current_scene.objects.player.controls[ controlName ] = false; + this.mesh.userData.object.controls[ controlName ] = false; if ( l.scenograph.controls.touch ) { // Check if any touchpad controls are being pressed @@ -350,54 +347,54 @@ export default class Valiant extends ValiantBase { ) { changing = true; if ( l.scenograph.controls.touch.controls.moveUp ) { - l.current_scene.objects.player.controls.moveUp = true; + this.mesh.userData.object.controls.moveUp = true; } if ( l.scenograph.controls.touch.controls.moveDown ) { - l.current_scene.objects.player.controls.moveDown = true; + this.mesh.userData.object.controls.moveDown = true; } if ( l.scenograph.controls.touch.controls.moveForward ) { - l.current_scene.objects.player.controls.throttleUp = true; + this.mesh.userData.object.controls.throttleUp = true; } if ( l.scenograph.controls.touch.controls.moveBackward ) { - l.current_scene.objects.player.controls.throttleDown = true; + this.mesh.userData.object.controls.throttleDown = true; } if ( l.scenograph.controls.touch.controls.moveLeft ) { - l.current_scene.objects.player.controls.moveLeft = true; + this.mesh.userData.object.controls.moveLeft = true; } if ( l.scenograph.controls.touch.controls.moveRight ) { - l.current_scene.objects.player.controls.moveRight = true; + this.mesh.userData.object.controls.moveRight = true; } } } } } - l.current_scene.objects.player.controls.changing = changing; + this.mesh.userData.object.controls.changing = changing; } updateAnimation( delta ) { - if ( l.current_scene.objects.player.mixer ) { - l.current_scene.objects.player.mixer.update( delta ); + if ( this.mixer ) { + this.mixer.update( delta ); } // Rock the ship forward and back when moving horizontally - if ( l.current_scene.objects.player.controls.throttleDown || l.current_scene.objects.player.controls.throttleUp ) { - let pitchChange = l.current_scene.objects.player.controls.throttleUp ? -1 : 1; - if ( Math.abs( l.current_scene.objects.player.mesh.rotation.x ) < 1 / 4 ) { - l.current_scene.objects.player.mesh.rotation.x += pitchChange / 10 / 180; + if ( this.mesh.userData.object.controls.throttleDown || this.mesh.userData.object.controls.throttleUp ) { + let pitchChange = this.mesh.userData.object.controls.throttleUp ? -1 : 1; + if ( Math.abs( this.mesh.rotation.x ) < 1 / 4 ) { + this.mesh.rotation.x += pitchChange / 10 / 180; } } // Rock the ship forward and back when moving vertically if ( - l.current_scene.objects.player.controls.moveDown + this.mesh.userData.object.controls.moveDown || - l.current_scene.objects.player.controls.moveUp + this.mesh.userData.object.controls.moveUp ) { - let elevationChange = l.current_scene.objects.player.controls.moveDown ? -1 : 1; - if ( Math.abs( l.current_scene.objects.player.mesh.rotation.x ) < 1 / 8 ) { - l.current_scene.objects.player.mesh.rotation.x += elevationChange / 10 / 180; + let elevationChange = this.mesh.userData.object.controls.moveDown ? -1 : 1; + if ( Math.abs( this.mesh.rotation.x ) < 1 / 8 ) { + this.mesh.rotation.x += elevationChange / 10 / 180; } if ( Math.abs( l.scenograph.cameras.player.rotation.x ) < 1 / 8 ) { @@ -413,21 +410,75 @@ export default class Valiant extends ValiantBase { // Update the position of the aircraft to spot determined by game logic. updateMesh() { - l.current_scene.objects.player.mesh.position.x = l.current_scene.objects.player.position.x; - l.current_scene.objects.player.mesh.position.y = l.current_scene.objects.player.position.y; - l.current_scene.objects.player.mesh.position.z = l.current_scene.objects.player.position.z; - - l.current_scene.objects.player.mesh.rotation.x = l.current_scene.objects.player.rotation.x; - l.current_scene.objects.player.mesh.rotation.y = l.current_scene.objects.player.rotation.y; - l.current_scene.objects.player.mesh.rotation.z = l.current_scene.objects.player.rotation.z; + this.mesh.position.x = this.mesh.userData.object.position.x; + this.mesh.position.y = this.mesh.userData.object.position.y; + this.mesh.position.z = this.mesh.userData.object.position.z; + this.mesh.rotation.x = this.mesh.userData.object.rotation.x; + this.mesh.rotation.y = this.mesh.userData.object.rotation.y; + this.mesh.rotation.z = this.mesh.userData.object.rotation.z; } + updateCamera( rY, tY, tZ ) { + var radian = ( Math.PI / 180 ); + + this.camera_distance = this.default_camera_distance + ( l.current_scene.room_depth / 2 ); + if ( this.mesh.userData.object.airSpeed < 0 ) { + this.camera_distance -= this.mesh.userData.object.airSpeed * 4; + } + + let xDiff = this.mesh.position.x; + let zDiff = this.mesh.position.z; + + l.scenograph.cameras.player.position.x = xDiff + this.camera_distance * Math.sin( this.mesh.rotation.y ); + l.scenograph.cameras.player.position.z = zDiff + this.camera_distance * Math.cos( this.mesh.rotation.y ); + + if ( rY != 0 ) { + + l.scenograph.cameras.player.rotation.y += rY; + } + else { + // Check there is y difference and the rotation pad isn't being pressed. + if ( + l.scenograph.cameras.player.rotation.y != this.mesh.rotation.y && + ( l.scenograph.controls.touch && !l.scenograph.controls.touch.controls.rotationPad.mouseDown ) + ) { + + // Get the difference in y rotation betwen the camera and ship + let yDiff = this.mesh.rotation.y - l.scenograph.cameras.player.rotation.y; + + // Check the y difference is larger than 1/100th of a radian + if ( + Math.abs( yDiff ) > radian / 100 + ) { + // Add 1/60th of the difference in rotation, as FPS currently capped to 60. + l.scenograph.cameras.player.rotation.y += ( this.mesh.rotation.y - l.scenograph.cameras.player.rotation.y ) * 1 / 60; + } + else { + l.scenograph.cameras.player.rotation.y = this.mesh.rotation.y; + } + + } + + } + + let xDiff2 = tZ * Math.sin( this.mesh.rotation.y ), + zDiff2 = tZ * Math.cos( this.mesh.rotation.y ); + + if ( this.mesh.position.y + tY >= 1 ) { + l.scenograph.cameras.player.position.y += tY; + } + + l.scenograph.cameras.player.position.x += xDiff2; + l.scenograph.cameras.player.position.z += zDiff2; + + l.scenograph.cameras.player.updateProjectionMatrix(); + } /** * Animate hook. - * + * * This method is called within the main animation loop and * therefore must only reference global objects or properties. - * + * * @method animate * @memberof Valiant * @global @@ -435,71 +486,78 @@ export default class Valiant extends ValiantBase { **/ animate( delta ) { - if ( l.current_scene.objects.player.ready ) { + if ( l.current_scene.objects.demoShip.ready ) { if ( l.current_scene.settings.game_controls ) { - // Detect keyboard input and pass it to the ship state model. - l.current_scene.objects.player.updateControls(); + + if ( l.scenograph.actors.player.mode == 'vehicle' ) { + // Detect keyboard input and pass it to the ship state model. + this.updateControls(); + } if ( l.scenograph.modes.multiplayer.connected ) { - l.scenograph.modes.multiplayer.socket.emit( 'input', l.current_scene.objects.player.controls ); + l.scenograph.modes.multiplayer.socket.emit( 'input', this.mesh.userData.object.controls ); } - l.current_scene.objects.player.mesh.userData.actor.animate( delta ); + this.mesh.userData.actor.animate( delta ); } - l.current_scene.objects.player.updateAnimation( delta ); + if ( l.mode != 'hangar') + this.updateAnimation( delta ); // Update the ships state model. - let [ rY, tY, tZ ] = l.current_scene.objects.player.move( l.current_scene.stats.currentTime - l.current_scene.stats.lastTime ); - l.current_scene.objects.player.updateMesh(); + let [ rY, tY, tZ ] = this.mesh.userData.object.move( l.current_scene.stats.currentTime - l.current_scene.stats.lastTime ); + this.updateMesh(); - l.scenograph.cameras.updatePlayer( rY, tY, tZ ); + this.updateCamera( rY, tY, tZ ); - if ( l.current_scene.objects.player.trail ) { + this.animateTrail( rY ); - // Fix the trail being too far behind. - let trailOffset = 0; + } + } - // Only offset the trail effect if we are going forward which is (z-1) in numerical terms - if ( l.current_scene.objects.player.airSpeed < 0 ) { + animateTrail( rY ) { + if ( this.trail ) { - // Update ship thruster - l.current_scene.objects.player.animateThruster( l.current_scene.objects.player.airSpeed, l.current_scene.objects.player.thruster.centralConeBurner, .5 ); - l.current_scene.objects.player.animateThruster( l.current_scene.objects.player.airSpeed, l.current_scene.objects.player.thruster.outerCylBurner, .5 ); + // Fix the trail being too far behind. + let trailOffset = 0; - l.current_scene.objects.player.spinThruster( l.current_scene.objects.player.airSpeed, l.current_scene.objects.player.thruster.rearConeBurner, -1 ); - l.current_scene.objects.player.spinThruster( l.current_scene.objects.player.airSpeed, l.current_scene.objects.player.thruster.centralConeBurner, 1 ); - l.current_scene.objects.player.spinThruster( l.current_scene.objects.player.airSpeed, l.current_scene.objects.player.thruster.outerCylBurner, -1 ); - l.current_scene.objects.player.spinThruster( l.current_scene.objects.player.airSpeed, l.current_scene.objects.player.thruster.innerCylBurner, 1 ); + // Only offset the trail effect if we are going forward which is (z-1) in numerical terms + if ( this.mesh.userData.object.airSpeed < 0 ) { - // Limit playback rate to 5x as large values freak out the browser. - l.current_scene.objects.player.thruster.videoElement.playbackRate = Math.min( 5, 0.25 + Math.abs( l.current_scene.objects.player.airSpeed ) ); + // Update ship thruster + this.animateThruster( this.mesh.userData.object.airSpeed, this.thruster.centralConeBurner, .5 ); + this.animateThruster( this.mesh.userData.object.airSpeed, this.thruster.outerCylBurner, .5 ); - trailOffset += l.current_scene.objects.player.trail_position_z - Math.abs( l.current_scene.objects.player.airSpeed ); + this.spinThruster( this.mesh.userData.object.airSpeed, this.thruster.rearConeBurner, -1 ); + this.spinThruster( this.mesh.userData.object.airSpeed, this.thruster.centralConeBurner, 1 ); + this.spinThruster( this.mesh.userData.object.airSpeed, this.thruster.outerCylBurner, -1 ); + this.spinThruster( this.mesh.userData.object.airSpeed, this.thruster.innerCylBurner, 1 ); - l.current_scene.objects.player.trail.mesh.material.uniforms.headColor.value.set( 255 / 255, 212 / 255, 148 / 255, .8 ); // RGBA. - } - else { - l.current_scene.objects.player.trail.mesh.material.uniforms.headColor.value.set( 255 / 255, 212 / 255, 148 / 255, 0 ); // RGBA. - } + // Limit playback rate to 5x as large values freak out the browser. + this.thruster.videoElement.playbackRate = Math.min( 5, 0.25 + Math.abs( this.mesh.userData.object.airSpeed ) ); - // Update the trail position based on above calculations. - l.current_scene.objects.player.trail.targetObject.position.y = l.current_scene.objects.player.trail_position_y + l.current_scene.objects.player.verticalSpeed; - l.current_scene.objects.player.trail.targetObject.position.z = trailOffset; + trailOffset += this.trail_position_z - Math.abs( this.mesh.userData.object.airSpeed ); - if ( rY != 0 ) { - l.current_scene.objects.player.trail.targetObject.position.x = rY * l.current_scene.objects.player.airSpeed; - l.current_scene.objects.player.trail.targetObject.position.y += Math.abs( l.current_scene.objects.player.trail.targetObject.position.x ) / 4; - } - else { - l.current_scene.objects.player.trail.targetObject.position.x = 0; - } - l.current_scene.objects.player.trail.update(); + this.trail.mesh.material.uniforms.headColor.value.set( 255 / 255, 212 / 255, 148 / 255, .8 ); // RGBA. + } + else { + this.trail.mesh.material.uniforms.headColor.value.set( 255 / 255, 212 / 255, 148 / 255, 0 ); // RGBA. } - + // Update the trail position based on above calculations. + this.trail.targetObject.position.y = this.trail_position_y + this.mesh.userData.object.verticalSpeed; + this.trail.targetObject.position.z = trailOffset; + + if ( rY != 0 ) { + this.trail.targetObject.position.x = rY * this.mesh.userData.object.airSpeed; + this.trail.targetObject.position.y += Math.abs( this.trail.targetObject.position.x ) / 4; + } + else { + this.trail.targetObject.position.x = 0; + } + this.trail.update(); } } diff --git a/client/src/app/scenograph/overlays/heads-up-display.js b/client/src/app/scenograph/overlays/heads-up-display.js index 573a83c7..4d282ebc 100644 --- a/client/src/app/scenograph/overlays/heads-up-display.js +++ b/client/src/app/scenograph/overlays/heads-up-display.js @@ -57,10 +57,10 @@ export default class HeadsUpDisplay { /** * Animate hook. - * + * * This method is called within the main animation loop and * therefore must only reference global objects or properties. - * + * * @method animate * @memberof HeadsUpDisplay * @global @@ -77,13 +77,13 @@ export default class HeadsUpDisplay { l.scenograph.overlays.hud.container.classList.remove('portrait'); } - let aspd = -l.scenograph.overlays.hud.frameToSecond(l.current_scene.objects.player.airSpeed); + let aspd = -l.scenograph.overlays.hud.frameToSecond(l.scenograph.actors.player.vehicle.mesh.userData.object.airSpeed); l.scenograph.overlays.hud.aspdElement.innerHTML = `AIRSPEED: ${aspd}km/h`; - let vspd = l.scenograph.overlays.hud.frameToSecond(l.current_scene.objects.player.verticalSpeed); + let vspd = l.scenograph.overlays.hud.frameToSecond(l.scenograph.actors.player.vehicle.mesh.userData.object.verticalSpeed); l.scenograph.overlays.hud.vspdElement.innerHTML = `VERT. SPD: ${vspd}km/h`; - let heading = THREE.MathUtils.radToDeg( l.current_scene.objects.player.rotation.y ); + let heading = THREE.MathUtils.radToDeg( l.scenograph.actors.player.vehicle.mesh.rotation.y ); heading = heading % 360; if (heading < 0) { heading += 360; @@ -91,7 +91,7 @@ export default class HeadsUpDisplay { heading = Math.round(360 - heading); l.scenograph.overlays.hud.headingElement.innerHTML = `HEADING: ${heading}°`; - let elevation = Math.round( l.current_scene.objects.player.position.y * 100 ) / 100; + let elevation = Math.round( l.scenograph.actors.player.vehicle.mesh.position.y * 100 ) / 100; l.scenograph.overlays.hud.elevationElement.innerHTML = `ELEVATION: ${elevation}m`; } @@ -104,4 +104,4 @@ export default class HeadsUpDisplay { return metersPerSecond; } -} \ No newline at end of file +} diff --git a/client/src/app/scenograph/overlays/map.js b/client/src/app/scenograph/overlays/map.js index 2bbaac6e..a9eca0ff 100644 --- a/client/src/app/scenograph/overlays/map.js +++ b/client/src/app/scenograph/overlays/map.js @@ -68,6 +68,7 @@ export default class Map { 'cargoShip': 'ship', 'city': 'structure', 'extractors': 'structure', + 'hangar': 'structure', 'missiles': 'aircraft', 'player': 'aircraft', 'refinery': 'structure', @@ -105,16 +106,16 @@ export default class Map { let offset = mapSize / l.scenograph.overlays.map.distance; // Pixels per world unit let halfMapSize = (mapSize / 2) - 2.5; // Half the map size to center objects - let leftEdge = l.current_scene.objects.player.mesh.position.x - l.scenograph.overlays.map.distance / 2; - let topEdge = l.current_scene.objects.player.mesh.position.z - l.scenograph.overlays.map.distance / 2; + let leftEdge = l.scenograph.actors.player.vehicle.mesh.position.x - l.scenograph.overlays.map.distance / 2; + let topEdge = l.scenograph.actors.player.vehicle.mesh.position.z - l.scenograph.overlays.map.distance / 2; - l.current_scene.objects.player.mesh.userData.actor.scanners.targets.forEach( target => { - let distance = target.mesh.position.distanceTo( l.current_scene.objects.player.mesh.position ); + l.scenograph.actors.player.vehicle.mesh.userData.actor.scanners.targets.forEach( target => { + let distance = target.mesh.position.distanceTo( l.scenograph.actors.player.vehicle.mesh.position ); // Check if the object is within the mapping distance. if ( distance <= l.scenograph.overlays.map.distance * 100 ) { - + // Check if the object is already present on the map, move it if so if ( target.mesh.uuid in l.scenograph.overlays.map.markers ) { @@ -160,17 +161,17 @@ export default class Map { /** * Animate hook. - * + * * This method is called within the main animation loop andw * therefore must only reference global objects or properties. - * + * * @method animate * @memberof Map * @global * @note All references within this method should be globally accessible. **/ animate() { - let heading = THREE.MathUtils.radToDeg( l.current_scene.objects.player.rotation.y ); + let heading = THREE.MathUtils.radToDeg( l.scenograph.actors.player.vehicle.mesh.rotation.y ); heading = heading % 360; if (heading < 0) { heading += 360; diff --git a/client/src/app/scenograph/overlays/scanners.js b/client/src/app/scenograph/overlays/scanners.js index e9965e2d..8fda3c40 100644 --- a/client/src/app/scenograph/overlays/scanners.js +++ b/client/src/app/scenograph/overlays/scanners.js @@ -129,10 +129,12 @@ export default class Scanners { l.scenograph.cameras.active.updateProjectionMatrix(); // Use the players scanners to update the overlays. - l.current_scene.objects.player.mesh.userData.actor.scanners.targets.forEach( target => l.scenograph.overlays.scanners.animateTarget( delta, target, frustum ) ); + l.scenograph.actors.player.vehicle.mesh.userData.actor.scanners.targets.forEach( target => l.scenograph.overlays.scanners.animateTarget( delta, target, frustum ) ); l.scenograph.overlays.scanners.removeOldTargets(); + l.scenograph.overlays.scanners.toggleRespawningTargets(); + } /** @@ -170,10 +172,28 @@ export default class Scanners { domElement.classList.remove('locking'); } + domElement.style.display = `block`; domElement.style.left = `${x-10}px`; domElement.style.top = `${y-10}px`; } + + /** + * Show/hide markers of objects that are respawning. + */ + toggleRespawningTargets() { + + const overlayKeys = Object.keys(l.scenograph.overlays.scanners.trackedObjects); + const scannerKeys = l.scenograph.actors.player.vehicle.mesh.userData.actor.scanners.targets.map(t => t.mesh.uuid); + + // Hide targets missing from player scanners, theoretically those are ones being relocated by the engine / respawning. + const respawningTargets = overlayKeys.filter(k => !scannerKeys.includes(k)); + respawningTargets.map( uuid => { + l.scenograph.overlays.scanners.trackedObjects[ uuid ].style.display = 'none'; + } ); + + } + /** * Remove markers for objects no longer in the scene. */ @@ -187,7 +207,7 @@ export default class Scanners { // Delete the marker domElement from memory. delete l.scenograph.overlays.scanners.trackedObjects[ uuid ]; - } + } } } @@ -206,6 +226,7 @@ export default class Scanners { 'cargoShip': 'ship', 'city': 'structure', 'extractors': 'structure', + 'hangar': 'structure', 'missiles': 'aircraft', 'player': 'aircraft', 'refinery': 'structure', diff --git a/client/src/app/scenograph/scenes/base.js b/client/src/app/scenograph/scenes/base.js deleted file mode 100644 index ca0cac0e..00000000 --- a/client/src/app/scenograph/scenes/base.js +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Scene base - */ - -export default class SceneBase { - constructor() { - - /** - * Animation queue. - */ - this.animation_queue = []; - - /** - * Primary scene camera - * - * @memberof THREE.Camera - */ - this.camera = false; - - /** - * Debug mode. - * - * @memberof Boolean - */ - this.debug = false; - - /** - * Effects composers and their layers. - * - * @memberof Object { postprocessing.EffectComposer } - */ - this.effects = { - particles: false, - postprocessing: false - }; - - /** - * Exit sign. - * - * @todo: Consolidate scene rigs. - * - * @memberOf function - */ - this.exitSignClick = false; - - /** - * Fast mode (bloom off, no shadows) - * - * @memberof Boolean - */ - this.fast = true; - - /** - * Reusable loaders for assets. - */ - this.loaders = { - gltf: false, - object: false, - texture: false, - stats: { - fonts: { - target: 0, // @todo: Check if this affects double loads, shouldn't with caching. - loaded: 0 - }, - gtlf: { - target: 0, // @todo: Check if this affects double loads, shouldn't with caching. - loaded: 0 - }, - screens: { - target: 0, - loaded: 0 - }, - svg: { - target: 1, - loaded: 0 - }, - textures: { - target: 9, - loaded: 0 - } - }, - - /** - * UI controller - */ - ui: false - }; - - /** - * Reusable materials for assets - */ - this.materials = false; - - /** - * Camera is being moved by tweening. - * - * @memberof Boolean - */ - this.moving = false; - - /** - * Current position of the users pointer. - * - * @memberof THREE.Vector2 - */ - this.pointer = false; - - /** - * Raycaster that projects into the scene from the users pointer and picks up collisions for interaction. - * - * @memberof THREE.Raycaster - */ - this.raycaster = false; - - /** - * Ready to begin. - */ - - this.ready = false; - - /** - * Renderers that create the scene. - * - * @memberof Object { THREE.Renderer , ... } - */ - this.renderers = { - webgl: false - }; - - /** - * Settings that controls the scene. - * - * @memberof Object - */ - this.settings = { - adjusted_gap: false, // calculated value - game_controls: false, // show in-game control overlays. - gap: 1.3, // depth(z axis) gap between desks - light: { - fast: { - desk: { - normal: 0.015, active: 0.05 - }, - neonSign: { - normal: 0.35, active: 0.05 - } - }, - highP: { - desk: { - normal: 0.015, active: 0.035 - }, - neonSign: { - normal: 0.1, active: 0.05 - } - } - - }, - room_depth: false, // calculated value - scale: 11, // do not change, braeks css screen sizes - startPosZ: - 10 // updated responsive eugene levy - }; - - /** - * The main scene container. - * - * @memberof THREE.Scene - */ - this.scene = false; - - /** - * Tracked meshes and mesh groups that compose the scene. - * - * @memberof Object - */ - this.objects = { }; - - /** - * Currently selected object. - * - * @memberof THREE.Object3d - */ - this.selected = false; - - /** - * Skip the intro sequence for this scene. - */ - this.skipintro = false; - - /** - * If the main sequence has begun. - * - * @memberof Boolean - */ - this.started = false; - - /** - * Custom array of game performance stats. - */ - this.stats = { - /** - * Frames Per Second (FPS) - * - * @memberof Integer - */ - currentTime: performance.now(), - fps: 0, - frameCount: 0, - lastTime: performance.now(), - }; - - /** - * All scene triggers. - * - * @memberof Object - */ - this.triggers = {}; - - /** - * All scene tweens. - * - * @memberof Object - */ - this.tweens = {}; - } -} diff --git a/client/src/app/scenograph/scenes/overworld.js b/client/src/app/scenograph/scenes/overworld.js index bf7d7b12..a1dc8c80 100644 --- a/client/src/app/scenograph/scenes/overworld.js +++ b/client/src/app/scenograph/scenes/overworld.js @@ -20,11 +20,13 @@ import Sky2 from "@/scenograph/objects/environment/sky2"; // Structures import Extractors from "@/scenograph/objects/structures/extractors"; +import Hangar from "@/scenograph/objects/structures/hangar"; import Platform from "@/scenograph/objects/structures/platform"; import Refineries from "@/scenograph/objects/structures/refineries"; // Vehicles import CargoShips from "@/scenograph/objects/vehicles/cargo_ships"; +import Person from "@/scenograph/objects/vehicles/person"; import Raven from "@/scenograph/objects/vehicles/raven"; import Valiant from "@/scenograph/objects/vehicles/valiant"; @@ -104,14 +106,23 @@ export default class Overworld extends SceneBase { // l.current_scene.objects.sky.animate // ); - // Setup Player, currently hardcoded to Valiant aircraft - l.current_scene.objects.player = new Valiant(); - await l.current_scene.objects.player.load(); + // Setup Player aircraft, used for the intro sequence. + l.scenograph.actors.player.vehicle = new Valiant(); + await l.scenograph.actors.player.vehicle.load(); l.current_scene.scene.add( - l.current_scene.objects.player.mesh + l.scenograph.actors.player.vehicle.mesh ); l.current_scene.animation_queue.push( - l.current_scene.objects.player.animate + l.scenograph.actors.player.vehicle.animate + ); + + // Setup Player person, used for the hangar scene. + l.scenograph.actors.player.person = new Person(); + l.current_scene.scene.add( + l.scenograph.actors.player.person.mesh + ); + l.current_scene.animation_queue.push( + l.scenograph.actors.player.person.animate ); // let scale = 500; @@ -172,6 +183,19 @@ export default class Overworld extends SceneBase { l.current_scene.objects.bot.animate ); + // Setup hangar + // @todo: #31 Implement first person mode and a way to go between being in the hangar and being in the aircraft + l.current_scene.objects.hangar = new Hangar(); + await l.current_scene.objects.hangar.load(); + // Hide the hangar so we can load it when needed. + l.current_scene.objects.hangar.mesh.visible = false; + l.current_scene.scene.add( + l.current_scene.objects.hangar.mesh + ); + l.current_scene.animation_queue.push( + l.current_scene.objects.hangar.animate + ); + // Setup Platform l.current_scene.objects.platform = new Platform(); diff --git a/client/src/app/scenograph/tweens.js b/client/src/app/scenograph/tweens.js index 38c3445e..289ae665 100644 --- a/client/src/app/scenograph/tweens.js +++ b/client/src/app/scenograph/tweens.js @@ -2,7 +2,7 @@ * Tweens * * Helpers for managing scene tweens. - * + * * @todo: Move overworld specific tweens into the scenes/overworld folder */ @@ -218,7 +218,7 @@ function flickerEffect() { */ function enterTheOffice() { let coords = { x: 15 + l.current_scene.room_depth / 2 }; // Start at (0, 0) - let targetZ = l.current_scene.objects.player.default_camera_distance + l.current_scene.room_depth / 2; + let targetZ = l.current_scene.objects.demoShip.default_camera_distance + l.current_scene.room_depth / 2; return new TWEEN.Tween( coords, false ) // Create a new tween that modifies 'coords'. .to( { x: targetZ }, l.config.settings.skipintro ? 0 : 1000 ) // Move to (300, 200) in 1 second. .easing( TWEEN.Easing.Quadratic.InOut ) // Use an easing function to make the animation smooth. @@ -311,7 +311,7 @@ function dollyUp() { return new TWEEN.Tween( l.scenograph.cameras.player.position ) .to( { y: l.scenograph.cameras.playerY }, l.config.settings.skipintro ? 0 : 500 ) // Set the duration of the animation .onUpdate( () => { - //l.scenograph.cameras.player.lookAt(l.current_scene.objects.player.mesh.position); + //l.scenograph.cameras.player.lookAt(l.current_scene.objects.demoShip.mesh.position); l.scenograph.cameras.player.updateProjectionMatrix(); } ) .onComplete( () => { diff --git a/client/src/app/ui/flight_instruments.js b/client/src/app/ui/flight_instruments.js index 941e35ce..f5e25f0c 100644 --- a/client/src/app/ui/flight_instruments.js +++ b/client/src/app/ui/flight_instruments.js @@ -1,6 +1,6 @@ /** * Controls flight instrument UI elements - * + * * @todo: v7: Remove if not used. */ @@ -26,10 +26,10 @@ export default class Flight_Instruments { /** * Update hook. - * + * * This method is called within the UI setInterval updater, allowing * HTML content to be updated at different rate than the 3D frame rate. - * + * * @method update * @memberof Flight_Instruments * @global @@ -38,7 +38,7 @@ export default class Flight_Instruments { update() { // Check if the main aircraft is loaded and ready - if (l.current_scene.objects.player && l.current_scene.objects.player.ready) { + if (l.scenograph.actors.player.vehicle && l.scenograph.actors.player.vehicle.ready) { if ( l.current_scene.settings.game_controls ) { if ( !l.ui.flight_instruments.activated ) { @@ -47,11 +47,11 @@ export default class Flight_Instruments { } // Update the angle of the needle - const angle = (Math.abs(l.current_scene.objects.player.airSpeed) * 1.94384) * 45; + const angle = (Math.abs(l.scenograph.actors.player.vehicle.mesh.userData.object.airSpeed) * 1.94384) * 45; // Update the needle rotation document.querySelector(l.ui.flight_instruments.containerSelector + ' #Airspeed #Needle').style.transform = `rotate(${angle}deg)`; } - + } } diff --git a/client/src/app/ui/menus/debugging_tools.js b/client/src/app/ui/menus/debugging_tools.js index 865dc475..176aa054 100644 --- a/client/src/app/ui/menus/debugging_tools.js +++ b/client/src/app/ui/menus/debugging_tools.js @@ -73,19 +73,19 @@ export default class Debugging_Tools { expanded: false, } ); - shipState.addBinding( l.current_scene.objects.player.controls, 'throttleUp', { + shipState.addBinding( l.scenograph.actors.player.vehicle.controls, 'throttleUp', { readonly: true, interval: 200 } ) - shipState.addBinding( l.current_scene.objects.player.controls, 'throttleDown', { + shipState.addBinding( l.scenograph.actors.player.vehicle.controls, 'throttleDown', { readonly: true, interval: 200 } ) - shipState.addBinding( l.current_scene.objects.player.controls, 'moveLeft', { + shipState.addBinding( l.scenograph.actors.player.vehicle.controls, 'moveLeft', { readonly: true, interval: 200 } ) - shipState.addBinding( l.current_scene.objects.player.controls, 'moveRight', { + shipState.addBinding( l.scenograph.actors.player.vehicle.controls, 'moveRight', { readonly: true, interval: 200 } ) diff --git a/client/src/app/ui/menus/main_menu.js b/client/src/app/ui/menus/main_menu.js index 70bd95d8..2b2ab675 100644 --- a/client/src/app/ui/menus/main_menu.js +++ b/client/src/app/ui/menus/main_menu.js @@ -70,19 +70,33 @@ export default class Main_Menu { l.ui.score_table.show(); }); - this.buttons.single_player = this.pane.addButton( { - title: 'Single Player', + this.buttons.test = this.pane.addButton( { + title: 'Test - Hangar', } ); - this.buttons.single_player.on( 'click', () => { - console.log( 'Single player launched' ); + this.buttons.test.on( 'click', () => { + new l.routes.hangar(); + + // Hide game mode buttons. + this.buttons.single_player.hidden = true; + this.buttons.multi_player.hidden = true; - // Start controls. - l.scenograph.controls.activate(); + // Hide main menu and change it's title + this.pane.expanded = false; + this.pane.title = "Menu"; + + // Show game exit button to return to main menu. + this.buttons.exit_game.hidden = false; - // Start overlays. - l.scenograph.overlays.activate(); + // Show game scores button + this.buttons.scores.hidden = false; + + } ); - //l.ui.show_flight_instruments(); + this.buttons.single_player = this.pane.addButton( { + title: 'Single Player', + } ); + this.buttons.single_player.on( 'click', () => { + new l.routes.singlePlayer(); // Hide game mode buttons. this.buttons.single_player.hidden = true; @@ -97,9 +111,7 @@ export default class Main_Menu { // Show game scores button this.buttons.scores.hidden = false; - - // Set client mode. - l.mode = 'single_player'; + } ); this.buttons.multi_player = this.pane.addButton( { @@ -107,11 +119,7 @@ export default class Main_Menu { disabled: true // @todo: v7 Restore multiplayer and server tracking of scene objects. } ); this.buttons.multi_player.on( 'click', () => { - console.log( 'Multi player launched' ); - - l.scenograph.controls.activate(); - - //l.ui.show_flight_instruments(); + new l.routes.multiPlayer(); // Hide game mode buttons. this.buttons.single_player.hidden = true; @@ -127,12 +135,7 @@ export default class Main_Menu { // Show game scores button this.buttons.scores.hidden = false; - let serverLocation = l.env == 'Dev' ? 'lcl.langenium.com:8090' : 'test.langenium.com:42069'; - - l.scenograph.modes.multiplayer.connect( '//' + serverLocation ); - // Set client mode. - l.mode = 'multi_player'; } ); this.buttons.settings = this.pane.addButton( { diff --git a/client/src/app/ui/targeting/list.js b/client/src/app/ui/targeting/list.js index 7ca0e501..2b1c1d5c 100644 --- a/client/src/app/ui/targeting/list.js +++ b/client/src/app/ui/targeting/list.js @@ -58,7 +58,7 @@ export default class List { // Check if targetable and not the current player. let targetable = mesh.userData && mesh.userData.targetable ? true : false; - if ( targetable && mesh.uuid != l.current_scene.objects.player.mesh.uuid ) { + if ( targetable && mesh.uuid != l.scenograph.actors.player.vehicle.mesh.uuid ) { let item = JSON.parse( JSON.stringify( l.ui.targeting.list.item_template ) ); let icon_class = ''; @@ -143,7 +143,7 @@ export default class List { let targetObject = l.current_scene.scene.getObjectByProperty( 'uuid', targetIcon.dataset.uuid ); // Update the distance to target. - let distance = targetObject.position.distanceTo( l.current_scene.objects.player.mesh.position ); + let distance = targetObject.position.distanceTo( l.scenograph.actors.player.vehicle.mesh.position ); if ( distance > 1000 ) { distance = Math.round( Math.round( distance ) / 10 ) / 100; targetIcon.querySelector( '.distance' ).innerHTML = distance + 'km'; diff --git a/client/src/app/ui/targeting/locked.js b/client/src/app/ui/targeting/locked.js index 89692ea0..5947b04c 100644 --- a/client/src/app/ui/targeting/locked.js +++ b/client/src/app/ui/targeting/locked.js @@ -80,7 +80,7 @@ export default class Locked { let targetObject = l.current_scene.scene.getObjectByProperty( 'uuid', targetIcon.dataset.uuid); // Update the distance to target. - let distance = targetObject.position.distanceTo( l.current_scene.objects.player.mesh.position ); + let distance = targetObject.position.distanceTo( l.scenograph.actors.player.vehicle.mesh.position ); if ( distance > 1000 ) { distance = Math.round(Math.round(distance) / 10) / 100; targetIcon.querySelector('.distance').innerHTML = distance + 'km'; diff --git a/client/watch.mjs b/client/watch.mjs index 029a4075..2acadbe9 100644 --- a/client/watch.mjs +++ b/client/watch.mjs @@ -2,6 +2,7 @@ import { fileURLToPath } from 'url'; import { dirname, resolve } from 'path'; import esbuild from 'esbuild'; +import { YAMLPlugin } from 'esbuild-yaml'; const __filename = fileURLToPath( import.meta.url ); const __dirname = dirname( __filename ); @@ -13,6 +14,9 @@ const context = await esbuild minify: false, outdir: '../docs', target: 'es2018', + plugins: [ + YAMLPlugin() + ], alias: { '@': resolve( __dirname, 'src/app' ), '#': resolve( __dirname, '..' ), diff --git a/game/src/actors/base2.ts b/game/src/actors/base2.ts new file mode 100644 index 00000000..d57e09e1 --- /dev/null +++ b/game/src/actors/base2.ts @@ -0,0 +1,41 @@ +/** + * Actor Base + * + * Provides an instance of an actor that can be attached to game objects. + * + * Behaviours such as bot AI, path finding and combat are attached to the entity of actors. + */ + +import * as YUKA from 'yuka'; + +export default class BaseActor { + + public entity: YUKA.GameEntity; // set by instantiator. + + public score: { kills: number; deaths: number } = { kills: 0, deaths: 0 }; + public faction: string = 'winthrom'; + public standing: { union: number, winthrom: number, zaar: number } = { union: 0.5, winthrom: 1.0, zaar: 0.0 } + + public controls: { + changing: boolean, + forward: boolean; + back: boolean; + jump: boolean; + crouch: boolean; + turnLeft: boolean; + turnRight: boolean; + } = { + changing: false, + forward: false, + back: false, + jump: false, + crouch: false, + turnLeft: false, + turnRight: false + }; + + constructor( ) { + + } + +} diff --git a/game/src/actors/pirate.ts b/game/src/actors/pirate.ts index a2d429f8..4e2b2d71 100644 --- a/game/src/actors/pirate.ts +++ b/game/src/actors/pirate.ts @@ -1,6 +1,6 @@ /** * Pirate NPC - * + * * Defines an aggressive pirate NPC that attacks nearby aircraft. */ @@ -16,11 +16,11 @@ export default class Pirate extends BaseActor { path; - pursue; + targets; constructor( mesh, scene ) { super( mesh, scene ); - + this.marker = document.querySelector('#map .marker-bot svg path'); if ( this.type == 'vehicle' ) { @@ -40,15 +40,11 @@ export default class Pirate extends BaseActor { this.path.add( new YUKA.Vector3( loopDistance, this.mesh.position.y, - loopDistance ) ); this.path.add( new YUKA.Vector3( - loopDistance, this.mesh.position.y, - loopDistance ) ); this.path.add( new YUKA.Vector3( - loopDistance, this.mesh.position.y, loopDistance ) ); - + this.follow = new YUKA.FollowPathBehavior( this.path ); this.entity.steering.add( this.follow ); - // @todo: v7 Figure out a way to signal this to happen without l. global object access - this.pursue = new YUKA.PursuitBehavior( l.current_scene.objects.player.mesh.userData.actor.entity, 1 ); - this.pursue.active = false; - this.entity.steering.add( this.pursue ); - + this.targets = new Map(); } } @@ -60,18 +56,30 @@ export default class Pirate extends BaseActor { } // @todo: v7 Figure out a way to signal this to happen without l. global object access - if ( this.entity.vision.visible( l.current_scene.objects.player.position ) === true ) { - this.pursue.active = true; - this.follow.active = false; - - if ( this.marker ) - this.marker.style.stroke = 'rgb( 255, 0, 0 )'; - } else { - this.pursue.active = false; - this.follow.active = true; - - if ( this.marker ) - this.marker.style.stroke = 'rgb( 255, 255, 0 )'; + for (let actor in l.scenograph.actors.getAll()) { + if ( this.targets.size < l.scenograph.actors.size ) { + this.targets.set(actor.name, actor); + let pursue = new YUKA.PursuitBehavior( actor.vehicle.mesh.userData.actor.entity, 1 ); + pursue.active = false; + this.entity.steering.add( pursue ); + } + + // @todo: v7 Figure out a way to signal this to happen without l. global object access + if ( this.entity.vision.visible( actor.vehicle.mesh.position ) === true ) { + this.pursue.active = true; + this.follow.active = false; + + if ( this.marker ) + this.marker.style.stroke = 'rgb( 255, 0, 0 )'; + } else { + this.pursue.active = false; + this.follow.active = true; + + if ( this.marker ) + this.marker.style.stroke = 'rgb( 255, 255, 0 )'; + } } + + } } diff --git a/game/src/actors/player.ts b/game/src/actors/player.ts index 5cbaafa7..71a3d48e 100644 --- a/game/src/actors/player.ts +++ b/game/src/actors/player.ts @@ -1,8 +1,8 @@ /** * Player Agent - * + * * Defines a player entity in the game world. - * + * * @todo: Remove this file if not used. */ @@ -14,7 +14,7 @@ export default class Player extends BaseActor { constructor( mesh, scene ) { super( mesh, scene ); - + if ( this.type == 'vehicle' ) { this.entity.position.z = this.mesh.position.z; this.entity.position.y = this.mesh.position.y; diff --git a/game/src/actors/player2.ts b/game/src/actors/player2.ts new file mode 100644 index 00000000..4c111a23 --- /dev/null +++ b/game/src/actors/player2.ts @@ -0,0 +1,32 @@ +/** + * Player Agent + * + * Defines a player entity in the game world. + */ + +import * as YUKA from 'yuka'; + +import ActorBase from './base2'; + +export default class ActorPlayer extends ActorBase { + + constructor( ) { + super( ); + + this.entity = new YUKA.GameEntity(); + + } + + /** + * Update hook. + * + * This method is called within the main game world update loop. + * + * @method update + * @memberof BaseActor + * @global + **/ + update(delta) { + this.entity.update(delta); + } +} diff --git a/game/src/helpers.ts b/game/src/helpers.ts index 6202a774..58e3ecfc 100644 --- a/game/src/helpers.ts +++ b/game/src/helpers.ts @@ -2,14 +2,57 @@ * Helpers */ + /** + * Change aircraft velocity based on current and what buttons are pushed by the player. + * + * @param currentVelocity + * @param increasePushed + * @param decreasePushed + */ + export function changeVelocity(stepIncrease: number, stepDecrease: number, currentVelocity: number, increasePushed: boolean, decreasePushed: boolean, increaseMax: number, decreaseMax: number, dragFactor: number): number { + let newVelocity = currentVelocity; + + if (increasePushed) { + + // Check if the change puts us over the max. + let tempVelocity = newVelocity - stepIncrease; + if (Math.abs(increaseMax) >= Math.abs(tempVelocity)) + newVelocity = tempVelocity; + } + else { + if (decreasePushed) { + + // Check if the change puts us over the max. + let tempVelocity = newVelocity + stepDecrease; + if (Math.abs(decreaseMax) >= tempVelocity) + newVelocity = tempVelocity; + } + else { + if (newVelocity != 0) { + + if (Math.abs(newVelocity) > 0.1) { + // Ease out the velocity exponentially to simulate drag + newVelocity *= dragFactor; + } + else { + newVelocity = 0; + } + } + + } + } + + return newVelocity; +} + /** * Get speed delta from ideal of 60FPS - * - * All in game speeds are calculated to 60 FPS, this function checks what the current delta is - * - * @param - * - * @returns + * + * All in game speeds are calculated to 60 FPS, this function checks what the current delta is + * + * @param + * + * @returns */ export function normaliseSpeedDelta(time_delta: number): number { return 1.; @@ -18,11 +61,11 @@ export function normaliseSpeedDelta(time_delta: number): number { /** * Ease out exponentially - * + * * @see https://easings.net/#easeOutExpo - * + * * @param x - * + * * @returns number eased out number */ export function easeOutExpo(x: number): number { @@ -31,11 +74,11 @@ export function easeOutExpo(x: number): number { /** * Ease in quadratrically - * + * * @see https://easings.net/#easeOutExpo - * + * * @param x - * + * * @returns number eased out number */ export function easeInQuad(x: number): number { @@ -44,11 +87,11 @@ export function easeInQuad(x: number): number { /** * Ease in and out expotentially - * + * * @see https://easings.net/#easeOutExpo - * + * * @param x - * + * * @returns number eased out number */ export function easeInOutExpo(x: number): number { @@ -58,4 +101,4 @@ export function easeInOutExpo(x: number): number { ? 1 : x < 0.5 ? Math.pow(2, 20 * x - 10) / 2 : (2 - Math.pow(2, -20 * x + 10)) / 2; -} \ No newline at end of file +} diff --git a/game/src/objects/aircraft/base.ts b/game/src/objects/aircraft/base.ts index 13741f22..c8e617c3 100644 --- a/game/src/objects/aircraft/base.ts +++ b/game/src/objects/aircraft/base.ts @@ -1,6 +1,6 @@ /** * Base Aircraft class - * + * * @todo: * - Add weight and wind resistance */ @@ -8,6 +8,7 @@ import { normaliseSpeedDelta, easeOutExpo, easeInQuad, easeInOutExpo } from '../../helpers'; export default class BaseAircraft { + public mesh; public score: { kills: number; deaths: number } = { kills: 0, deaths: 0 }; public standing: number = 0; public hitPoints: number = 100; @@ -43,38 +44,46 @@ export default class BaseAircraft { moveRight: false }; - constructor() { + constructor( mesh ) { + this.mesh = mesh; } public blowUp( meshPosition ) { let seed = Math.round(Math.random() * 10); - for ( var i = 0; i < seed; i++ ) { - let xOffset = 10 - Math.random() * 20; - let yOffset = 10 - Math.random() * 20; - let zOffset = 10 - Math.random() * 20; + if ( window.location.pathname == 'https://langenium.com' && l.config.settings.fast == false ) { + for ( var i = 0; i < seed; i++ ) { + let xOffset = 10 - Math.random() * 20; + let yOffset = 10 - Math.random() * 20; + let zOffset = 10 - Math.random() * 20; - let explosionPosition = meshPosition.clone(); - explosionPosition.x += xOffset; - explosionPosition.y += yOffset; - explosionPosition.z += zOffset; + let explosionPosition = meshPosition.clone(); + explosionPosition.x += xOffset; + explosionPosition.y += yOffset; + explosionPosition.z += zOffset; - setTimeout( () => { - l.current_scene.objects.projectiles.missile.loadExplosion( explosionPosition ); - }, 250 * Math.random() ) - + setTimeout( () => { + l.scenograph.objects.projectiles.missile.loadExplosion( explosionPosition ); + }, 250 * Math.random() ) + + } + } + else { + setTimeout( () => { + l.scenograph.objects.projectiles.missile.loadExplosion( meshPosition ); + }, 250 * Math.random() ) } }; /** * Damages the aircraft based on the incoming damage. - * + * * Returns the calculated final damage amount. - * - * @param damagePoints + * + * @param damagePoints * @param originMesh - * @returns + * @returns */ public damage( damagePoints, originMesh ): number { let targetDestroyed = false; @@ -111,12 +120,6 @@ export default class BaseAircraft { this.score.deaths += 1; originMesh.userData.object.score.kills += 1; - // Hide the scanner marker during respawn. - const scannerMarker = l.scenograph.overlays.scanners.trackedObjects[ this.mesh.uuid ]; - if ( scannerMarker ) - scannerMarker.style.display = 'none'; - - // Wait 3 seconds before 'respawn'. setTimeout( () => { // Reset hitpoints @@ -136,9 +139,6 @@ export default class BaseAircraft { this.mesh.userData.targetable = true; this.mesh.visible = true; - // Restore the scanner marker after respawn. - if ( scannerMarker ) - scannerMarker.style.display = 'block'; }, 3000 ); } @@ -151,10 +151,10 @@ export default class BaseAircraft { /** * Change aircraft velocity based on current and what buttons are pushed by the player. - * + * * @param currentVelocity - * @param increasePushed - * @param decreasePushed + * @param increasePushed + * @param decreasePushed */ private _changeVelocity(stepIncrease, stepDecrease, currentVelocity, increasePushed, decreasePushed, increaseMax, decreaseMax, dragFactor): number { let newVelocity = currentVelocity; @@ -185,7 +185,7 @@ export default class BaseAircraft { newVelocity = 0; } } - + } } @@ -194,13 +194,13 @@ export default class BaseAircraft { /** * Move the aircraft based on velocity, direction and time delta between frames. - * - * @param time_delta + * + * @param time_delta */ public move( time_delta: number ): object { let stepSize: number = .05 * normaliseSpeedDelta( time_delta ), - rY: number = 0, - tZ: number = 0, + rY: number = 0, + tZ: number = 0, tY: number = 0, radian: number = (Math.PI / 180); @@ -258,7 +258,7 @@ export default class BaseAircraft { ) { this.rotation.x *= .9; } - + if (rY != 0) { if (Math.abs(this.rotation.z) < Math.PI / 4) { this.rotation.z += rY / Math.PI; @@ -272,7 +272,7 @@ export default class BaseAircraft { let xDiff = tZ * Math.sin(this.rotation.y), zDiff = tZ * Math.cos(this.rotation.y); - + // "1" is the floor limit as it's the ocean surface and the camera clips through the water any lower. if (this.position.y + tY >= 1 ) { this.position.y += tY; @@ -284,7 +284,7 @@ export default class BaseAircraft { this.position.z += zDiff; return [ rY, tY, tZ ]; - + } } diff --git a/game/src/objects/aircraft/raven.ts b/game/src/objects/aircraft/raven.ts index c7c4555c..cb20fe78 100644 --- a/game/src/objects/aircraft/raven.ts +++ b/game/src/objects/aircraft/raven.ts @@ -11,8 +11,8 @@ class Raven extends BaseAircraft { public maxUp: number = 3.7 * 2.5; public maxDown: number = 3.7 * 5; // gravity? - constructor() { - super(); // Call the constructor of the base class + constructor( mesh ) { + super( mesh ); // Call the constructor of the base class } } diff --git a/game/src/objects/aircraft/valiant.ts b/game/src/objects/aircraft/valiant.ts index d4d608ad..ad75b337 100644 --- a/game/src/objects/aircraft/valiant.ts +++ b/game/src/objects/aircraft/valiant.ts @@ -6,8 +6,8 @@ import BaseAircraft from './base'; class Valiant extends BaseAircraft { - constructor() { - super(); // Call the constructor of the base class + constructor( mesh ) { + super( mesh ); // Call the constructor of the base class } } diff --git a/game/src/objects/base.ts b/game/src/objects/base.ts new file mode 100644 index 00000000..d52fd044 --- /dev/null +++ b/game/src/objects/base.ts @@ -0,0 +1,63 @@ +/** + * Base Object class + */ + +import { AABB, Vec3 } from '../types'; + +export default class ObjectBase { + + // World position and rotation. + public position: Vec3 = { x: 0, y: 0, z: 0 }; + public rotation: Vec3 = { x: 0, y: 0, z: 0 }; + + // Track next position (before collision check). + public nextPosition: Vec3 = { x: 0, y: 0, z: 0 }; + + // Movement deltas. + public rY: number = 0; + public tY: number = 0; + public tZ: number = 0; + + // Axis-Aligned Bounding Box. + public aabb: AABB = { + halfSize: { x: 0.5, y: 0.5, z: 0.5 } + }; + + // Whether to do a collision check. + public solid: boolean = false; + + constructor() { + } + + public getAABB() { + return { + min: { + x: this.position.x - this.aabb.halfSize.x, + y: this.position.y - this.aabb.halfSize.y, + z: this.position.z - this.aabb.halfSize.z + }, + max: { + x: this.position.x + this.aabb.halfSize.x, + y: this.position.y + this.aabb.halfSize.y, + z: this.position.z + this.aabb.halfSize.z + } + }; + } + + public getAABBNext() { + return { + min: { + x: this.nextPosition.x - this.aabb.halfSize.x, + y: this.nextPosition.y - this.aabb.halfSize.y, + z: this.nextPosition.z - this.aabb.halfSize.z + }, + max: { + x: this.nextPosition.x + this.aabb.halfSize.x, + y: this.nextPosition.y + this.aabb.halfSize.y, + z: this.nextPosition.z + this.aabb.halfSize.z + } + }; + } + + +} diff --git a/game/src/objects/person.ts b/game/src/objects/person.ts new file mode 100644 index 00000000..6e8fc02f --- /dev/null +++ b/game/src/objects/person.ts @@ -0,0 +1,179 @@ +/** + * Base Aircraft class + * + * @todo: + * - Add weight and wind resistance + */ + +import { normaliseSpeedDelta, easeOutExpo, easeInQuad, easeInOutExpo } from '../helpers'; + +export default class Person { + public score: { kills: number; deaths: number } = { kills: 0, deaths: 0 }; + public standing: number = 0; + public hitPoints: number = 100; + public airSpeed: number = 0; + public verticalSpeed: number = 0; + public maxForward: number = 16 / 60; // 8 km/h @ 60 FPS + public maxBackward: number = 16 / 60; + public maxUp: number = 4 / 60; + public maxDown: number = 16 / 60; // gravity? + + public position: { x: number; y: number; z: number } = { x: 0, y: 8.5, z: 0 }; + public startPosition: { x: number; y: number; z: number } = { x: 0, y: 8.5, z: 0 }; + public rotation: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 }; + + public controls: { + changing: boolean, + forward: boolean; + back: boolean; + jump: boolean; + crouch: boolean; + turnLeft: boolean; + turnRight: boolean; + } = { + changing: false, + forward: false, + back: false, + jump: false, + crouch: false, + turnLeft: false, + turnRight: false + }; + + constructor() { + } + + /** + * Change aircraft velocity based on current and what buttons are pushed by the player. + * + * @param currentVelocity + * @param increasePushed + * @param decreasePushed + */ + private _changeVelocity(stepIncrease, stepDecrease, currentVelocity, increasePushed, decreasePushed, increaseMax, decreaseMax, dragFactor): number { + let newVelocity = currentVelocity; + + if (increasePushed) { + + // Check if the change puts us over the max. + let tempVelocity = newVelocity - stepIncrease; + if (Math.abs(increaseMax) >= Math.abs(tempVelocity)) + newVelocity = tempVelocity; + } + else { + if (decreasePushed) { + + // Check if the change puts us over the max. + let tempVelocity = newVelocity + stepDecrease; + if (Math.abs(decreaseMax) >= tempVelocity) + newVelocity = tempVelocity; + } + else { + if (newVelocity != 0) { + + if (Math.abs(newVelocity) > 0.1) { + // Ease out the velocity exponentially to simulate drag + newVelocity *= dragFactor; + } + else { + newVelocity = 0; + } + } + + } + } + + return newVelocity; + } + + /** + * Move the aircraft based on velocity, direction and time delta between frames. + * + * @param time_delta + */ + public move( time_delta: number ): object { + let stepSize: number = .025 * normaliseSpeedDelta( time_delta ), + rY: number = 0, + tZ: number = 0, + tY: number = 0, + radian: number = - (Math.PI / 180) * stepSize * 100; + + if ( this.controls.forward || this.controls.back ){ + // Update Airspeed (horizontal velocity) + this.airSpeed = this._changeVelocity( + stepSize * easeInOutExpo( 1 - ( Math.abs ( this.airSpeed ) / this.maxForward ) ), + stepSize, + this.airSpeed, + this.controls.forward, + this.controls.back, + this.maxForward, + this.maxBackward, + easeOutExpo( 0.987 ) + ); + } + else { + this.airSpeed = 0; + } + + + // Update Vertical Speed (velocity) + this.verticalSpeed = this._changeVelocity( + stepSize * easeInOutExpo( 1 - ( Math.abs ( this.verticalSpeed ) / this.maxUp ) ), + stepSize * easeInOutExpo( 1 - ( Math.abs ( this.verticalSpeed ) / this.maxDown ) ), + this.verticalSpeed, + this.controls.crouch, // Note: Move Down/Up is reversed by design. + this.controls.jump, + this.maxDown, + this.maxUp, + easeInQuad( 0.321 ) + ); + + // Check the vertical speed exceeds minimum threshold for change in vertical position + if (Math.abs(this.verticalSpeed) > 0.01) { + tY = this.verticalSpeed; + } + + // Turning + if (this.controls.turnRight) { + rY += radian; + } + else { + if (this.controls.turnLeft) { + rY -= radian; + } + } + + // Check if we have significant airspeed + if (Math.abs(this.airSpeed) > 0.01) { + + // Set change in Z position based on airspeed + tZ = this.airSpeed; + + } + + if (rY != 0) { + if (Math.abs(this.rotation.z) < Math.PI / 4) { + this.rotation.z += rY / Math.PI; + } + + this.rotation.y += rY; + } + + let xDiff = tZ * Math.sin(this.rotation.y), + zDiff = tZ * Math.cos(this.rotation.y); + + // "1" is the floor limit as it's the ocean surface and the camera clips through the water any lower. + if (this.position.y + tY >= 1 ) { + this.position.y += tY; + } else { + this.verticalSpeed = 0; + } + + this.position.x += xDiff; + this.position.z += zDiff; + + return [ rY, tY, tZ ]; + + } + +} diff --git a/game/src/objects/person2.ts b/game/src/objects/person2.ts new file mode 100644 index 00000000..a804f7b0 --- /dev/null +++ b/game/src/objects/person2.ts @@ -0,0 +1,125 @@ +/** + * Person class + */ + +import ObjectBase from './base'; +import BaseActor from '../actors/base2'; +import { changeVelocity, normaliseSpeedDelta, easeOutExpo, easeInQuad, easeInOutExpo } from '../helpers'; +import { Vec3 } from '../types'; + + +export default class Person extends ObjectBase { + + // Actor that controls this object. + public actor?: BaseActor; + + // Object world parameters + public hitPoints: number = 100; + public airSpeed: number = 0; + public verticalSpeed: number = 0; + public maxForward: number = 16 / 60; // 8 km/h @ 60 FPS + public maxBackward: number = 16 / 60; + public maxUp: number = 4 / 60; + public maxDown: number = 16 / 60; // gravity? + + constructor() { + super(); + this.aabb = { + halfSize: { x: 0.5, y: 0.75, z: 0.3 } + }; + } + + update( time_delta: number ) { + if (!this.actor) return; + this.nextPosition = { ...this.position }; // start with current position + + let stepSize: number = .025 * normaliseSpeedDelta( time_delta ), + rY: number = 0, + tZ: number = 0, + tY: number = 0, + radian: number = - (Math.PI / 180) * stepSize * 100; + + if ( this.actor.controls.forward || this.actor.controls.back ){ + // Update Airspeed (horizontal velocity) + this.airSpeed = changeVelocity( + stepSize * easeInOutExpo( 1 - ( Math.abs ( this.airSpeed ) / this.maxForward ) ), + stepSize, + this.airSpeed, + this.actor.controls.forward, + this.actor.controls.back, + this.maxForward, + this.maxBackward, + easeOutExpo( 0.987 ) + ); + } + else { + this.airSpeed = 0; + } + + // Update Vertical Speed (velocity) + this.verticalSpeed = changeVelocity( + stepSize * easeInOutExpo( 1 - ( Math.abs ( this.verticalSpeed ) / this.maxUp ) ), + stepSize * easeInOutExpo( 1 - ( Math.abs ( this.verticalSpeed ) / this.maxDown ) ), + this.verticalSpeed, + this.actor.controls.crouch, // Note: Move Down/Up is reversed by design. + this.actor.controls.jump, + this.maxDown, + this.maxUp, + easeInQuad( 0.321 ) + ); + + // Check the vertical speed exceeds minimum threshold for change in vertical position + if (Math.abs(this.verticalSpeed) > 0.01) { + tY = this.verticalSpeed; + } + + // Turning + if (this.actor.controls.turnRight) { + rY += radian; + } + else { + if (this.actor.controls.turnLeft) { + rY -= radian; + } + } + + // Check if we have significant airspeed + if (Math.abs(this.airSpeed) > 0.01) { + + // Set change in Z position based on airspeed + tZ = this.airSpeed; + + } + + if (rY != 0) { + if (Math.abs(this.rotation.z) < Math.PI / 4) { + this.rotation.z += rY / Math.PI; + } + + this.rotation.y += rY; + } + + let xDiff = tZ * Math.sin(this.rotation.y), + zDiff = tZ * Math.cos(this.rotation.y); + + // "1" is the floor limit as it's the ocean surface and the camera clips through the water any lower. + if (this.nextPosition.y + tY >= 1 ) { + this.nextPosition.y += tY; + } else { + this.verticalSpeed = 0; + } + + this.nextPosition.x += xDiff; + this.nextPosition.z += zDiff; + + this.rY = rY; + this.tY = tY; + this.tZ = tZ; + + } + + commitNextPosition() { + this.position = this.nextPosition; + } + +} diff --git a/game/src/objects/structures/hangar.ts b/game/src/objects/structures/hangar.ts new file mode 100644 index 00000000..bdb702bd --- /dev/null +++ b/game/src/objects/structures/hangar.ts @@ -0,0 +1,136 @@ +/** + * Aircraft hangar. + */ + +import ObjectBase from '../base'; + +import { AABB } from '../types'; + +class Hangar extends ObjectBase { + + // axis aligned bounding box + public aabb: AABB; + + // Hangar component configuration. + public design: any; + + constructor(config = {}) { + super(); // Call the constructor of the base class + + if (config.design) { + this.design = this.getDesign(config.design); + } else { + this.design = this.getDesign(); + } + if (config.position) { + this.position.x = config.position.x; + this.position.y = config.position.y; + this.position.z = config.position.z; + } + if (config.rotation) { + this.rotation.x = config.rotation.x; + this.rotation.y = config.rotation.y; + this.rotation.z = config.rotation.z; + } + + this.aabb = this.getComponentAABBs(); + } + + detectCollision () { + } + + getDesign( designIndex = 0 ) { + const designs = [ + { + name: 'Bay with quarters', + size: 5, + components: [ + { + name: 'Main Bay', + width: 10, + height: 5, + depth: 10, + position: { + x: 0, + y: 0, + z: 0 + }, + rotation: { + x: 0, + y: 0, + z: 0 + } + }, + { + name: 'Quarters', + width: 5, + height: 2.5, + depth: 5, + position: { + x: -8.375, + y: -1.25, + z: -2.5 + }, + rotation: { + x: 0, + y: 0, + z: 0 + } + }, + { + name: 'Corridor', + width: 2.5, + height: 2.55, + depth: 1.25, + position: { + x: -5, + y: -1.225, + z: -1.25 + }, + rotation: { + x: 0, + y: 0, // 90 degrees or half pi + z: 0 + } + }, + ] + } + ]; + return designs[designIndex]; + } + + /** + * Returns world-space AABBs for all solid components + */ + public getComponentAABBs(): AABB[] { + return this.design.components.map(component => { + const halfSize = { + x: component.width, + y: component.height, + z: component.depth + }; + + const worldPos = { + x: this.position.x + component.position.x * 2.5, + y: this.position.y + component.position.y * 2.5, + z: this.position.z + component.position.z * 2.5 + }; + + return { + min: { + x: worldPos.x - halfSize.x, + y: worldPos.y - halfSize.y, + z: worldPos.z - halfSize.z + }, + max: { + x: worldPos.x + halfSize.x, + y: worldPos.y + halfSize.y, + z: worldPos.z + halfSize.z + } + }; + }); + } + +} + +module.exports = Hangar; diff --git a/game/src/scenes/overworld.yml b/game/src/scenes/overworld.yml new file mode 100644 index 00000000..e7382a6d --- /dev/null +++ b/game/src/scenes/overworld.yml @@ -0,0 +1,126 @@ +# Dynamic Objects +actors: + - name: Capy One + model: cargoShip + class: cargoShip + position: + x: -35000 + y: -2000 + z: 10000 + - name: Capy Two + model: cargoShip + class: cargoShip + position: + x: -36000 + y: -1500 + z: 10000 + - name: Capy Three + model: cargoShip + class: cargoShip + position: + x: -34000 + y: -1500 + z: 10000 + - name: Player One + model: valiant + class: player + position: + x: -65000 + y: -35000 + z: 1500 + rotation: + x: 0 + y: -12.56 + z: 0 + - name: Player Two + model: person + class: player + hangar: + structure: Lambda City + hangarName: Hangar 1 + position: + x: -65000 + y: -35000 + z: 1500 + rotation: + x: 0 + y: -12.56 + z: 0 + - name: Pirate One + model: raven + class: pirate + position: + x: -65000 + y: -35000 + z: 1500 + rotation: + x: 0 + y: -12.56 + z: 0 +# Static Objects +objects: + - name: Einstein Well + model: extractor + position: + x: 0 + y: -7450 + z: -70000 + - name: Newton Well + model: extractor + position: + x: 0 + y: -7450 + z: 70000 + - name: Galileo Well + model: extractor + position: + x: -70000 + y: -7450 + z: 0 + - name: Planck Well + model: extractor + position: + x: 70000 + y: -7450 + z: 0 + - name: Lambda City + model: platform + position: + x: -35000 + y: 1500 + z: -65000 + rotation: + x: 0 + y: -12.56 + z: 0 + hangars: + - name: Hangar 1 + position: + x: 0 + y: 6100 + z: -8620 + rotation: + x: 0 + y: 0 + z: 0 + - name: Hangar 2 + position: + x: 0 + y: 505 + z: 0 + rotation: + x: 0 + y: 3.14159 + z: 0 + - name: Refinery 91 + model: refinery + position: + x: 20000 + y: 153 + z: -20000 + - name: Refinery 92 + model: refinery + position: + x: -20000 + y: 153 + z: 20000 diff --git a/game/src/systems/weapons.ts b/game/src/systems/weapons.ts index 2aa7d83c..a0af9edc 100644 --- a/game/src/systems/weapons.ts +++ b/game/src/systems/weapons.ts @@ -63,7 +63,7 @@ export default class Weapons extends BaseSystem { // @todo: v7 Figure out a way to signal this to happen without l. global object access this.last = l.current_scene.stats.currentTime; - l.current_scene.objects.projectiles.missile.fireMissile( + l.scenograph.objects.projectiles.missile.fireMissile( this.mesh, this.mesh.position, target.mesh, diff --git a/game/src/types.ts b/game/src/types.ts new file mode 100644 index 00000000..835b7154 --- /dev/null +++ b/game/src/types.ts @@ -0,0 +1,5 @@ +export type Vec3 = { x: number; y: number; z: number }; + +export interface AABB { + halfSize: Vec3; +} diff --git a/game/src/world.ts b/game/src/world.ts new file mode 100644 index 00000000..3c329cb9 --- /dev/null +++ b/game/src/world.ts @@ -0,0 +1,225 @@ +/** + * Game World class + * + * Loads and runs simulations of game scenes + * + */ +// @todo: allow dynamic loading of other scenes. +import Overworld from "./scenes/overworld.yml"; + +import ActorPlayer from './actors/player2'; + +import ObjectHangar from './objects/structures/hangar'; +import ObjectPerson from './objects/person2'; + +interface WorldConfig { + actors: Record; + objects: Record; +} + +interface WorldInstance { + actors: Record; + objects: Record; +} + +export default class World { + + public config: WorldConfig; + public instance: WorldInstance; + + private accumulator = 0; + private running = false; + + constructor( sceneName: string ) { + if ( sceneName === 'Overworld' ) { + this.initialise( Overworld ); + } + } + + /** + * Initialise game world instance. + * + * Parses config and builds a self updating virtual world simulation. + * + * @todo: + * - load abstract scene definition file overworld.yml and parse it + * - loop over config to load game world simulation in here and scenograph in the client + */ + initialise( config ) { + this.config = { + actors: config.actors, + objects: config.objects + } + this.instance = { + actors: new Map(), + objects: new Map() + }; + + this.lastUpdateTime = performance.now(); + this.fixedDelta = 16; // ~60 FPS for logic + + + this.load(); + this.start(); + + } + + load() { + // Load actors and their attached objects. + for (const actorConfig of this.config.actors.values()) { + const actorInstance: any = { config: actorConfig }; + // Set actor class first. + actorInstance.actor = this.loadActor(actorConfig.class); + + // Set object class. + actorInstance.object = this.loadObject(actorConfig); + + // Set objects actor properties to actor. + if ( actorInstance.actor && actorInstance.object ) { + actorInstance.object.actor = actorInstance.actor; + } + + this.instance.actors.set(actorConfig.name, actorInstance); + } + + // Load static objects into the world. + for (const objectConfig of this.config.objects.values()) { + const objectInstance: any = { config: objectConfig }; + + // Set object class. + objectInstance.object = this.loadObject(objectConfig); + + if (objectConfig.hangars){ + objectInstance.hangars = []; + objectConfig.hangars.forEach(hangarConfig => { + // Merge hangar position with world position; + hangarConfig.position = { + x: hangarConfig.position.x + objectConfig.position.x, + y: hangarConfig.position.y, // skip this one as the y offset is model specific. + z: hangarConfig.position.z + objectConfig.position.z + }; + const hangarInstance: any = { config: hangarConfig }; + hangarInstance.object = this.loadObject({ + ...hangarConfig, + model: 'hangar' + }); + objectInstance.hangars.push(hangarInstance); + }); + } + + this.instance.objects.set(objectConfig.name, objectInstance); + } + } + + loadActor(actorClass: string) { + if (actorClass == 'player') { + return new ActorPlayer(); + } + else { + return false; + } + } + + loadObject(config: any) { + if (config.model == 'person') { + return new ObjectPerson(); + } + else if (config.model == 'hangar') { + return new ObjectHangar(config); + } + else { + return false; + } + } + + start() { + this.running = true; + this.lastUpdateTime = performance.now(); + + const loop = () => { + if (!this.running) return; + + const now = performance.now(); + const frameTime = now - this.lastUpdateTime; + this.lastUpdateTime = now; + + // Prevent spiral of death + this.accumulator += Math.min(frameTime, 250); + + while (this.accumulator >= this.fixedDelta) { + this.update(); + this.accumulator -= this.fixedDelta; + } + + setTimeout(loop, 0); + }; + + loop(); + } + + stop() { + this.running = false; + } + + update() { + for (const actorInstance of this.instance.actors.values()) { + if (actorInstance.actor) { + actorInstance.actor.update(this.fixedDelta); + } + if (actorInstance.object) { + actorInstance.object.update(this.fixedDelta); + if (actorInstance.actor.controls.forward || actorInstance.actor.controls.back) { + this.checkHangarCollisions(actorInstance); + } + } + } + } + + checkHangarCollisions(actorInstance: any) { + // Get object's proposed AABB at next position + const actorAABB = actorInstance.object.getAABBNext(); + + // Ensure the user is still inside the hangar area. + let inside = false; + if (actorInstance.config.hangar) { + this.instance.objects.forEach((objectInstance, objectName) => { + if (objectName == actorInstance.config.hangar.structure) { + if (objectInstance.hangars) { + objectInstance.hangars.forEach((hangarInstance) => { + if (hangarInstance.config.name == actorInstance.config.hangar.hangarName) { + + for (const componentAABB of hangarInstance.object.aabb ) { + if (this.aabbContained(actorAABB, componentAABB)) { + inside = true; + break; + } + } + + } + }); + } + + } + }); + + } + if (inside) { + actorInstance.object.commitNextPosition(); + } + + } + + // Checks if actor Bounding Box is within the component Bounding Box + aabbContained(actorBounds, componentBounds) { + let offset = 1.25; + return ( + actorBounds.min.x >= componentBounds.min.x - offset && + actorBounds.max.x <= componentBounds.max.x + offset && + actorBounds.min.y >= componentBounds.min.y - offset && + actorBounds.max.y <= componentBounds.max.y + offset && + actorBounds.min.z >= componentBounds.min.z - offset && + actorBounds.max.z <= componentBounds.max.z + offset + ); + } + +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..c9333b9d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "langenium.com", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}