├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── app.js ├── package-lock.json ├── package.json └── public ├── assets ├── GROBOLD.ttf ├── action.png ├── base.png ├── cross.png ├── loading.gif ├── matrix.gif ├── models │ ├── hero-texture-0.png │ ├── hero-texture-1.png │ ├── hero-texture-2.png │ ├── hero-texture-3.png │ └── new.jpg ├── redo-alt-solid.svg ├── stamina-backgorund.png ├── stick.png └── tile-gizmo.png ├── index.html ├── js ├── AssetManager.js ├── CameraControl.js ├── DynamicItems.js ├── Easing.js ├── Interaction.js ├── MapManager.js ├── Optimizer.js ├── SocketIO.js ├── Stamina.js ├── Utils.js ├── atlas.js ├── charaAnim.js ├── controler.js ├── gameState.js ├── init.js ├── input.js ├── loop.js ├── main.js └── soundMixer.js └── libs ├── CopyShader.js ├── DRACOLoader.js ├── EffectComposer.js ├── FXAAShader.js ├── GLTFLoader.js ├── RenderPass.js ├── ShaderPass.js ├── SkeletonUtils.js ├── TransformControls.js ├── draco ├── draco_decoder.js ├── draco_decoder.wasm ├── draco_encoder.js └── draco_wasm_wrapper.js ├── lzjs.js ├── socket.io.js ├── three.js ├── ua-parser.min.js └── virtualjoystick.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web:node app.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Edelweiss 2 | 3 | Open source webGL game made with three.js 4 | 5 | ## Play online here : http://edelweiss.32x.io 6 | 7 | ![Screenshot of Edelweiss](https://felixmariotto.s3.eu-west-3.amazonaws.com/new_teaser_github1.gif) 8 | 9 | # How it works 10 | 11 | **Custom physics engine** 12 | The game works with a simplistic physics engine based on axis-aligned bounding box collision detection. All the physic objects in this game are either boxes or axis-aligned square tiles. [Find the code here.](https://github.com/felixmariotto/Edelweiss/blob/master/public/js/controler.js) 13 | 14 | **Custom map editor** 15 | The information about the physical map is contained in an JSON called sceneGraph that the game loads on statup. I created this file using a custom map editor, that I coding for the sole purpose of making this game. [Find the code here in a separate repository.](https://github.com/felixmariotto/Edelweiss-Editor) 16 | 17 | **Manual camera positioning** 18 | Moving the camera to support a 3D platformer game is a challenge, that I had to face on my own since I used a custom physics engine. [Find the code here.](https://github.com/felixmariotto/Edelweiss/blob/master/public/js/CameraControl.js) 19 | 20 | **Automatic optimization** 21 | The game is playable from mid-range mobile to high-range desktop. To support this adaptability, the game adapt itself to the device capability at runtime. [Find the code here](https://github.com/felixmariotto/Edelweiss/blob/master/public/js/Optimizer.js) 22 | 23 | **Runtime assets loading** 24 | This game is light, but I still wanted to optimize loading time by loading map tiles at runtime. [Find the code here](https://github.com/felixmariotto/Edelweiss/blob/master/public/js/MapManager.js) 25 | 26 | # More open source games 27 | 28 | Check out my previous game [The Temple of Doom](https://github.com/felixmariotto/Temple_Of_Doom) 29 | 30 | Follow me on Github or Itch.io for more upcoming games 31 | 32 | # Contributing 33 | 34 | If you fancy extending the game, I'm up to merge your work, as I already did with Makc multiplayer and debug mods. 35 | 36 | If you feel more like correcting a few bugs here and there, here is a wishlist : 37 | - fix hero climb-down-right animation 38 | - if the player dies, got the first edelweiss, but didn't save their progress yet, respawn them in front of the first cave instead of the starting point of the game 39 | - add movement sounds (climbing, footstep...) 40 | - add powerup sounds 41 | - add tweening for the reduction of the stamina bar 42 | - add dust animation when player fall because of a fall-tile 43 | - fix issue of joystick position with Galaxy S9+ (chrome for android) 44 | 45 | # Big thanks to [Makc](https://github.com/makc) for contributing ! 46 | 47 | He made : 48 | - The multiplayer feature 49 | - The debug and live-map-editing features, that you can access by typing these commands into your browser's console while playing the game : 50 | ```javascript 51 | atlas.debug() // show the physical assets used for collision while you play, and UI for editing the map 52 | atlas.player.showHelpers() // show a helper for the player's direction 53 | controler.permissions.airborne // cheat code for flying freely with the glider 54 | ``` 55 | 56 | You can find his fork here : https://github.com/makc/Edelweiss 57 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express'); 3 | const path = require('path'); 4 | const socketIO = require('socket.io'); 5 | const PORT = process.env.PORT || 5050; 6 | 7 | 8 | /////////// 9 | /// APP 10 | /////////// 11 | 12 | const app = express() 13 | 14 | .use(express.static('public')) 15 | 16 | .get('/', (req, res)=> { 17 | res.sendFile(path.join(__dirname + '/public/index.html')); 18 | }) 19 | 20 | .listen(PORT, ()=> { 21 | console.log('App listening on port ' + PORT); 22 | }) 23 | 24 | 25 | ////////////////// 26 | /// SOCKET.IO 27 | ////////////////// 28 | 29 | const io = socketIO( app ); 30 | 31 | io.on( 'connection', async (client)=> { 32 | 33 | client.on('playerInfo', (message)=> { 34 | 35 | // join the room with the requested game name 36 | io.sockets.sockets[ client.id ].join( message.pass ); 37 | 38 | client.roomId = message.pass ; 39 | 40 | // record the ID created on client side. 41 | // when the client quit, its game ID will be broadcasted to 42 | // every other player in the same room. 43 | client.gameId = message.id ; 44 | 45 | // broadcast player position to every player in the same socket io "room" 46 | client.broadcast.to( message.pass ).emit( 'playerInfo', message ); 47 | 48 | }); 49 | 50 | // 51 | 52 | client.on( 'disconnect', async ()=> { 53 | 54 | // broadcast the disconnection information to the other 55 | // players of the same room 56 | if ( client.roomId ) { 57 | 58 | client.broadcast.to( client.roomId ).emit( 'playerLeft', client.gameId ); 59 | 60 | }; 61 | 62 | }); 63 | 64 | }) 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edelweiss", 3 | "version": "0.0.0", 4 | "description": "platformer game", 5 | "main": "app.js", 6 | "engines": { 7 | "node": "12.x" 8 | }, 9 | "scripts": { 10 | "start": "node app.js", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/felixmariotto/Edelweiss.git" 16 | }, 17 | "keywords": [ 18 | "platform", 19 | "game", 20 | "mountain", 21 | "switzerland", 22 | "edelweiss" 23 | ], 24 | "author": "felix mariotto", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/felixmariotto/Edelweiss/issues" 28 | }, 29 | "homepage": "https://github.com/felixmariotto/Edelweiss#readme", 30 | "dependencies": { 31 | "express": "^4.17.1", 32 | "socket.io": "^2.4.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/assets/GROBOLD.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/Edelweiss/4181238d5a50011309f7be714ffe60105d06dd9b/public/assets/GROBOLD.ttf -------------------------------------------------------------------------------- /public/assets/action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/Edelweiss/4181238d5a50011309f7be714ffe60105d06dd9b/public/assets/action.png -------------------------------------------------------------------------------- /public/assets/base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/Edelweiss/4181238d5a50011309f7be714ffe60105d06dd9b/public/assets/base.png -------------------------------------------------------------------------------- /public/assets/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/Edelweiss/4181238d5a50011309f7be714ffe60105d06dd9b/public/assets/cross.png -------------------------------------------------------------------------------- /public/assets/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/Edelweiss/4181238d5a50011309f7be714ffe60105d06dd9b/public/assets/loading.gif -------------------------------------------------------------------------------- /public/assets/matrix.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/Edelweiss/4181238d5a50011309f7be714ffe60105d06dd9b/public/assets/matrix.gif -------------------------------------------------------------------------------- /public/assets/models/hero-texture-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/Edelweiss/4181238d5a50011309f7be714ffe60105d06dd9b/public/assets/models/hero-texture-0.png -------------------------------------------------------------------------------- /public/assets/models/hero-texture-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/Edelweiss/4181238d5a50011309f7be714ffe60105d06dd9b/public/assets/models/hero-texture-1.png -------------------------------------------------------------------------------- /public/assets/models/hero-texture-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/Edelweiss/4181238d5a50011309f7be714ffe60105d06dd9b/public/assets/models/hero-texture-2.png -------------------------------------------------------------------------------- /public/assets/models/hero-texture-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/Edelweiss/4181238d5a50011309f7be714ffe60105d06dd9b/public/assets/models/hero-texture-3.png -------------------------------------------------------------------------------- /public/assets/models/new.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/Edelweiss/4181238d5a50011309f7be714ffe60105d06dd9b/public/assets/models/new.jpg -------------------------------------------------------------------------------- /public/assets/redo-alt-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/assets/stamina-backgorund.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/Edelweiss/4181238d5a50011309f7be714ffe60105d06dd9b/public/assets/stamina-backgorund.png -------------------------------------------------------------------------------- /public/assets/stick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/Edelweiss/4181238d5a50011309f7be714ffe60105d06dd9b/public/assets/stick.png -------------------------------------------------------------------------------- /public/assets/tile-gizmo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/Edelweiss/4181238d5a50011309f7be714ffe60105d06dd9b/public/assets/tile-gizmo.png -------------------------------------------------------------------------------- /public/js/AssetManager.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | AssetManager keeps track of all the special assets like animated NPCs and bonuses. 4 | At initialisation, it create groups that will hold the loaded assets once loading is done. 5 | AssetManager is able to hide/show the groups when gameState tells it to change of graph. 6 | */ 7 | 8 | function AssetManager() { 9 | 10 | // assets constants 11 | const SCALE_ALPINIST = 0.1 ; 12 | const SCALE_LADY = 0.08 ; 13 | const SCALE_CHAR = 0.075 ; 14 | const SCALE_EDELWEISS = 0.02 ; 15 | 16 | const OFFSET_EDELWEISS = new THREE.Vector3( 0, 0.1, 0 ); 17 | 18 | const particleMaterial = new THREE.MeshBasicMaterial({ color:0xffffff }); 19 | 20 | // What graph the player is currently playing in ? 21 | var currentGraph = 'mountain' ; 22 | 23 | var charGlb; 24 | 25 | // will be used to add a label at the top of the hero if multiplayer 26 | const textCanvas = document.createElement( 'canvas' ); 27 | textCanvas.height = 34; 28 | 29 | // Hold one mixer and one action per asset iteration 30 | var alpinistMixers = [], alpinistIdles = []; 31 | var ladyMixers = [], ladyIdles = []; 32 | var charMixers = [], charActions = []; 33 | 34 | // Asset groups arrays 35 | var alpinists = []; 36 | var edelweisses = []; 37 | var ladies = []; 38 | var bonuses = []; 39 | var characters = []; 40 | 41 | // different sets of color for the hero character, 42 | // for multiplayer differentiation. 43 | var charSkins = [ 44 | textureLoader.load( 'assets/models/hero-texture-0.png' ), 45 | textureLoader.load( 'assets/models/hero-texture-1.png' ), 46 | textureLoader.load( 'assets/models/hero-texture-2.png' ), 47 | textureLoader.load( 'assets/models/hero-texture-3.png' ) 48 | ]; 49 | 50 | ////////////// 51 | /// INIT 52 | ////////////// 53 | 54 | // Create one group per iteration, before the assets is loaded/created 55 | addGroups( alpinists, 10 ); 56 | addGroups( edelweisses, 7 ); 57 | addGroups( ladies, 12 ); 58 | addGroups( bonuses, 9 ); 59 | addGroups( characters, 4 ); 60 | 61 | function addGroups( arr, groupsNumber ) { 62 | 63 | for ( let i = 0 ; i < groupsNumber ; i++ ) { 64 | 65 | let group = new THREE.Group(); 66 | 67 | if ( arr == bonuses || 68 | arr == edelweisses ) { 69 | 70 | addParticles( group ); 71 | 72 | }; 73 | 74 | if ( arr == bonuses ) { 75 | 76 | let bonus = new THREE.Mesh( 77 | new THREE.ConeBufferGeometry( 0.1, 0.20, 4 ), 78 | particleMaterial 79 | ); 80 | 81 | let bonus2 = new THREE.Mesh( 82 | new THREE.ConeBufferGeometry( 0.1, 0.2, 4 ), 83 | particleMaterial 84 | ); 85 | 86 | bonus.position.y = 0.1 ; 87 | bonus2.position.y = - 0.1 ; 88 | bonus2.rotation.x = Math.PI ; 89 | 90 | group.add( bonus, bonus2 ); 91 | 92 | }; 93 | 94 | arr.push( group ); 95 | 96 | }; 97 | 98 | }; 99 | 100 | // create animation of little balls spinning around bonuses 101 | function addParticles( group ) { 102 | 103 | for ( let i = 0 ; i < 26 ; i ++ ) { 104 | 105 | let particle = new THREE.Mesh( 106 | new THREE.SphereBufferGeometry( 0.03, 4, 3 ), 107 | particleMaterial 108 | ); 109 | 110 | let particleGroup = new THREE.Group(); 111 | 112 | let yOffset = Math.random() ; 113 | 114 | particle.position.y += ( yOffset * 1.7 ) - 0.3 ; 115 | particle.position.x += ( Math.random() * 0.1 ) + ( ( 1 - yOffset ) * 0.2 ) + 0.1 ; 116 | 117 | particle.scale.setScalar( (1 - yOffset) + 0.1 ); 118 | 119 | particleGroup.rotation.y = Math.random() * ( Math.PI * 2 ); 120 | particleGroup.userData.rotationSpeed = ( Math.random() * 0.1 ) + 0.02 ; 121 | 122 | particleGroup.add( particle ); 123 | group.add( particleGroup ); 124 | 125 | }; 126 | 127 | }; 128 | 129 | //// ASSETS LOADING ///// 130 | 131 | gltfLoader.load('https://edelweiss-game.s3.eu-west-3.amazonaws.com/models/alpinist.glb', (glb)=> { 132 | 133 | createMultipleModels( 134 | glb, 135 | SCALE_ALPINIST, 136 | null, 137 | alpinists, 138 | alpinistMixers, 139 | alpinistIdles 140 | ); 141 | 142 | }); 143 | 144 | gltfLoader.load('https://edelweiss-game.s3.eu-west-3.amazonaws.com/models/lady.glb', (glb)=> { 145 | 146 | createMultipleModels( 147 | glb, 148 | SCALE_LADY, 149 | null, 150 | ladies, 151 | ladyMixers, 152 | ladyIdles 153 | ); 154 | 155 | }); 156 | 157 | gltfLoader.load('https://edelweiss-game.s3.eu-west-3.amazonaws.com/models/edelweiss.glb', (glb)=> { 158 | 159 | createMultipleModels( 160 | glb, 161 | SCALE_EDELWEISS, 162 | OFFSET_EDELWEISS, 163 | edelweisses, 164 | null, 165 | null, 166 | true 167 | ); 168 | 169 | }); 170 | 171 | gltfLoader.load('https://edelweiss-game.s3.eu-west-3.amazonaws.com/hero.glb', (glb)=> { 172 | 173 | charGlb = glb; 174 | 175 | createMultipleModels( 176 | glb, 177 | SCALE_CHAR, 178 | null, 179 | characters, 180 | charMixers, 181 | charActions 182 | ); 183 | 184 | }); 185 | 186 | // Create iterations of the same loaded asset. nasty because of skeletons. 187 | // Hopefully THREE.SkeletonUtils.clone() is able to clone skeletons correctly. 188 | function createMultipleModels( glb, scale, offset, modelsArr, mixers, actions, lightEmissive ) { 189 | 190 | glb.scene.scale.set( scale, scale, scale ); 191 | if ( offset ) glb.scene.position.add( offset ); 192 | 193 | for ( let i = mixers ? mixers.length : 0 ; i < modelsArr.length ; i++ ) { 194 | 195 | let newModel = THREE.SkeletonUtils.clone( glb.scene ); 196 | 197 | modelsArr[ i ].add( newModel ); 198 | 199 | if ( mixers ) { 200 | 201 | mixers[ i ] = new THREE.AnimationMixer( newModel ); 202 | 203 | actions[ i ] = {}; 204 | for ( let clip of glb.animations ) { 205 | actions[ i ][ clip.name ] = mixers[ i ].clipAction( clip ).play(); 206 | }; 207 | 208 | }; 209 | 210 | setLambert( newModel, lightEmissive !== undefined ); 211 | 212 | }; 213 | 214 | }; 215 | 216 | // Create a label at the top of the hero characters head, 217 | // for multiplayer differentiation 218 | function createCharacterLabel( text ) { 219 | 220 | const ctx = textCanvas.getContext( '2d' ); 221 | const font = '24px grobold'; 222 | 223 | ctx.font = font; 224 | textCanvas.width = Math.ceil( ctx.measureText( text ).width + 16 ); 225 | 226 | ctx.font = font; 227 | ctx.strokeStyle = '#222'; 228 | ctx.lineWidth = 8; 229 | ctx.lineJoin = 'miter'; 230 | ctx.miterLimit = 3; 231 | ctx.strokeText( text, 8, 26 ); 232 | ctx.fillStyle = 'white'; 233 | ctx.fillText( text, 8, 26 ); 234 | 235 | const spriteMap = new THREE.Texture( ctx.getImageData( 0, 0, textCanvas.width, textCanvas.height ) ); 236 | spriteMap.minFilter = THREE.LinearFilter; 237 | spriteMap.generateMipmaps = false; 238 | spriteMap.needsUpdate = true; 239 | 240 | const sprite = new THREE.Sprite( new THREE.SpriteMaterial( { map: spriteMap } ) ); 241 | sprite.scale.set( 0.12 * textCanvas.width / textCanvas.height, 0.12, 1 ); 242 | sprite.position.y = 0.7 ; 243 | 244 | return sprite; 245 | 246 | }; 247 | 248 | // 249 | 250 | function createCharacter( skinIndex, displayName ) { 251 | 252 | for ( let i = 0; i < characters.length; i++ ) { 253 | 254 | if ( !characters[ i ].userData.isUsed ) { 255 | 256 | characters[ i ].userData.isUsed = true; 257 | 258 | // assign character skin 259 | let skin = charSkins[ skinIndex % charSkins.length ]; 260 | if( skin ) { 261 | let body = characters[ i ].getObjectByName( 'hero001' ); 262 | if( body ) { 263 | body.material.map = skin; 264 | }; 265 | }; 266 | 267 | // set up charater display name 268 | if( displayName ) { 269 | characters[ i ].add( createCharacterLabel( displayName ) ); 270 | }; 271 | 272 | // return both the character and its actions 273 | return { 274 | model : characters[ i ], actions : charActions[ i ] 275 | }; 276 | }; 277 | 278 | }; 279 | 280 | // if here, we have exhausted all the characters - make some more 281 | 282 | addGroups( characters, 2 ); 283 | 284 | createMultipleModels( 285 | charGlb, 286 | SCALE_CHAR, 287 | null, 288 | characters, 289 | charMixers, 290 | charActions 291 | ); 292 | 293 | return createCharacter( skinIndex, displayName ); 294 | }; 295 | 296 | // 297 | 298 | function releaseCharacter( model ) { 299 | 300 | model.userData.isUsed = false; 301 | 302 | const label = model.getObjectByProperty( 'type', 'Sprite' ); 303 | if ( label ) model.remove( label ) && label.material.map.dispose(); 304 | 305 | }; 306 | 307 | // 308 | 309 | function toggleCharacterShadows( enabled ) { 310 | 311 | for ( let character of characters ) { 312 | 313 | character.traverse( function (child) { 314 | 315 | if ( child.type == 'Mesh' || 316 | child.type == 'SkinnedMesh' ) { 317 | 318 | child.castShadow = enabled ; 319 | child.receiveShadow = enabled ; 320 | }; 321 | 322 | }); 323 | 324 | }; 325 | 326 | }; 327 | 328 | ///////////////////// 329 | /// INSTANCES SETUP 330 | ///////////////////// 331 | 332 | // methods called by atlas when it loads cubes with required names 333 | 334 | function createNewLady( logicCube ) { 335 | 336 | setAssetAt( ladies, logicCube, true, 0.45 ); 337 | 338 | }; 339 | 340 | function createNewAlpinist( logicCube ) { 341 | 342 | setAssetAt( alpinists, logicCube, true, 0.6 ); 343 | 344 | }; 345 | 346 | function createNewEdelweiss( logicCube ) { 347 | 348 | setAssetAt( edelweisses, logicCube ); 349 | 350 | }; 351 | 352 | function createNewBonus( logicCube ) { 353 | 354 | setAssetAt( bonuses, logicCube ); 355 | 356 | }; 357 | 358 | // Take the last free group from the right asset array, position it, and hide/show it. 359 | function setAssetAt( assetArray, logicCube, floor, bubbleOffset ) { 360 | 361 | let pos = logicCube.position ; 362 | let tag = logicCube.tag ; 363 | 364 | for ( let asset of assetArray ) { 365 | 366 | if ( !asset.userData.isSet ) { 367 | 368 | asset.position.copy( pos ); 369 | 370 | if ( floor ) { 371 | asset.position.y = Math.floor( asset.position.y ); 372 | 373 | // patch the cube position itself to get the 374 | // exclamation mark sign positioned properly 375 | 376 | pos.y = Math.floor( pos.y ) + bubbleOffset; 377 | } 378 | 379 | asset.userData.isSet = true ; 380 | asset.userData.tag = tag ; 381 | asset.userData.graph = getGraphFromTag( tag ); 382 | 383 | // anchor for bonus floating animation 384 | 385 | asset.userData.initPos = asset.position.clone(); 386 | 387 | setGroupVisibility( asset ); 388 | 389 | scene.add( asset ); 390 | 391 | break ; 392 | 393 | }; 394 | 395 | }; 396 | 397 | }; 398 | 399 | // Get the name of the graph bound to a given asset 400 | function getGraphFromTag( tag ) { 401 | 402 | if ( tag.match( /bonus-stamina-1/ ) ) { 403 | 404 | return 'cave-A'; 405 | 406 | } else { 407 | 408 | return 'mountain'; 409 | 410 | }; 411 | 412 | }; 413 | 414 | /////////////// 415 | //// GENERAL 416 | /////////////// 417 | 418 | // Create a new lambert material for the passed model, with the original map 419 | function setLambert( model, lightEmissive ) { 420 | 421 | model.traverse( (obj)=> { 422 | 423 | if ( obj.type == 'Mesh' || 424 | obj.type == 'SkinnedMesh' ) { 425 | 426 | obj.material = new THREE.MeshLambertMaterial({ 427 | map: obj.material.map, 428 | side: obj.material.side, 429 | skinning: obj.material.skinning, 430 | emissive: lightEmissive ? 0x191919 : 0x000000 431 | }); 432 | 433 | // fix self-shadows on double-sided materials 434 | 435 | obj.material.onBeforeCompile = function(stuff) { 436 | var chunk = THREE.ShaderChunk.shadowmap_pars_fragment 437 | .split ('z += shadowBias') 438 | .join ('z += shadowBias - 0.001'); 439 | stuff.fragmentShader = stuff.fragmentShader 440 | .split ('#include ') 441 | .join (chunk); 442 | }; 443 | 444 | obj.castShadow = true ; 445 | obj.receiveShadow = true ; 446 | 447 | }; 448 | 449 | }); 450 | 451 | }; 452 | 453 | // Called by gameState to hide/show assets depending on sceneGraph 454 | function updateGraph( destination ) { 455 | 456 | if ( destination ) { 457 | currentGraph = destination 458 | }; 459 | 460 | alpinists.forEach( ( assetGroup )=> { 461 | setGroupVisibility( assetGroup ); 462 | }); 463 | 464 | ladies.forEach( ( assetGroup )=> { 465 | setGroupVisibility( assetGroup ); 466 | }); 467 | 468 | edelweisses.forEach( ( assetGroup )=> { 469 | setGroupVisibility( assetGroup ); 470 | }); 471 | 472 | bonuses.forEach( ( assetGroup )=> { 473 | setGroupVisibility( assetGroup ); 474 | }); 475 | 476 | }; 477 | 478 | // 479 | 480 | function setGroupVisibility( assetGroup ) { 481 | 482 | if ( assetGroup.userData.graph == currentGraph ) { 483 | 484 | assetGroup.visible = true ; 485 | 486 | } else { 487 | 488 | assetGroup.visible = false ; 489 | 490 | }; 491 | 492 | if ( assetGroup.userData.isDeleted ) { 493 | 494 | assetGroup.visible = false ; 495 | 496 | }; 497 | 498 | }; 499 | 500 | // 501 | 502 | function deleteBonus( bonusName ) { 503 | 504 | if ( bonusName.match( /stamina/ ) ) { 505 | 506 | checkForBonus( edelweisses ); 507 | 508 | } else { 509 | 510 | checkForBonus( bonuses ); 511 | 512 | }; 513 | 514 | function checkForBonus( groupArr ) { 515 | 516 | groupArr.forEach( (group)=> { 517 | 518 | if ( group.userData.tag == bonusName ) { 519 | 520 | group.visible = false ; 521 | group.userData.isDeleted = true ; 522 | 523 | }; 524 | 525 | }); 526 | 527 | }; 528 | 529 | }; 530 | 531 | // 532 | 533 | function update( delta ) { 534 | 535 | for ( let mixer of alpinistMixers ) { 536 | 537 | mixer.update( delta ); 538 | 539 | }; 540 | 541 | for ( let mixer of ladyMixers ) { 542 | 543 | mixer.update( delta ); 544 | 545 | }; 546 | 547 | for ( let mixer of charMixers ) { 548 | 549 | mixer.update( delta ); 550 | 551 | }; 552 | 553 | for ( let group of edelweisses ) { 554 | 555 | updateBonus( group ); 556 | 557 | }; 558 | 559 | for ( let group of bonuses ) { 560 | 561 | updateBonus( group ); 562 | 563 | }; 564 | 565 | }; 566 | 567 | // 568 | 569 | function updateBonus( group ) { 570 | 571 | if ( group.userData.initPos ) { 572 | 573 | group.rotation.y += 0.01 ; 574 | 575 | group.position.copy( group.userData.initPos ); 576 | group.position.y += ( Math.sin( Date.now() / 700 ) * 0.08 ); 577 | 578 | for ( let child of group.children ) { 579 | 580 | if ( child.userData.rotationSpeed ) { 581 | 582 | child.rotation.y += child.userData.rotationSpeed ; 583 | 584 | }; 585 | 586 | }; 587 | 588 | }; 589 | 590 | }; 591 | 592 | // 593 | 594 | return { 595 | createCharacter, 596 | releaseCharacter, 597 | toggleCharacterShadows, 598 | createNewLady, 599 | createNewAlpinist, 600 | createNewEdelweiss, 601 | createNewBonus, 602 | updateGraph, 603 | update, 604 | deleteBonus 605 | }; 606 | 607 | }; 608 | -------------------------------------------------------------------------------- /public/js/CameraControl.js: -------------------------------------------------------------------------------- 1 | 2 | function CameraControl( player, camera ) { 3 | 4 | var group = new THREE.Group(); 5 | scene.add( group ); 6 | 7 | const MAX_YAW = 0.2 ; 8 | const CAMERA_DIRECTION = new THREE.Vector3( 0, 0.4, 1 ).normalize(); 9 | const DEFAULT_CAMERA_DISTANCE = 2.2 ; 10 | const MIN_CAMERA_DISTANCE = 1.7 ; 11 | const CAMERA_WIDTH = 0.29 ; 12 | const CAMERA_TWEENING_SPEED = 0.08 ; 13 | 14 | var backupCameraPos = new THREE.Vector3(); 15 | var cameraTarget = new THREE.Vector3(); 16 | var cameraWantedPos = new THREE.Vector3(); 17 | 18 | var testRayOrigin = new THREE.Vector3(); 19 | var testRayDirection = new THREE.Vector3(); 20 | var testRay = new THREE.Ray( testRayOrigin, testRayDirection ); 21 | 22 | var cameraRayOrigin = new THREE.Vector3( 0, 0.3, 0 ); 23 | var cameraRayDirection = new THREE.Vector3(); 24 | var cameraRayAxis = new THREE.Vector3( 0, 1, 0 ); 25 | var cameraRay = new THREE.Ray( cameraRayOrigin, cameraRayDirection ); 26 | 27 | var cameraOffsetVec = new THREE.Vector3(); 28 | 29 | var cameraColRayTop = new THREE.Ray( 30 | new THREE.Vector3(), 31 | new THREE.Vector3( 0, 1, 0 ) 32 | ); 33 | 34 | var cameraColRayBottom = new THREE.Ray( 35 | new THREE.Vector3(), 36 | new THREE.Vector3( 0, -1, 0 ) 37 | ); 38 | 39 | var cameraColRayLeft = new THREE.Ray( 40 | new THREE.Vector3(), 41 | new THREE.Vector3( -1, 1, 0 ) 42 | ); 43 | 44 | var cameraColRayRight = new THREE.Ray( 45 | new THREE.Vector3(), 46 | new THREE.Vector3( 1, 1, 0 ) 47 | ); 48 | 49 | ///////////// 50 | /// LIGHT 51 | ///////////// 52 | 53 | var directionalLight = addShadowedLight( 3, 25, 7, 0xffffff, 0.85 ); 54 | group.add( directionalLight ); 55 | group.add( directionalLight.target ); 56 | 57 | function addShadowedLight( x, y, z, color, intensity ) { 58 | 59 | var directionalLight = new THREE.DirectionalLight( color, intensity ); 60 | 61 | directionalLight.position.set( x, y, z ); 62 | directionalLight.castShadow = true; 63 | 64 | var d = 10; 65 | 66 | directionalLight.shadow.camera.left = -d; 67 | directionalLight.shadow.camera.right = d; 68 | directionalLight.shadow.camera.top = d; 69 | directionalLight.shadow.camera.bottom = -d; 70 | directionalLight.shadow.camera.near = 0.1; 71 | directionalLight.shadow.camera.far = 50; 72 | directionalLight.shadow.mapSize.width = 1024; 73 | directionalLight.shadow.mapSize.height = 1024; 74 | directionalLight.shadow.bias = -0; 75 | 76 | return directionalLight; 77 | }; 78 | 79 | function hideLight() { 80 | directionalLight.visible = false; 81 | }; 82 | 83 | function showLight() { 84 | directionalLight.visible = true; 85 | }; 86 | 87 | /////////// 88 | /// INIT 89 | /////////// 90 | 91 | adaptFOV(); 92 | 93 | resetCameraPos(); 94 | 95 | scene.add( camera ); 96 | 97 | // Set the FOV depending on wether the display 98 | // is horizontal or vertical 99 | function adaptFOV() { 100 | 101 | // display is vertical 102 | if ( window.innerHeight > window.innerWidth ) { 103 | 104 | camera.fov = 110 ; 105 | camera.updateProjectionMatrix(); 106 | 107 | // display is horizontal 108 | } else { 109 | 110 | camera.fov = 90 ; 111 | camera.updateProjectionMatrix(); 112 | 113 | }; 114 | 115 | }; 116 | 117 | //////////////////////// 118 | /// UPDATE 119 | ////////////////////// 120 | 121 | function update( delta ) { 122 | 123 | group.position.copy( player.position ); 124 | group.position.z -= 7 ; 125 | 126 | cameraTarget.copy( player.position ); 127 | cameraTarget.y += atlas.PLAYERHEIGHT / 2 ; 128 | 129 | ////////////////////////////////////////////////////////// 130 | /// GET INTERSECTION POINTS ON RIGHT AND LEFT OF PLAYER 131 | ////////////////////////////////////////////////////////// 132 | 133 | testRay.origin.copy( cameraTarget ); 134 | 135 | // get the scene graph stages to check 136 | let stages = [ 137 | Math.floor( player.position.y ), 138 | Math.floor( player.position.y ) + 1 , 139 | Math.floor( player.position.y ) - 1 140 | ]; 141 | 142 | /// LEFT 143 | 144 | testRay.direction.set( -1, 0, 0 ); 145 | 146 | let rayCollision = atlas.intersectRay( testRay, stages, false ); 147 | 148 | let intersectionLeft = rayCollision.points ? 149 | rayCollision.points[ 0 ].x : 150 | false ; 151 | 152 | /// RIGHT 153 | 154 | testRay.direction.set( 1, 0, 0 ); 155 | 156 | rayCollision = atlas.intersectRay( testRay, stages, false ); 157 | 158 | let intersectionRight = rayCollision.points ? 159 | rayCollision.points[ 0 ].x : 160 | false ; 161 | 162 | ///////////////////////// 163 | /// ANGLE OF CAMERA RAY 164 | ///////////////////////// 165 | 166 | if ( intersectionLeft === false && 167 | intersectionRight === false ) { 168 | 169 | var leftRightRatio = 0.5 ; 170 | 171 | } else if ( intersectionLeft === false ) { 172 | 173 | var leftRightRatio = 1 ; 174 | 175 | } else if ( intersectionRight === false ) { 176 | 177 | var leftRightRatio = 0 ; 178 | 179 | } else { 180 | 181 | // cross product to get a ratio between 0 and 1 where 182 | // 0 means a wall is very close on the LEFT, and 183 | // 1 a wall is very close on the RIGHT. 184 | var leftRightRatio = ( player.position.x - intersectionLeft ) / 185 | ( intersectionRight - intersectionLeft ); 186 | 187 | }; 188 | 189 | // a radian angle between -1.57 and 1.57 is computed from the ratio 190 | let angle = Math.asin( (leftRightRatio * 2) -1 ); 191 | 192 | // constraint to MAX_YAW 193 | angle = (angle * MAX_YAW) / (Math.PI / 2); 194 | 195 | /////////////////////////// 196 | /// INTERSECT CAMERA RAY 197 | /////////////////////////// 198 | 199 | // The computed angle is applied to the ray we use 200 | // to position the camera 201 | 202 | cameraRay.origin.copy( cameraTarget ); 203 | cameraRay.direction.copy( CAMERA_DIRECTION ); 204 | 205 | cameraRay.direction.applyAxisAngle( 206 | cameraRayAxis, 207 | -angle 208 | ); 209 | 210 | /// CAMERA DISTANCE 211 | 212 | // scene graph stages to check for collision with camera ray 213 | stages = [ 214 | Math.floor( player.position.y ), 215 | Math.floor( player.position.y ) +1, 216 | Math.floor( player.position.y ) +2, 217 | Math.floor( player.position.y ) +3 218 | ]; 219 | 220 | rayCollision = atlas.intersectRay( cameraRay, stages, true ); 221 | 222 | if ( rayCollision ) { 223 | 224 | // We want to camera to be positioned at the intersection 225 | // between the ray and the obstacle 226 | var distCamera = rayCollision.points[ 0 ].distanceTo( cameraRay.origin ) - 0.05; 227 | 228 | if ( distCamera > DEFAULT_CAMERA_DISTANCE ) { 229 | 230 | distCamera = DEFAULT_CAMERA_DISTANCE ; 231 | 232 | }; 233 | 234 | } else { 235 | 236 | var distCamera = DEFAULT_CAMERA_DISTANCE ; 237 | 238 | }; 239 | 240 | // Set the vector cameraWantedPos at the computed point 241 | cameraRay.at( distCamera, cameraWantedPos ); 242 | 243 | // Check if wanted position is too close from player, 244 | // if yes, then make it higher 245 | 246 | if ( distCamera < MIN_CAMERA_DISTANCE ) { 247 | 248 | testRay.origin.copy( cameraWantedPos ); 249 | testRay.direction.set( 0, 1, 0 ); 250 | 251 | stages = [ 252 | Math.floor( cameraWantedPos.y ), 253 | Math.floor( cameraWantedPos.y ) +1, 254 | Math.floor( cameraWantedPos.y ) +2 255 | ]; 256 | 257 | let rayCollision = atlas.intersectRay( testRay, stages, true ); 258 | 259 | if ( !rayCollision.points || 260 | !( rayCollision.points[ 0 ].distanceTo( cameraWantedPos ) < ( CAMERA_WIDTH / 2 ) ) ) { 261 | 262 | let height = Math.sqrt( Math.pow( MIN_CAMERA_DISTANCE, 2 ) - Math.pow( distCamera, 2 ) ); 263 | 264 | cameraWantedPos.y += height ; 265 | 266 | }; 267 | 268 | }; 269 | 270 | ////////////////// 271 | /// CAMERA PATH 272 | ////////////////// 273 | 274 | stages = [ 275 | Math.floor( camera.position.y ), 276 | Math.floor( camera.position.y ) +1, 277 | Math.floor( camera.position.y ) -1, 278 | ]; 279 | 280 | testRay.origin.copy( camera.position ); 281 | 282 | testRay.direction.copy( cameraWantedPos ) 283 | .sub( camera.position ) 284 | .normalize(); 285 | 286 | // We check if intersection between camera and cameraWantedPos 287 | rayCollision = atlas.intersectRay( testRay, stages, true ); 288 | 289 | // If there is, we try to avoid the obstacle on the path 290 | if ( rayCollision && 291 | rayCollision.points[ 0 ].distanceTo( camera.position ) < cameraWantedPos.distanceTo( camera.position ) ) { 292 | 293 | if ( rayCollision.closestTile.isWall && 294 | rayCollision.closestTile.isXAligned ) { 295 | 296 | dodgeCamera( rayCollision, 'z', [ 297 | 298 | { 299 | dist : rayCollision.points[ 0 ].y - 300 | Math.min( rayCollision.closestTile.points[ 0 ].y, 301 | rayCollision.closestTile.points[ 1 ].y ), 302 | pos : Math.min( rayCollision.closestTile.points[ 0 ].y, 303 | rayCollision.closestTile.points[ 1 ].y ), 304 | dir : 'y', 305 | sign : -1 306 | }, 307 | 308 | { 309 | dist : rayCollision.points[ 0 ].x - 310 | Math.min( rayCollision.closestTile.points[ 0 ].x, 311 | rayCollision.closestTile.points[ 1 ].x ), 312 | pos : Math.min( rayCollision.closestTile.points[ 0 ].x, 313 | rayCollision.closestTile.points[ 1 ].x ), 314 | dir : 'x', 315 | sign : -1 316 | }, 317 | 318 | { 319 | dist: rayCollision.points[ 0 ].y - 320 | Math.max( rayCollision.closestTile.points[ 0 ].y, 321 | rayCollision.closestTile.points[ 1 ].y ), 322 | pos: Math.max( rayCollision.closestTile.points[ 0 ].y, 323 | rayCollision.closestTile.points[ 1 ].y ), 324 | dir : 'y', 325 | sign : 1 326 | }, 327 | 328 | { 329 | dist : rayCollision.points[ 0 ].x - 330 | Math.max( rayCollision.closestTile.points[ 0 ].x, 331 | rayCollision.closestTile.points[ 1 ].x ), 332 | pos : Math.max( rayCollision.closestTile.points[ 0 ].x, 333 | rayCollision.closestTile.points[ 1 ].x ), 334 | dir : 'x', 335 | sign : 1 336 | } 337 | 338 | ]); 339 | 340 | 341 | } else if ( rayCollision.closestTile.isWall && 342 | !rayCollision.closestTile.isXAligned ) { 343 | 344 | dodgeCamera( rayCollision, 'x', [ 345 | 346 | { 347 | dist : rayCollision.points[ 0 ].y - 348 | Math.min( rayCollision.closestTile.points[ 0 ].y, 349 | rayCollision.closestTile.points[ 1 ].y ), 350 | pos : Math.min( rayCollision.closestTile.points[ 0 ].y, 351 | rayCollision.closestTile.points[ 1 ].y ), 352 | dir : 'y', 353 | sign : -1 354 | }, 355 | 356 | { 357 | dist : rayCollision.points[ 0 ].z - 358 | Math.min( rayCollision.closestTile.points[ 0 ].z, 359 | rayCollision.closestTile.points[ 1 ].z ), 360 | pos : Math.min( rayCollision.closestTile.points[ 0 ].z, 361 | rayCollision.closestTile.points[ 1 ].z ), 362 | dir : 'z', 363 | sign : -1 364 | }, 365 | 366 | { 367 | dist: rayCollision.points[ 0 ].y - 368 | Math.max( rayCollision.closestTile.points[ 0 ].y, 369 | rayCollision.closestTile.points[ 1 ].y ), 370 | pos: Math.max( rayCollision.closestTile.points[ 0 ].y, 371 | rayCollision.closestTile.points[ 1 ].y ), 372 | dir : 'y', 373 | sign : 1 374 | }, 375 | 376 | { 377 | dist : rayCollision.points[ 0 ].z - 378 | Math.max( rayCollision.closestTile.points[ 0 ].z, 379 | rayCollision.closestTile.points[ 1 ].z ), 380 | pos : Math.max( rayCollision.closestTile.points[ 0 ].z, 381 | rayCollision.closestTile.points[ 1 ].z ), 382 | dir : 'z', 383 | sign : 1 384 | } 385 | 386 | ]); 387 | 388 | }; 389 | 390 | }; 391 | 392 | ////////////////////// 393 | /// POSITION CAMERA 394 | ////////////////////// 395 | 396 | backupCameraPos.copy( camera.position ); 397 | 398 | attemptCameraMove( 'x', delta ); 399 | attemptCameraMove( 'y', delta ); 400 | attemptCameraMove( 'z', delta ); 401 | 402 | camera.lookAt( cameraTarget ); 403 | 404 | }; 405 | 406 | // 407 | 408 | function dodgeCamera( rayCollision, adjDir, edges ) { 409 | 410 | // 'edges' object contain the information about the edges of the tile 411 | // being an obstacle on the camera path. We will use it to move the 412 | // camera in the shortest escape path 413 | 414 | // Find the closest edge 415 | edges.sort( ( a, b )=> { 416 | 417 | return Math.abs( a.dist ) - Math.abs( b.dist ); 418 | 419 | }); 420 | 421 | // For each edge, we check if an adjacent tile exists. 422 | // If so, we try the next edge. 423 | // If not we move the camera in the wanted position. 424 | 425 | for ( edge of edges ) { 426 | 427 | if ( atlas.adjTileExists( rayCollision.closestTile, edge.dir, edge.sign ) ) { 428 | 429 | continue ; 430 | 431 | } else { 432 | 433 | // Align the camera on the same plane as the obstacle tile on the x axis 434 | camera.position[ adjDir ] = rayCollision.closestTile.points[ 0 ][ adjDir ] ; 435 | 436 | // push the camera on X or Y to avoid the obstacle tile 437 | camera.position[ edge.dir ] = edge.pos + 438 | ( ( CAMERA_WIDTH / 2 ) * edge.sign ) + 439 | ( 0.1 * edge.sign ); 440 | 441 | break ; 442 | 443 | }; 444 | 445 | }; 446 | 447 | }; 448 | 449 | // 450 | 451 | function attemptCameraMove( dir, delta ) { 452 | 453 | camera.position[ dir ] = utils.lerp( camera.position[ dir ], cameraWantedPos[ dir ], CAMERA_TWEENING_SPEED * delta ); 454 | 455 | if ( atlas.collideCamera() ) { 456 | 457 | camera.position[ dir ] = backupCameraPos[ dir ]; 458 | 459 | }; 460 | 461 | }; 462 | 463 | // 464 | 465 | function resetCameraPos() { 466 | 467 | camera.position.copy( CAMERA_DIRECTION ); 468 | camera.position.multiplyScalar( DEFAULT_CAMERA_DISTANCE ); 469 | camera.position.add( player.position ); 470 | 471 | }; 472 | 473 | // 474 | 475 | return { 476 | update, 477 | directionalLight, 478 | adaptFOV, 479 | CAMERA_WIDTH, 480 | resetCameraPos, 481 | hideLight, 482 | showLight 483 | }; 484 | 485 | }; 486 | -------------------------------------------------------------------------------- /public/js/DynamicItems.js: -------------------------------------------------------------------------------- 1 | 2 | function DynamicItems() { 3 | 4 | var interactionSign = new THREE.Group(); // will contain the sign sprite 5 | interactionSign.visible = false ; 6 | scene.add( interactionSign ); 7 | 8 | var interactiveCubes = []; 9 | 10 | // INIT 11 | 12 | var spriteMap = textureLoader.load( "https://edelweiss-game.s3.eu-west-3.amazonaws.com/assets/bubble.png" ); 13 | var spriteMaterial = new THREE.SpriteMaterial( { map: spriteMap, color: 0xffffff } ); 14 | 15 | sprite = new THREE.Sprite( spriteMaterial ); 16 | sprite.scale.set( 0.3, 0.6, 1 ); 17 | sprite.position.y = 0.6 ; 18 | 19 | interactionSign.add( sprite ) 20 | 21 | // 22 | 23 | // Add a cube to the three arrays containing cubes to interact with 24 | function addCube( logicCube ) { 25 | 26 | switch ( logicCube.type ) { 27 | 28 | case 'cube-interactive' : 29 | 30 | interactiveCubes.push( logicCube ); 31 | 32 | if ( logicCube.tag.match( /npc/ ) && 33 | !logicCube.tag.match( /npc-respawn/ ) && 34 | !logicCube.tag.match( /npc-dev/ ) ) { 35 | 36 | assetManager.createNewLady( logicCube ); 37 | 38 | } else if ( logicCube.tag.match( /npc-respawn/ ) ) { 39 | 40 | assetManager.createNewAlpinist( logicCube ); 41 | 42 | }; 43 | 44 | break; 45 | 46 | 47 | case 'cube-trigger' : 48 | 49 | if ( logicCube.tag && 50 | logicCube.tag.match( /bonus-stamina/ ) ) { 51 | 52 | assetManager.createNewEdelweiss( logicCube ); 53 | 54 | } else if ( logicCube.tag && 55 | logicCube.tag.match( /bonus/ ) ) { 56 | 57 | assetManager.createNewBonus( logicCube ); 58 | 59 | }; 60 | 61 | break; 62 | 63 | }; 64 | 65 | }; 66 | 67 | //////////////////////// 68 | /// INTERACTION SIGN 69 | //////////////////////// 70 | 71 | function showInteractionSign( tag ) { 72 | 73 | interactionSign.visible = true ; 74 | 75 | interactiveCubes.forEach( ( logicCube )=> { 76 | 77 | if ( logicCube.tag == tag ) { 78 | 79 | interactionSign.position.copy( logicCube.position ); 80 | 81 | }; 82 | 83 | }); 84 | 85 | }; 86 | 87 | // 88 | 89 | function clearInteractionSign() { 90 | 91 | interactionSign.visible = false ; 92 | 93 | }; 94 | 95 | // 96 | 97 | return { 98 | showInteractionSign, 99 | clearInteractionSign, 100 | addCube 101 | }; 102 | 103 | }; 104 | -------------------------------------------------------------------------------- /public/js/Easing.js: -------------------------------------------------------------------------------- 1 | 2 | function Easing() { 3 | 4 | return { 5 | // no easing, no acceleration 6 | linear: function (t) { return t }, 7 | // accelerating from zero velocity 8 | easeInQuad: function (t) { return t*t }, 9 | // decelerating to zero velocity 10 | easeOutQuad: function (t) { return t*(2-t) }, 11 | // acceleration until halfway, then deceleration 12 | easeInOutQuad: function (t) { return t<.5 ? 2*t*t : -1+(4-2*t)*t }, 13 | // accelerating from zero velocity 14 | easeInCubic: function (t) { return t*t*t }, 15 | // decelerating to zero velocity 16 | easeOutCubic: function (t) { return (--t)*t*t+1 }, 17 | // acceleration until halfway, then deceleration 18 | easeInOutCubic: function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 }, 19 | // accelerating from zero velocity 20 | easeInQuart: function (t) { return t*t*t*t }, 21 | // decelerating to zero velocity 22 | easeOutQuart: function (t) { return 1-(--t)*t*t*t }, 23 | // acceleration until halfway, then deceleration 24 | easeInOutQuart: function (t) { return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t }, 25 | // accelerating from zero velocity 26 | easeInQuint: function (t) { return t*t*t*t*t }, 27 | // decelerating to zero velocity 28 | easeOutQuint: function (t) { return 1+(--t)*t*t*t*t }, 29 | // acceleration until halfway, then deceleration 30 | easeInOutQuint: function (t) { return t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t } 31 | }; 32 | 33 | }; 34 | -------------------------------------------------------------------------------- /public/js/MapManager.js: -------------------------------------------------------------------------------- 1 | 2 | function MapManager() { 3 | 4 | const CHUNK_SIZE = 12 ; 5 | const LAST_CHUNK_ID = 13 ; 6 | 7 | // LIGHTS 8 | 9 | const LIGHT_BASE_INTENS = 0.48; 10 | const LIGHT_CAVE_INTENS = 0.30; 11 | 12 | const POINT_LIGHT_INTENS = 0.5; 13 | const POINT_LIGHT_LENGTH = 9; 14 | 15 | // FOG 16 | 17 | const FOG = new THREE.FogExp2( 0xd7cbb1, 0.06 ); 18 | 19 | scene.fog = FOG; 20 | 21 | // CUBEMAP 22 | 23 | var path = 'https://edelweiss-game.s3.eu-west-3.amazonaws.com/skybox/'; 24 | var format = '.jpg'; 25 | var urls = [ 26 | path + 'px' + format, path + 'nx' + format, 27 | path + 'py' + format, path + 'ny' + format, 28 | path + 'pz' + format, path + 'nz' + format 29 | ]; 30 | 31 | var reflectionCube = new THREE.CubeTextureLoader().load( urls ); 32 | reflectionCube.format = THREE.RGBFormat; 33 | 34 | var caveBackground = new THREE.Color( 0x251e16 ); 35 | var caveBackgroundGrey = new THREE.Color( 0x171614 ); 36 | 37 | scene.background = reflectionCube; 38 | 39 | // 40 | 41 | // Object that will contain a positive boolean on the index 42 | // corresponding to the ID of the loaded mountain map chunks, 43 | // and the name of the loaded caves (cave-A...) 44 | var record = {}; 45 | 46 | // Can be "mountain", or "cave-A" (B,C,D,E,F,G) 47 | var params = { 48 | currentMap: "mountain" 49 | }; 50 | 51 | /* 52 | Creation of groups that will contain the different maps. 53 | All these groups will be added to the scene, and 54 | hided/showed later on. 55 | */ 56 | 57 | var maps = {}; 58 | addMapGroup( 'cave-A' ); 59 | addMapGroup( 'cave-B' ); 60 | addMapGroup( 'cave-C' ); 61 | addMapGroup( 'cave-D' ); 62 | addMapGroup( 'cave-E' ); 63 | addMapGroup( 'cave-F' ); 64 | addMapGroup( 'cave-G' ); 65 | addMapGroup( 'dev-home' ); 66 | addMapGroup( 'mountain' ); 67 | maps.mountain.visible = true ; 68 | 69 | function addMapGroup( groupName ) { 70 | 71 | maps[ groupName ] = new THREE.Group(); 72 | maps[ groupName ].visible = false; 73 | scene.add( maps[ groupName ] ); 74 | 75 | }; 76 | 77 | /* 78 | Only run if the player is in the main map (mountain). 79 | It loads new chunks of map to the scene along the path 80 | of the player, to save loading time at startup. 81 | */ 82 | function update( mustFindMap ) { 83 | 84 | if ( mustFindMap && 85 | params.currentMap == 'mountain' && 86 | atlas && 87 | atlas.player ) { 88 | 89 | // Get current map chunk ID from player's z pos 90 | let z = Math.floor( -atlas.player.position.z / CHUNK_SIZE ) ; 91 | if ( z < 0 ) z = 0 ; 92 | 93 | // request chunks of map near player's position 94 | 95 | requestChunk( z ); 96 | requestChunk( z + 1 ); 97 | requestChunk( z + 2 ); 98 | requestChunk( z + 3 ); 99 | 100 | function requestChunk( z ) { 101 | 102 | if ( z <= LAST_CHUNK_ID && !record[ z ] ) { 103 | 104 | record[ z ] = true ; 105 | 106 | // Load the map chunk 107 | loadMap( z ); 108 | 109 | }; 110 | 111 | }; 112 | 113 | }; 114 | 115 | }; 116 | 117 | // 118 | 119 | function loadMap( mapName, resolve ) { 120 | 121 | gltfLoader.load( `https://edelweiss-game.s3.eu-west-3.amazonaws.com/map/${ mapName }.glb`, (glb)=> { 122 | 123 | // console.log( '///// MAP LOADED : ' + mapName ); 124 | 125 | glb.scene.traverse( (child)=> { 126 | 127 | if ( child.material ) { 128 | 129 | child.material = new THREE.MeshLambertMaterial({ 130 | map: child.material.map, 131 | side: THREE.FrontSide 132 | }); 133 | 134 | child.castShadow = true ; 135 | child.receiveShadow = true ; 136 | 137 | }; 138 | 139 | }); 140 | 141 | maps[ params.currentMap ].add( glb.scene ); 142 | record[ mapName ] = true; 143 | 144 | if ( resolve ) resolve(); 145 | 146 | }, null, (err)=> { 147 | 148 | console.error( `Impossible to load file ${ mapName }.glb` ); 149 | 150 | if ( resolve ) resolve(); 151 | 152 | }); 153 | 154 | }; 155 | 156 | // Make current map disappear, and show a new map 157 | function switchMap( newMapName ) { 158 | 159 | if ( newMapName === "mountain" ) { 160 | 161 | scene.fog = FOG; 162 | scene.background = reflectionCube; 163 | ambientLight.intensity = LIGHT_BASE_INTENS; 164 | 165 | } else { 166 | 167 | scene.fog = undefined; 168 | scene.background = caveBackground; 169 | ambientLight.intensity = LIGHT_CAVE_INTENS; 170 | 171 | }; 172 | 173 | if ( newMapName === "cave-F" ) scene.background = caveBackgroundGrey; 174 | if ( newMapName === "dev-home" ) ambientLight.intensity = LIGHT_BASE_INTENS; 175 | 176 | return new Promise( (resolve, reject)=> { 177 | 178 | if ( !maps[ newMapName ] ) addMapGroup( newMapName ); 179 | 180 | maps[ params.currentMap ].visible = false ; 181 | maps[ newMapName ].visible = true ; 182 | params.currentMap = newMapName ; 183 | 184 | // change lighting according to future map 185 | if ( newMapName == 'mountain' ) { 186 | 187 | cameraControl.showLight(); 188 | removeCaveLights(); 189 | 190 | } else { 191 | 192 | cameraControl.hideLight(); 193 | createCaveLights( newMapName ); 194 | 195 | }; 196 | 197 | /* 198 | if the new map is the mountain, then the map will be udpated 199 | on the fly. If not, then the cave map is loaded here. 200 | */ 201 | if ( newMapName == 'mountain' || 202 | record[ newMapName ] ) { 203 | 204 | resolve(); 205 | 206 | } else { 207 | 208 | loadMap( newMapName, resolve ); 209 | 210 | }; 211 | 212 | }); 213 | 214 | }; 215 | 216 | // 217 | 218 | var caveLights = []; 219 | 220 | function createCaveLights( graphName ) { 221 | 222 | var graph = gameState.sceneGraphs[ graphName ].cubesGraph; 223 | 224 | for (let i = 0 ; i < graph.length ; i++ ) { 225 | 226 | if ( !graph[ i ] ) continue ; 227 | 228 | graph[ i ].forEach( ( cube )=> { 229 | 230 | if ( cube.tag && cube.tag.match( /cave-/ ) ) { 231 | 232 | var pos = cube.position ; 233 | 234 | var light = new THREE.PointLight( 235 | 0xffffff, 236 | POINT_LIGHT_INTENS, 237 | POINT_LIGHT_LENGTH 238 | ); 239 | 240 | light.position.set( pos.x, pos.y, pos.z ); 241 | scene.add( light ); 242 | caveLights.push( light ); 243 | 244 | }; 245 | 246 | }); 247 | 248 | }; 249 | 250 | }; 251 | 252 | // 253 | 254 | function removeCaveLights() { 255 | 256 | caveLights.forEach( ( light )=> { 257 | 258 | scene.remove( light ); 259 | 260 | }); 261 | 262 | caveLights = []; 263 | 264 | }; 265 | 266 | // 267 | 268 | return { 269 | update, 270 | switchMap, 271 | params 272 | }; 273 | 274 | }; 275 | -------------------------------------------------------------------------------- /public/js/Optimizer.js: -------------------------------------------------------------------------------- 1 | 2 | function Optimizer() { 3 | 4 | /* 5 | These two constants define in what range of FPS the game 6 | will be displayed. The larger the range, the more stable 7 | it will be, as there will be fewer attempts or optimization/ deoptimization 8 | */ 9 | 10 | const OPTFPS = 1 / 28 ; // FPS rate above which optimization must occur 11 | const DEOPTFPS = 1 / 53 ; // FPS rate under which de-optimisation will occur 12 | 13 | // 14 | 15 | var optStep = 50 ; // ms duration of FPS sampling between each opti 16 | var lastOptiTime = 0 ; 17 | var samples = []; 18 | 19 | // 20 | 21 | const domWorldCheap = document.getElementById('worldCheap'); 22 | const domWorldHigh = document.getElementById('worldHigh'); 23 | 24 | var params = { 25 | level: 0, 26 | attempts: [ 0, 0, 0, 0, 0 ], // holds the number of failed attempt to set the optimization at given level 27 | timeOpti: Date.now() // last time an optimisation was done 28 | }; 29 | 30 | /* 31 | optimize is called by the loop everytime the frame rate 32 | is above the OPTFPS (which means rendering is slow). 33 | It will increment the level of optimization by one, 34 | so rendering will be faster, with worst graphics as 35 | a trade-off 36 | */ 37 | function optimize() { 38 | 39 | // Will disable FXAA 40 | if ( params.level == 0 ) { 41 | 42 | renderer.setPixelRatio( 1 ); 43 | params.level = 1 ; 44 | 45 | // set pixel ratio to 1, which has effect mostly on smartphones 46 | } else if ( params.level == 1 ) { 47 | 48 | params.level = 2 ; 49 | 50 | // Remove the shadow from the dynamic objects, 51 | // and stop rendering shadows dynamically 52 | } else if ( params.level == 2 ) { 53 | 54 | assetManager.toggleCharacterShadows( false ); 55 | 56 | setTimeout( ()=> { 57 | renderer.shadowMap.enabled = false; 58 | }, 0); 59 | 60 | params.level = 3 ; 61 | 62 | } else if ( params.level == 3 ) { 63 | 64 | camera.far = 11.5 ; 65 | camera.updateProjectionMatrix(); 66 | 67 | params.level = 4 ; 68 | 69 | }; 70 | 71 | }; 72 | 73 | /* 74 | deOptimize is called by the loop every time the frame rate 75 | is under DEOPTFPS (meaning rendering is fast). 76 | It will decrement the level of optimization by one, 77 | which will make graphics better but frame rate maybe lower 78 | */ 79 | function deOptimize() { 80 | 81 | // There is already no optimization occuring 82 | if ( params.level == 0 ) { 83 | 84 | return 85 | 86 | // enable FXAA 87 | } else if ( params.level == 1 ) { 88 | 89 | renderer.setPixelRatio( window.devicePixelRatio ); 90 | params.level = 0 ; 91 | 92 | // set pixel ratio to the default device pixel ratio 93 | } else if ( params.level == 2 ) { 94 | 95 | params.level = 1 ; 96 | 97 | // enable shadows on dynamic objects 98 | } else if ( params.level == 3 ) { 99 | 100 | renderer.shadowMap.enabled = true; 101 | 102 | assetManager.toggleCharacterShadows( true ); 103 | 104 | params.level = 2 ; 105 | 106 | } else if ( params.level == 4 ) { 107 | 108 | camera.far = 23.5 ; 109 | camera.updateProjectionMatrix(); 110 | 111 | params.level = 3 ; 112 | 113 | }; 114 | 115 | }; 116 | 117 | ////////////////////////////// 118 | /// GENERAL FUNCTIONS 119 | ////////////////////////////// 120 | 121 | /* 122 | update will sample the current frame's delta, then when it's time 123 | to decide if an opti/de-opti is needed, it decides over an average of 124 | the sampled deltas. 125 | */ 126 | function update( delta ) { 127 | 128 | // We don't want neither to optimize or to sample the performance 129 | // inside the caves, because it would necessarily be better, 130 | // and lead to uneven randering and opti/deopti 131 | if ( mapManager.params.currentMap != 'mountain' ) return ; 132 | 133 | if ( Date.now() > lastOptiTime + optStep ) { 134 | 135 | lastOptiTime = Date.now(); 136 | 137 | if ( optStep < 3200 ) { 138 | 139 | optStep *= 2 ; 140 | 141 | }; 142 | 143 | let total = samples.reduce( ( accu, current )=> { 144 | 145 | return accu + current ; 146 | 147 | }, 0 ); 148 | 149 | let average = total / ( samples.length - 1 ); 150 | samples = []; 151 | 152 | if ( average < DEOPTFPS && 153 | params.level != 0 && 154 | params.attempts[ params.level - 1 ] <= 2 ) { 155 | 156 | deOptimize(); 157 | 158 | } else if ( average > OPTFPS ) { 159 | 160 | params.attempts[ params.level ] ++ ; // record the failure of the current opti level 161 | optimize(); 162 | 163 | }; 164 | 165 | } else { 166 | 167 | samples.push( delta ); 168 | 169 | }; 170 | 171 | }; 172 | 173 | // 174 | 175 | return { 176 | params, 177 | update 178 | }; 179 | 180 | }; 181 | -------------------------------------------------------------------------------- /public/js/SocketIO.js: -------------------------------------------------------------------------------- 1 | 2 | function SocketIO() { 3 | 4 | var playerInfo; 5 | 6 | var socket = io( 'http://edelweiss.32x.io' /* 'http://edelweiss-stage.herokuapp.com' */ ); 7 | 8 | function joinGame( id, pass, name ) { 9 | 10 | playerInfo = { 11 | 12 | id, pass, name 13 | 14 | }; 15 | 16 | setInterval( function() { 17 | 18 | charaAnim.getPlayerState( playerInfo ); 19 | 20 | socket.emit( 'playerInfo', playerInfo ); 21 | 22 | }, 300 ); 23 | 24 | }; 25 | 26 | function onPlayerUpdates( handler ) { 27 | socket.on( 'playerInfo', handler ); 28 | }; 29 | 30 | function onPlayerDisconnects( handler ) { 31 | socket.on( 'playerLeft', handler ); 32 | }; 33 | 34 | return { 35 | joinGame, 36 | onPlayerUpdates, 37 | onPlayerDisconnects 38 | }; 39 | 40 | }; 41 | -------------------------------------------------------------------------------- /public/js/Stamina.js: -------------------------------------------------------------------------------- 1 | 2 | function Stamina() { 3 | 4 | const domBar = document.getElementById('stamina-bar'); 5 | 6 | const STARTSTAMINA = 2 ; // max 9 7 | const TOLERANCE = 0.2 ; 8 | 9 | var params = { 10 | stamina: 0, 11 | maxStamina: 0, 12 | playerKnowsStamina: false 13 | }; 14 | 15 | var gauges = []; 16 | 17 | //// INIT 18 | 19 | // Create a stamina section for each level 20 | for ( let i=0 ; i < STARTSTAMINA ; i++ ) { 21 | incrementMaxStamina(); 22 | }; 23 | 24 | function incrementMaxStamina() { 25 | 26 | let divSection = document.createElement('DIV'); 27 | divSection.classList.add('stamina-section'); 28 | domBar.append( divSection ); 29 | 30 | let divGauge = document.createElement('DIV'); 31 | divGauge.classList.add('stamina-gauge'); 32 | divSection.append( divGauge ); 33 | 34 | gauges.unshift( divGauge ); 35 | 36 | params.stamina = gauges.length ; 37 | params.maxStamina = gauges.length ; 38 | 39 | // Make the "new" section blink (in fact, the first one). 40 | if ( clock.elapsedTime > 5 ) { 41 | 42 | let sections = document.querySelectorAll( '.stamina-section' ); 43 | 44 | sections[ 0 ].classList.remove( 'show-stamina' ); 45 | 46 | setTimeout( ()=> { 47 | sections[ 0 ].classList.add( 'show-stamina' ); 48 | }, 100); 49 | 50 | }; 51 | 52 | }; 53 | 54 | // 55 | 56 | function update( mustUpdateDom ) { 57 | 58 | if ( mustUpdateDom ) { 59 | 60 | updateDom(); 61 | 62 | }; 63 | 64 | }; 65 | 66 | /////////////////////////// 67 | // DOM STAMINA BAR UPDATE 68 | /////////////////////////// 69 | 70 | function updateDom() { 71 | 72 | gauges.forEach( ( domGauge, i )=> { 73 | 74 | let a = ( ( params.stamina * ( params.maxStamina + TOLERANCE ) ) / params.maxStamina ) - i - TOLERANCE ; 75 | let b = Math.max( Math.min( a, 1 ), 0 ); 76 | domGauge.style.width = `${ b * 100 }%` ; 77 | 78 | // domGauge.style.width = `${ Math.max( Math.min( params.stamina - i, 1 ), 0 ) * 100 }%` ; 79 | 80 | }); 81 | 82 | }; 83 | 84 | ///////////////////////////////// 85 | /// STAMINA LEVEL OPERATIONS 86 | ///////////////////////////////// 87 | 88 | // Called by the controler module when player make movements 89 | function reduceStamina( factor, update ) { 90 | 91 | params.stamina -= factor ; 92 | 93 | // Check if stamina is bellow 0 + tolerance 94 | if ( ( params.stamina * params.maxStamina ) / ( params.maxStamina - TOLERANCE ) < TOLERANCE ) { 95 | 96 | // make stamina bar UI blink 97 | domBar.classList.add( 'blink-stamina' ); 98 | 99 | let domSections = document.querySelectorAll('.stamina-section'); 100 | 101 | domSections.forEach( (domSection)=> { 102 | 103 | domSection.style.backgroundColor = '#c7001e' ; 104 | 105 | }); 106 | 107 | }; 108 | 109 | if ( params.stamina < 0 ) { 110 | 111 | params.stamina = 0 ; 112 | 113 | if ( !params.playerKnowsStamina ) { 114 | 115 | params.playerKnowsStamina = true ; 116 | 117 | interaction.showMessage( 'Out of stamina !
Go back on the ground' ); 118 | 119 | }; 120 | 121 | }; 122 | 123 | if ( update ) { 124 | 125 | updateDom(); 126 | 127 | }; 128 | 129 | }; 130 | 131 | // Called by Controler when the user is on the ground, 132 | // so the user regain all their stamina and can start 133 | // climbing again 134 | function resetStamina() { 135 | 136 | if ( params.stamina != params.maxStamina ) { 137 | 138 | domBar.classList.remove( 'blink-stamina' ); 139 | 140 | let domSections = document.querySelectorAll('.stamina-section'); 141 | 142 | domSections.forEach( (domSection)=> { 143 | 144 | domSection.style.backgroundColor = 'rgba(153, 228, 78, 0.294)' ; 145 | 146 | }); 147 | 148 | }; 149 | 150 | params.stamina = params.maxStamina ; 151 | 152 | }; 153 | 154 | // 155 | 156 | return { 157 | params, 158 | reduceStamina, 159 | resetStamina, 160 | incrementMaxStamina, 161 | update 162 | }; 163 | 164 | }; -------------------------------------------------------------------------------- /public/js/Utils.js: -------------------------------------------------------------------------------- 1 | 2 | function Utils() { 3 | 4 | // This function takes any number, 5 | // and returns a angle value in the range -PI to PI. 6 | function toPiRange( rad ) { 7 | 8 | rad = rad % (Math.PI * 2) ; 9 | 10 | if ( rad > Math.PI || rad < -Math.PI ) { 11 | 12 | return ( ( - Math.PI ) - ( Math.PI - Math.abs(rad) ) ) * Math.sign( rad ) ; 13 | 14 | } else { 15 | 16 | return rad ; 17 | 18 | }; 19 | 20 | }; 21 | 22 | // This is a linear interpolation able to interpolate two Euler angles, 23 | // which values loop from -PI to PI. 24 | function lerpAngles( vStart, vEnd, t ) { 25 | 26 | // Check if there is a problem of lerp going above PI or bellow -PI 27 | if ( Math.abs( vStart - vEnd ) > Math.PI / 2 ) { 28 | 29 | // The smallest value is added 2 * PI to do the lerp, 30 | // then reduced to PI range. 31 | if ( vStart < vEnd ) { 32 | 33 | return toPiRange( lerp( vStart + (Math.PI * 2), vEnd, t ) ); 34 | 35 | } else { 36 | 37 | return toPiRange( lerp( vStart, vEnd + (Math.PI * 2), t ) ); 38 | 39 | }; 40 | 41 | }; 42 | 43 | return lerp( vStart, vEnd, t ); 44 | 45 | }; 46 | 47 | // Linear interpolation function 48 | // It return the value between vStart and vEnd pointed by 49 | // the floating point t. 50 | // t = 0 ==> return vStart 51 | // t = 1 ==> return vEnd 52 | function lerp( vStart, vEnd, t ) { 53 | 54 | return ( ( vEnd - vStart ) * t ) + vStart ; 55 | 56 | }; 57 | 58 | // Returns the value between 0 and 1 representing 59 | // the interpolant point between vStart and vEnd on v. 60 | function interp( vStart, v, vEnd ) { 61 | return ( v - vStart ) / ( vEnd - vStart ); 62 | }; 63 | 64 | // Get the minimal difference (delta) between two radians 65 | // ex : -2.5 <--> 2.5 ? => 1.28 66 | function minDiffRadians( rad1, rad2 ) { 67 | return Math.atan2( Math.sin( rad1 - rad2), Math.cos( rad1 - rad2) ); 68 | }; 69 | 70 | // returns the distance between two vectors 2 or 3 71 | function distanceVecs( vec1, vec2 ) { 72 | 73 | return Math.sqrt( 74 | Math.pow( vec1.x - vec2.x, 2 ) + 75 | Math.pow( vec1.y - vec2.y, 2 ) + 76 | Math.pow( vec1.z - vec2.z, 2 ) 77 | ); 78 | 79 | }; 80 | 81 | function vecEquals( vec1, vec2 ) { 82 | 83 | return ( 84 | 85 | vec1.x == vec2.x && 86 | vec1.y == vec2.y && 87 | vec1.z == vec2.z 88 | 89 | ); 90 | 91 | }; 92 | 93 | /* MAKC */ 94 | 95 | // This function returns placeholder display name from https://www.youtube.com/watch?v=gzBZFArR4mc list. 96 | 97 | const names = [ 98 | 'DragonKiller75', 'DragonDeesNuts', 'Sug_Madic', 99 | 'Phil_Mcrackin', 'Ice_Wallow_Come', 'Come_Stayin', 'Pen15' 100 | ]; 101 | 102 | var nameIndex = Math.floor( names.length * Math.random() ); 103 | 104 | function randomDisplayName() { 105 | 106 | return names[ nameIndex++ % names.length ]; 107 | 108 | }; 109 | 110 | // This function returns short random string. 111 | 112 | const bytes = new Uint8Array( 15 ); 113 | 114 | function randomString() { 115 | 116 | crypto.getRandomValues( bytes ); 117 | 118 | return Array.prototype.map.call( bytes, function (x) { 119 | 120 | return x.toString( 36 ) 121 | 122 | } ).join( '' ).substr( 0, 15 ); 123 | 124 | }; 125 | 126 | // This function returns certain numeric hash of the string (we want the 127 | // result % 4 to be evenly distributed when passed randomString output). 128 | function stringHash( s ) { 129 | 130 | return s.charCodeAt( 0 ) + s.charCodeAt( 1 ); 131 | 132 | }; 133 | 134 | // 135 | 136 | return { 137 | randomDisplayName, 138 | randomString, 139 | stringHash, 140 | toPiRange, 141 | lerpAngles, 142 | minDiffRadians, 143 | distanceVecs, 144 | interp, 145 | lerp, 146 | vecEquals 147 | }; 148 | 149 | }; -------------------------------------------------------------------------------- /public/js/charaAnim.js: -------------------------------------------------------------------------------- 1 | 2 | function CharaAnim( player ) { 3 | 4 | const actions = player.actions ; 5 | 6 | const group = player.charaGroup ; 7 | 8 | // get the glider object, and give it to the animation 9 | // module, the hide it from the scene. 10 | const glider = group.getObjectByName( 'glider' ); 11 | glider.visible = false; 12 | 13 | // this variable stock the state waiting to be played 14 | // after ground hitting 15 | var waitingState ; 16 | 17 | var currentClimbAction; 18 | 19 | for ( let i in actions ) { 20 | actions[ i ].setEffectiveWeight( 0 ); 21 | }; 22 | 23 | // set start action to 1 ; 24 | actions.idle.setEffectiveWeight( 1 ); 25 | actions.gliderAction.setEffectiveWeight( 1 ); 26 | 27 | // activate the glider animation, because anyway 28 | // the glider is not visible when not in use 29 | 30 | // actions.gliderAction.setEffectiveWeight( 1 ); 31 | 32 | var climbingActions = [ 33 | actions.climbUp, 34 | actions.climbDown, 35 | actions.climbLeft, 36 | actions.climbRight, 37 | actions.climbLeftUp, 38 | actions.climbLeftDown, 39 | actions.climbRightUp, 40 | actions.climbRightDown 41 | ]; 42 | 43 | /// TIMESCALE 44 | 45 | actions.gliderAction.setEffectiveTimeScale( 1.5 ); 46 | actions.haulDown.setEffectiveTimeScale( 2 ); 47 | actions.haulUp.setEffectiveTimeScale( 2 ); 48 | actions.pullUnder.setEffectiveTimeScale( 2 ); 49 | actions.landOnWall.setEffectiveTimeScale( 1 ); 50 | 51 | /// CLAMP WHEN FINISHED 52 | 53 | setLoopOnce( actions.gliderDeploy ); 54 | 55 | setLoopOnce( actions.haulDown ); 56 | setLoopOnce( actions.haulUp ); 57 | setLoopOnce( actions.pullUnder ); 58 | setLoopOnce( actions.landOnWall ); 59 | 60 | setLoopOnce( actions.jumbRise ); 61 | setLoopOnce( actions.hitGround ); 62 | setLoopOnce( actions.die ); 63 | 64 | setLoopOnce( actions.dashUp ); 65 | setLoopOnce( actions.dashDown ); 66 | setLoopOnce( actions.dashLeft ); 67 | setLoopOnce( actions.dashRight ); 68 | setLoopOnce( actions.dashDownLeft ); 69 | setLoopOnce( actions.dashDownRight ); 70 | 71 | function setLoopOnce( action ) { 72 | action.clampWhenFinished = true ; 73 | action.loop = THREE.LoopOnce ; 74 | }; 75 | 76 | // 77 | 78 | // This object stores the weight factor of each 79 | // climbing animation. It is updated when the user moves 80 | // while climbing by the function setClimbBalance. 81 | var climbDirectionPowers = { 82 | up: 0, 83 | down: 0, 84 | left: 0, 85 | right: 0 86 | }; 87 | 88 | var dashDirectionPowers = { 89 | up: 0, 90 | down: 0, 91 | left: 0, 92 | right: 0 93 | }; 94 | 95 | var currentState = 'idleGround' ; 96 | 97 | /* POSSIBLE STATES : 98 | 99 | idleGround 100 | idleClimb 101 | 102 | runningSlow 103 | 104 | climbing 105 | slipping 106 | landingOnWall 107 | 108 | gliding 109 | jumping 110 | falling 111 | hittingGround 112 | dying 113 | 114 | dashing 115 | chargingDash 116 | 117 | haulingDown 118 | haulingUp 119 | switchInward 120 | switchOutward 121 | pullingUnder 122 | 123 | */ 124 | 125 | // 126 | 127 | /* 128 | actionsToFadeIn and actionsToFadeOut store 129 | objects like this : 130 | { 131 | actionName, 132 | targetWeight, 133 | fadeSpeed (between 0 and 1) 134 | } 135 | This is used in the update function to tween the 136 | weight of actions 137 | */ 138 | 139 | var actionsToFadeIn = []; 140 | var actionsToFadeOut = []; 141 | 142 | var moveSpeedRatio ; 143 | 144 | // 145 | 146 | /* 147 | Creation of animation of stamina charge, that will be placed 148 | within the character group inside the scene. They will be 149 | hided, and showed when the player charges a dash 150 | */ 151 | var chargeGroup = new THREE.Group(); 152 | chargeGroup.visible = false ; 153 | group.add( chargeGroup ); 154 | 155 | var dashMaterial = new THREE.MeshBasicMaterial( {color: 0x2fde2c} ); 156 | 157 | var chargeCubes = []; 158 | 159 | for ( let i = 0 ; i < 20 ; i++ ) { 160 | 161 | var geometry = new THREE.BoxBufferGeometry( 0.03, 0.03, 0.03 ); 162 | var cube = new THREE.Mesh( geometry, dashMaterial ); 163 | 164 | cube.position.y = ( Math.random() * 0.35 ) + 0.1 ; 165 | cube.position.x = ( Math.random() * 0.15 ) + 0.05 ; 166 | 167 | chargeCubes.push( cube ); 168 | 169 | let group = new THREE.Group(); 170 | 171 | group.rotation.y = Math.random() * ( Math.PI * 2 ); 172 | 173 | chargeGroup.add( group ); 174 | group.add( cube ); 175 | 176 | }; 177 | 178 | /* 179 | Direction pointer animation, to tell the player where 180 | they are going to dash toward 181 | */ 182 | 183 | let pointerContainer = new THREE.Group(); 184 | pointerContainer.visible = false ; 185 | 186 | let pointer = new THREE.Mesh( 187 | new THREE.ConeBufferGeometry( 0.1, 0.23, 4 ), 188 | dashMaterial 189 | ); 190 | 191 | pointer.rotation.x -= Math.PI / 2 ; 192 | 193 | var pointerTarget = new THREE.Vector3(); 194 | 195 | pointerContainer.add( pointer ); 196 | scene.add( pointerContainer ); 197 | 198 | // 199 | 200 | function update( delta ) { 201 | 202 | if ( Object.keys( actions ).length == 0 ) return 203 | 204 | moveSpeedRatio = delta / ( 1 / 60 ) ; 205 | 206 | if( player.target ) { 207 | 208 | var d = player.position.distanceTo( player.target ); 209 | 210 | if( ( 0.2 > d ) || ( d > 2.0 ) ) { 211 | 212 | player.position.copy( player.target ); 213 | 214 | } else { 215 | 216 | var k = 0.1 * moveSpeedRatio; k /= (1 + k); 217 | 218 | player.position.multiplyScalar( 1 - k ).addScaledVector( player.target, k ); 219 | 220 | }; 221 | 222 | }; 223 | 224 | // update the dash charging animation 225 | 226 | if ( currentState == 'chargingDash' ) { 227 | 228 | chargeGroup.visible = true ; 229 | 230 | chargeGroup.children.forEach( (child)=> { 231 | child.rotation.y += 0.06 ; 232 | }); 233 | 234 | chargeCubes.forEach( (mesh)=> { 235 | mesh.scale.setScalar( (Math.sin(Date.now() / 20) * 0.2) + 0.7 ); 236 | }); 237 | 238 | /* 239 | Point Animation 240 | we don't want to show a pointer if the player is 241 | charging without pointing to a direction 242 | */ 243 | if ( input.moveKeys.length > 0 ) { 244 | 245 | pointerContainer.visible = true ; 246 | 247 | pointerTarget.copy( player.position ); 248 | pointerTarget.y += 0.35 ; 249 | pointerContainer.position.copy( pointerTarget ); 250 | 251 | pointerContainer.position.addScaledVector( controler.dashVec, 0.4 ); 252 | pointerContainer.lookAt( pointerTarget ); 253 | 254 | pointer.scale.setScalar( (Math.sin(Date.now() / 100) * 0.15) + 0.8 ); 255 | 256 | } else { 257 | 258 | pointerContainer.visible = false ; 259 | 260 | }; 261 | 262 | } else { 263 | 264 | chargeGroup.visible = false ; 265 | pointerContainer.visible = false ; 266 | 267 | }; 268 | 269 | // handle the hittingGround action, that makes everything 270 | // standby until it's played 271 | if ( currentState == 'hittingGround' && 272 | actions.hitGround.time > ( actions.hitGround._clip.duration * 0.7 ) ) { 273 | 274 | if ( waitingState ) { 275 | setState( waitingState ); 276 | }; 277 | 278 | }; 279 | 280 | if ( actionsToFadeIn.length > 0 ) { 281 | 282 | actionsToFadeIn.forEach( (action)=> { 283 | 284 | actions[ action.actionName ].setEffectiveWeight( 285 | actions[ action.actionName ].weight + ( action.fadeSpeed * moveSpeedRatio ) 286 | ); 287 | 288 | if ( actions[ action.actionName ].weight >= 289 | action.targetWeight ) { 290 | 291 | actions[ action.actionName ].setEffectiveWeight( action.targetWeight ); 292 | 293 | actionsToFadeIn.splice( actionsToFadeIn.indexOf( action ), 1 ); 294 | 295 | }; 296 | 297 | }); 298 | 299 | }; 300 | 301 | if ( actionsToFadeOut.length > 0 ) { 302 | 303 | actionsToFadeOut.forEach( (action)=> { 304 | 305 | actions[ action.actionName ].setEffectiveWeight( 306 | actions[ action.actionName ].weight - ( action.fadeSpeed * moveSpeedRatio ) 307 | ); 308 | 309 | if ( actions[ action.actionName ].weight <= 0 ) { 310 | 311 | actions[ action.actionName ].setEffectiveWeight( 0 ); 312 | 313 | actionsToFadeOut.splice( actionsToFadeOut.indexOf( action ), 1 ); 314 | 315 | }; 316 | 317 | }); 318 | 319 | }; 320 | 321 | }; 322 | 323 | // 324 | 325 | function setFadeIn( actionName, targetWeight, fadeSpeed ) { 326 | 327 | actionsToFadeIn.push({ 328 | actionName, 329 | targetWeight, 330 | fadeSpeed 331 | }); 332 | 333 | // Delete the starting action from the fadeOut list, 334 | // or it would fadein and fadeout at the same time. 335 | actionsToFadeOut.forEach( (action, i)=> { 336 | 337 | if ( action.actionName == actionName ) { 338 | actionsToFadeOut.splice( i, 1 ); 339 | }; 340 | 341 | }); 342 | 343 | }; 344 | 345 | // 346 | 347 | function setFadeOut( actionName, fadeSpeed ) { 348 | 349 | actionsToFadeOut.push({ 350 | actionName, 351 | fadeSpeed 352 | }); 353 | 354 | // Delete the starting action from the fadeIn list, 355 | // or it would fadein and fadeout at the same time. 356 | actionsToFadeIn.forEach( (action, i)=> { 357 | 358 | if ( action.actionName == actionName ) { 359 | actionsToFadeIn.splice( i, 1 ); 360 | }; 361 | 362 | }); 363 | 364 | }; 365 | 366 | // 367 | 368 | function setCharaRot( angle ) { 369 | 370 | player.charaGroup.rotation.y = angle ; 371 | 372 | }; 373 | 374 | // 375 | 376 | function setState( newState ) { 377 | 378 | if ( currentState == 'hittingGround' && 379 | actions.hitGround.time <= ( actions.hitGround._clip.duration * 0.7 ) ) { 380 | 381 | waitingState = newState ; 382 | 383 | return 384 | }; 385 | 386 | if ( newState == 'dying' ) { 387 | 388 | if ( currentState == 'dying' ) return 389 | 390 | setTimeout( ()=> { 391 | setState( 'respawn' ); 392 | }, 1500 ); 393 | 394 | }; 395 | 396 | if ( currentState == "dying" && newState != 'respawn' ) { 397 | return 398 | }; 399 | 400 | if ( currentState != newState ) { 401 | 402 | // FADE IN 403 | 404 | switch ( newState ) { 405 | 406 | case 'running' : 407 | setFadeIn( 'run', 1, 0.1 ); 408 | break; 409 | 410 | case 'idleGround' : 411 | actions.idle.reset(); 412 | setFadeIn( 'idle', 1, 0.1 ); 413 | break; 414 | 415 | case 'idleClimb' : 416 | actions.climbIdle.reset(); 417 | setFadeIn( 'climbIdle', 1, 0.1 ); 418 | break; 419 | 420 | case 'jumping' : 421 | setFadeIn( 'jumbRise', 1, 0.1 ); 422 | break; 423 | 424 | case 'haulingDown' : 425 | actions.haulDown.reset(); 426 | setFadeIn( 'haulDown', 1, 0.1 ); 427 | break; 428 | 429 | case 'haulingUp' : 430 | actions.haulUp.reset(); 431 | setFadeIn( 'haulUp', 1, 0.1 ); 432 | break; 433 | 434 | case 'pullingUnder' : 435 | actions.pullUnder.reset(); 436 | setFadeIn( 'pullUnder', 1, 0.1 ); 437 | break; 438 | 439 | case 'falling' : 440 | setFadeIn( 'fall', 1, 0.1 ); 441 | break; 442 | 443 | case 'dying' : 444 | setFadeIn( 'die', 1, 1 ); 445 | break; 446 | 447 | case 'slipping' : 448 | setFadeIn( 'slip', 1, 0.1 ); 449 | break; 450 | 451 | case 'gliding' : 452 | actions.glide.reset(); 453 | actions.glide.time = 0.5 ; 454 | glider.visible = true ; 455 | actions.gliderDeploy.reset(); 456 | actions.gliderAction.setEffectiveWeight( 0 ); 457 | setFadeIn( 'gliderDeploy', 1, 1 ); 458 | setFadeIn( 'gliderAction', 1, 0.1 ); 459 | setFadeIn( 'glide', 1, 0.2 ); 460 | break; 461 | 462 | case 'chargingDash' : 463 | setFadeIn( 'chargeDash', 1, 0.1 ); 464 | break; 465 | 466 | case 'switchingInward' : 467 | setFadeIn( 'switchDirection', 1, 0.1 ); 468 | break; 469 | 470 | case 'switchingOutward' : 471 | setFadeIn( 'switchDirection', 1, 0.1 ); 472 | break; 473 | 474 | case 'hittingGround' : 475 | actions.hitGround.reset(); 476 | setFadeIn( 'hitGround', 1, 0.1 ); 477 | break; 478 | 479 | }; 480 | 481 | // FADE OUT 482 | 483 | switch ( currentState ) { 484 | 485 | case 'idleGround' : 486 | setFadeOut( 'idle', 0.1 ); 487 | break; 488 | 489 | case 'idleClimb' : 490 | setFadeOut( 'climbIdle', 0.1 ); 491 | break; 492 | 493 | case 'running' : 494 | setFadeOut( 'run', 0.1 ); 495 | break; 496 | 497 | case 'jumping' : 498 | setFadeOut( 499 | 'jumbRise', 500 | newState == 'landingOnWall' ? 0.5 : 0.1 501 | ); 502 | break; 503 | 504 | case 'haulingDown' : 505 | setFadeOut( 'haulDown', 0.1 ); 506 | break; 507 | 508 | case 'haulingUp' : 509 | setFadeOut( 'haulUp', 0.1 ); 510 | break; 511 | 512 | case 'pullingUnder' : 513 | setFadeOut( 'pullUnder', 0.1 ); 514 | break; 515 | 516 | case 'slipping' : 517 | if ( newState == 'jumping' ) { 518 | setFadeOut( 'slip', 0.2 ); 519 | } else { 520 | setFadeOut( 'slip', 0.1 ); 521 | }; 522 | break; 523 | 524 | case 'dying' : 525 | setFadeOut( 'die', 1 ); 526 | break; 527 | 528 | case 'falling' : 529 | setFadeOut( 530 | 'fall', 531 | newState == 'landingOnWall' ? 0.5 : 0.1 532 | ); 533 | break; 534 | 535 | case 'switchingInward' : 536 | setFadeOut( 'switchDirection', 0.1 ); 537 | break; 538 | 539 | case 'switchingOutward' : 540 | setFadeOut( 'switchDirection', 0.1 ); 541 | break; 542 | 543 | case 'gliding' : 544 | glider.visible = false ; 545 | setFadeOut( 546 | 'glide', 547 | newState == 'landingOnWall' ? 0.5 : 0.1 548 | ); 549 | break; 550 | 551 | case 'chargingDash' : 552 | setFadeOut( 'chargeDash', 0.1 ); 553 | break; 554 | 555 | case 'climbing' : 556 | setFadeOut( 'climbUp', 0.1 ); 557 | setFadeOut( 'climbDown', 0.1 ); 558 | setFadeOut( 'climbLeft', 0.1 ); 559 | setFadeOut( 'climbRight', 0.1 ); 560 | setFadeOut( 'climbLeftUp', 0.1 ); 561 | setFadeOut( 'climbRightUp', 0.1 ); 562 | setFadeOut( 'climbLeftDown', 0.1 ); 563 | setFadeOut( 'climbRightDown', 0.1 ); 564 | currentClimbAction = undefined ; 565 | break; 566 | 567 | case 'dashing' : 568 | setFadeOut( 'dashUp', 0.1 ); 569 | setFadeOut( 'dashDown', 0.1 ); 570 | setFadeOut( 'dashLeft', 0.1 ); 571 | setFadeOut( 'dashRight', 0.1 ); 572 | setFadeOut( 'dashDownLeft', 0.1 ); 573 | setFadeOut( 'dashDownRight', 0.1 ); 574 | break; 575 | 576 | case 'hittingGround' : 577 | setFadeOut( 'hitGround', 0.1 ); 578 | break; 579 | 580 | }; 581 | 582 | currentState = newState ; 583 | 584 | }; 585 | 586 | }; 587 | 588 | // This function combute the direction, to call a passed 589 | // value attribution funcion with the right arguments. 590 | function callWithDirection( fn, faceDirection ) { 591 | 592 | 593 | switch ( faceDirection ) { 594 | 595 | case 'up' : 596 | fn( 'up', Math.PI ); 597 | fn( 'down', 0 ); 598 | fn( 'left', -Math.PI / 2 ); 599 | fn( 'right', Math.PI / 2 ); 600 | break; 601 | 602 | case 'down' : 603 | fn( 'up', Math.PI ); 604 | fn( 'down', 0 ); 605 | fn( 'left', Math.PI / 2 ); 606 | fn( 'right', -Math.PI / 2 ); 607 | break; 608 | 609 | case 'left' : 610 | fn( 'up', -Math.PI / 2 ); 611 | fn( 'down', Math.PI / 2 ); 612 | fn( 'left', 0 ); 613 | fn( 'right', Math.PI ); 614 | break; 615 | 616 | case 'right' : 617 | fn( 'up', Math.PI / 2 ); 618 | fn( 'down', -Math.PI / 2 ); 619 | fn( 'left', Math.PI ); 620 | fn( 'right', 0 ); 621 | break; 622 | 623 | }; 624 | 625 | }; 626 | 627 | // Here we need to compute the climbing direction from the 628 | // arguments, to balance climbing-up, climbing-right etc.. 629 | function setClimbBalance( faceDirection, moveDirection, speed ) { 630 | 631 | 632 | if ( currentState == 'climbing' ) { 633 | 634 | callWithDirection( setClimbDirection, faceDirection ); 635 | 636 | climbingActions.forEach( (action)=> { 637 | action.setEffectiveTimeScale( speed + 0.7 ); 638 | }); 639 | 640 | actions.climbUp.setEffectiveTimeScale( speed + 0.18 ); 641 | actions.climbDown.setEffectiveTimeScale( speed + 0.18 ); 642 | 643 | function switchClimbAction( newAction ) { 644 | 645 | if ( newAction != currentClimbAction ) { 646 | 647 | if ( currentClimbAction ) { 648 | 649 | setFadeOut( currentClimbAction._clip.name, 0.1 ); 650 | 651 | }; 652 | 653 | setFadeIn( newAction._clip.name, 1, 0.1 ); 654 | 655 | currentClimbAction = newAction ; 656 | 657 | }; 658 | 659 | }; 660 | 661 | if ( climbDirectionPowers.up > 0.65 ) { 662 | 663 | switchClimbAction( actions.climbUp ); 664 | 665 | } else if ( climbDirectionPowers.down > 0.65 ) { 666 | 667 | switchClimbAction( actions.climbDown ); 668 | 669 | } else if ( climbDirectionPowers.left > 0.65 ) { 670 | 671 | switchClimbAction( actions.climbLeft ); 672 | 673 | } else if ( climbDirectionPowers.right > 0.65 ) { 674 | 675 | switchClimbAction( actions.climbRight ); 676 | 677 | } else if ( climbDirectionPowers.up > 0 ) { 678 | 679 | if ( climbDirectionPowers.right > 0 ) { 680 | 681 | switchClimbAction( actions.climbRightUp ); 682 | 683 | } else { 684 | 685 | switchClimbAction( actions.climbLeftUp ); 686 | 687 | }; 688 | 689 | } else { 690 | 691 | if ( climbDirectionPowers.right > 0 ) { 692 | 693 | switchClimbAction( actions.climbRightDown ); 694 | 695 | } else { 696 | 697 | switchClimbAction( actions.climbLeftDown ); 698 | 699 | }; 700 | 701 | }; 702 | 703 | }; 704 | 705 | // Attribute a value between 0 and 1 to a climbing animation according 706 | // to the difference between the requested angle and the target angle 707 | // that would make this action 100% played 708 | function setClimbDirection( directionName, target ) { 709 | 710 | climbDirectionPowers[ directionName ] = Math.max( 711 | ( 1 - 712 | ( Math.abs( utils.minDiffRadians( target, moveDirection ) ) / 713 | (Math.PI / 2) ) 714 | ) 715 | , 0 ); 716 | 717 | }; 718 | 719 | }; 720 | 721 | // 722 | 723 | function setDashBalance( faceDirection, moveDirection ) { 724 | 725 | if ( currentState != 'dashing' ) { 726 | 727 | callWithDirection( setDashDirection, faceDirection ); 728 | 729 | // This part plays the set of upper dash animations 730 | if ( dashDirectionPowers.up >= 0 ) { 731 | 732 | actions.dashUp.reset(); 733 | actions.dashLeft.reset(); 734 | actions.dashRight.reset(); 735 | 736 | actions.dashUp.setEffectiveWeight( dashDirectionPowers.up ); 737 | actions.dashLeft.setEffectiveWeight( dashDirectionPowers.left ); 738 | actions.dashRight.setEffectiveWeight( dashDirectionPowers.right ); 739 | 740 | // This part plays the bottom dash animations 741 | } else { 742 | 743 | actions.dashUp.reset(); 744 | actions.dashDownLeft.reset(); 745 | actions.dashDownRight.reset(); 746 | 747 | actions.dashUp.setEffectiveWeight( dashDirectionPowers.up ); 748 | actions.dashDownLeft.setEffectiveWeight( dashDirectionPowers.left ); 749 | actions.dashDownRight.setEffectiveWeight( dashDirectionPowers.right ); 750 | 751 | }; 752 | 753 | }; 754 | 755 | function setDashDirection( directionName, target ) { 756 | 757 | dashDirectionPowers[ directionName ] = Math.max( 758 | ( 1 - 759 | ( Math.abs( utils.minDiffRadians( target, moveDirection ) ) / 760 | (Math.PI / 2) ) 761 | ) 762 | , 0 ); 763 | 764 | }; 765 | 766 | }; 767 | 768 | // 769 | 770 | function setGlider( gliderMesh ) { 771 | glider = gliderMesh ; 772 | glider.visible = false ; 773 | }; 774 | 775 | // setting 'climbing' and 'dashing' states requires these parameters 776 | 777 | var currentFaceDirection = '', currentMoveDirection = 0, currentSpeed = 0; 778 | 779 | function getPlayerState( data ) { 780 | 781 | // position 782 | 783 | data.x = player.position.x; 784 | data.y = player.position.y; 785 | data.z = player.position.z; 786 | 787 | // rotation 788 | 789 | data.r = group.rotation.y; 790 | 791 | // animation 792 | 793 | data.a = currentState; 794 | data.f = currentFaceDirection; 795 | data.m = currentMoveDirection; 796 | data.s = currentSpeed; 797 | 798 | }; 799 | 800 | // 801 | 802 | function setPlayerState( data ) { 803 | 804 | ( player.target || player.position ).set( data.x, data.y, data.z ); 805 | 806 | group.rotation.y = data.r; 807 | 808 | switch( data.a ) { 809 | case 'climbing': 810 | climb( data.f || undefined, data.m, data.s ); 811 | break; 812 | case 'dashing': 813 | dash( data.f || undefined, data.m ); 814 | break; 815 | case 'hittingGround': 816 | hitGround(); 817 | break; 818 | default: 819 | setState( data.a ); 820 | break; 821 | }; 822 | }; 823 | 824 | /////////////////////// 825 | /// ACTIONS SETTERS 826 | /////////////////////// 827 | 828 | function climb( faceDirection, moveDirection, speed ) { 829 | currentFaceDirection = faceDirection || ''; 830 | currentMoveDirection = moveDirection; 831 | currentSpeed = speed; 832 | setState( 'climbing' ); 833 | setClimbBalance( faceDirection, moveDirection, speed ); 834 | }; 835 | 836 | function dash( faceDirection, moveDirection ) { 837 | currentFaceDirection = faceDirection || ''; 838 | currentMoveDirection = moveDirection; 839 | setDashBalance( faceDirection, moveDirection ); 840 | // We set the dashing state after, because we want 841 | // the dash balance to be set only when the dashing 842 | // animation is not played 843 | setState( 'dashing' ); 844 | }; 845 | 846 | function run() { 847 | setState( 'running' ); 848 | }; 849 | 850 | function idleClimb() { 851 | setState( 'idleClimb' ); 852 | }; 853 | 854 | function idleGround() { 855 | setState( 'idleGround' ); 856 | }; 857 | 858 | function glide() { 859 | setState('gliding'); 860 | }; 861 | 862 | function chargeDash() { 863 | setState('chargingDash'); 864 | }; 865 | 866 | function jump() { 867 | setState('jumping'); 868 | }; 869 | 870 | function fall() { 871 | setState('falling'); 872 | }; 873 | 874 | function slip() { 875 | setState('slipping'); 876 | }; 877 | 878 | function haulDown() { 879 | setState('haulingDown'); 880 | }; 881 | 882 | function haulUp() { 883 | setState('haulingUp'); 884 | }; 885 | 886 | function switchOutward() { 887 | setState('switchingOutward'); 888 | }; 889 | 890 | function switchInward() { 891 | setState('switchingInward'); 892 | }; 893 | 894 | function pullUnder() { 895 | setState('pullingUnder'); 896 | }; 897 | 898 | function hitGround() { 899 | groundHit = false ; 900 | setState('hittingGround'); 901 | }; 902 | 903 | function die() { 904 | setState('dying'); 905 | }; 906 | 907 | function respawn() { 908 | setState( 'respawn' ); 909 | }; 910 | 911 | // 912 | 913 | return { 914 | setPlayerState, 915 | getPlayerState, 916 | update, 917 | setCharaRot, 918 | group, 919 | hitGround, 920 | die, 921 | run, 922 | idleClimb, 923 | climb, 924 | idleGround, 925 | glide, 926 | dash, 927 | chargeDash, 928 | jump, 929 | fall, 930 | slip, 931 | haulDown, 932 | haulUp, 933 | switchOutward, 934 | switchInward, 935 | pullUnder, 936 | respawn 937 | }; 938 | 939 | }; -------------------------------------------------------------------------------- /public/js/gameState.js: -------------------------------------------------------------------------------- 1 | 2 | function GameState() { 3 | 4 | const domStartMenu = document.getElementById('start-menu'); 5 | const domStartButton = document.getElementById('start-button'); 6 | const domStartLoaded = document.getElementById( 'start-loaded' ); 7 | const domStartBack = document.getElementById('start-background'); 8 | const domHomepageLoadingIcon = document.getElementById('homepage-loading-icon'); 9 | 10 | const domTitleBackground = document.getElementById('title-background'); 11 | 12 | const domStaminaBar = document.getElementById('stamina-bar'); 13 | 14 | const domJoystickContainer = document.getElementById('joystick-container'); 15 | const domActionButton = document.getElementById('action-button'); 16 | 17 | const domBlackScreen = document.getElementById('black-screen'); 18 | 19 | // will hold the sceneGraphs of the caves as well 20 | var sceneGraphs = { 21 | mountain: undefined 22 | }; 23 | 24 | var params = { 25 | isGamePaused: true, 26 | isCrashing: false, 27 | isDying: false 28 | }; 29 | 30 | var respawnPos = new THREE.Vector3(); 31 | var gateTilePos = new THREE.Vector3(); 32 | 33 | var enterGateTime ; 34 | const ENTER_GATE_DURATION = 300; 35 | 36 | var loadingFinished = false; 37 | 38 | /// EVENTS 39 | 40 | domStartButton.addEventListener( 'touchstart', (e)=> { 41 | 42 | if ( loadingFinished ) { 43 | startGame( true ); 44 | }; 45 | 46 | }); 47 | 48 | domStartButton.addEventListener( 'click', (e)=> { 49 | 50 | if ( loadingFinished ) { 51 | startGame(); 52 | }; 53 | 54 | }); 55 | 56 | // LOADING MANAGER 57 | 58 | THREE.DefaultLoadingManager.onStart = function ( url, itemsLoaded, itemsTotal ) { 59 | 60 | // console.log( `${ (itemsLoaded / itemsTotal) * 100 }%` ) 61 | // console.log( 'Started loading file: ' + url + '.\nLoaded ' + itemsLoaded + ' of ' + itemsTotal + ' files.' ); 62 | updateLoadingBar( (itemsLoaded / itemsTotal) * 100 ); 63 | }; 64 | 65 | THREE.DefaultLoadingManager.onLoad = function ( ) { 66 | 67 | // console.log( 'Loading Complete!'); 68 | unlockStartButton(); 69 | 70 | }; 71 | 72 | THREE.DefaultLoadingManager.onProgress = function ( url, itemsLoaded, itemsTotal ) { 73 | 74 | // console.log( `${ (itemsLoaded / itemsTotal) * 100 }%` ) 75 | // console.log( 'Loading file: ' + url + '.\nLoaded ' + itemsLoaded + ' of ' + itemsTotal + ' files.' ); 76 | updateLoadingBar( (itemsLoaded / itemsTotal) * 100 ); 77 | }; 78 | 79 | // 80 | 81 | function updateLoadingBar( percent ) { 82 | 83 | domStartLoaded.style.width = percent + '%' ; 84 | 85 | if ( percent >= 100 ) { 86 | unlockStartButton(); 87 | }; 88 | 89 | }; 90 | 91 | // 92 | 93 | function unlockStartButton() { 94 | 95 | domStartButton.style.color = "#111111" ; 96 | domStartLoaded.style.backgroundColor = "#111111" ; 97 | loadingFinished = true ; 98 | 99 | }; 100 | 101 | /// LAYOUT INIT 102 | 103 | domStartMenu.style.display = 'flex'; 104 | domHomepageLoadingIcon.style.display = 'none'; 105 | 106 | fileLoader.load( 'https://edelweiss-game.s3.eu-west-3.amazonaws.com/mountain.json', function( file ) { 107 | 108 | var graph = parseJSON( file ); 109 | 110 | // Initialize atlas with the scene graph 111 | atlas.init( graph ); 112 | 113 | // store this sceneGraph into the graphs object 114 | sceneGraphs.mountain = graph ; 115 | 116 | }); 117 | 118 | // 119 | 120 | function debugLoadGraph( graphData, graphName ) { 121 | 122 | var sceneGraph = (typeof graphData === 'string' ) ? parseJSON( graphData ) : graphData; 123 | 124 | console.log( `Loaded ${ graphName } graph:`, sceneGraph ); 125 | 126 | // at this point the game is already started so we 127 | // want to load the json as if it was another cave 128 | 129 | params.isGamePaused = true ; 130 | 131 | sceneGraphs[ graphName ] = sceneGraph; 132 | 133 | atlas.switchGraph( graphName, null, function() { 134 | 135 | soundMixer.animEnd(); 136 | 137 | // try to place the player on the ground 138 | 139 | var pos; 140 | 141 | for ( let tilesGraphStage of sceneGraph.tilesGraph ) { 142 | 143 | if ( tilesGraphStage && !pos ) { 144 | 145 | for ( let logicTile of tilesGraphStage ) { 146 | 147 | if ( /ground-s/.test( logicTile.type ) ) { 148 | 149 | pos = new THREE.Vector3 ( 150 | (logicTile.points[0].x + logicTile.points[1].x) / 2, 151 | (logicTile.points[0].y + logicTile.points[1].y) / 2, 152 | (logicTile.points[0].z + logicTile.points[1].z) / 2 153 | ); 154 | 155 | break; 156 | }; 157 | }; 158 | }; 159 | }; 160 | 161 | resetPlayerPos( pos ); 162 | 163 | controler.setSpeedUp( 0 ); 164 | 165 | params.isCrashing = false ; 166 | params.isDying = false ; 167 | params.isGamePaused = false ; 168 | 169 | domBlackScreen.classList.remove( 'show-black-screen' ); 170 | domBlackScreen.classList.add( 'hide-black-screen' ); 171 | 172 | } ); 173 | }; 174 | 175 | // 176 | 177 | document.querySelector( '#json-load input' ).onchange = function( event ) { 178 | 179 | var files = event.target.files; 180 | 181 | // FileReader support 182 | if (FileReader && files && files.length) { 183 | 184 | var matches = files[0].name.match(/^(.*)\.json$/); 185 | 186 | var graphName = matches ? matches[1] : 'unknown'; 187 | 188 | var fr = new FileReader(); 189 | 190 | fr.onload = function () { 191 | 192 | debugLoadGraph( fr.result, graphName ); 193 | 194 | }; 195 | 196 | fr.readAsText(files[0]); 197 | 198 | files.length = 0; 199 | 200 | document.querySelector( '#json-load input' ).blur(); 201 | }; 202 | 203 | }; 204 | 205 | /// STARTING THE GAME 206 | 207 | function startGame( isTouchScreen ) { 208 | 209 | soundMixer.start(); 210 | 211 | domStartMenu.style.display = 'none' ; 212 | domTitleBackground.style.display = 'none' ; 213 | 214 | domStaminaBar.style.display = 'flex' ; 215 | 216 | if ( isTouchScreen ) { 217 | 218 | domJoystickContainer.style.display = 'block' ; 219 | domActionButton.style.display = 'inherit' ; 220 | 221 | input.initJoystick(); 222 | 223 | }; 224 | 225 | params.isGamePaused = false ; 226 | 227 | const gamePass = document.getElementById( 'game-pass' ).value.substr( 0, 15 ); 228 | 229 | if ( gamePass ) { 230 | 231 | socketIO.joinGame( 232 | atlas.player.id, 233 | gamePass, 234 | document.getElementById( 'game-name' ).value.substr( 0, 15 ) || ( 'Anon ' + atlas.player.id.substr(0, 5) ) 235 | ); 236 | 237 | } 238 | 239 | }; 240 | 241 | ///////////////////// 242 | /// IMPORT JSON 243 | ///////////////////// 244 | 245 | var hashTable = { 246 | true: '$t', 247 | false: '$f', 248 | position: '$p', 249 | scale: '$b', 250 | type: '$k', 251 | points: '$v', 252 | isWall: '$w', 253 | isXAligned: '$i', 254 | 'ground-basic': '$g', 255 | 'ground-start': '$s', 256 | 'wall-limit': '$l', 257 | 'wall-easy': '$e', 258 | 'wall-medium' : '$m', 259 | 'wall-hard': '$h', 260 | 'wall-fall': '$a', 261 | 'wall-slip': '$c', 262 | 'cube-inert': '$r', 263 | 'cube-interactive': '$q', 264 | 'cube-trigger': '$o' 265 | }; 266 | 267 | function parseJSON( file ) { 268 | 269 | let data = lzjs.decompress( file ); 270 | 271 | for ( let valueToReplace of Object.keys( hashTable ) ) { 272 | 273 | text = hashTable[ valueToReplace ] 274 | text = text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); 275 | 276 | data = data.replace( new RegExp( text , 'g' ), valueToReplace ); 277 | 278 | }; 279 | 280 | return JSON.parse( data ) ; 281 | }; 282 | 283 | // 284 | 285 | document.getElementById( 'json-save' ).onclick = function() { 286 | 287 | const curentSceneGraph = atlas.getSceneGraph(); 288 | 289 | for( let graphName in sceneGraphs ) { 290 | 291 | if( sceneGraphs[ graphName ] == curentSceneGraph ) { 292 | 293 | let data = JSON.stringify( curentSceneGraph ); 294 | 295 | for ( let valueToReplace of Object.keys( hashTable ) ) { 296 | 297 | data = data.replace( new RegExp( valueToReplace, 'g' ), hashTable[ valueToReplace ] ); 298 | 299 | }; 300 | 301 | let link = document.createElement( 'a' ); 302 | 303 | link.download = graphName + '.json'; 304 | 305 | link.href = URL.createObjectURL( new File( [lzjs.compress( data )], graphName + '.json', { type: 'text/plain;charset=utf-8' } ) ); 306 | 307 | link.dispatchEvent( new MouseEvent( 'click' ) ); 308 | } 309 | } 310 | }; 311 | 312 | /////////////////////// 313 | /// GENERAL FUNCTIONS 314 | /////////////////////// 315 | 316 | // This function is called when the player fell from too high. 317 | // Show a black screen, wait one second, respawn, remove black screen. 318 | function die( hasCrashed ) { 319 | 320 | params.isDying = true ; 321 | if ( hasCrashed ) params.isCrashing = true ; 322 | 323 | domBlackScreen.classList.remove( 'hide-black-screen' ); 324 | domBlackScreen.classList.add( 'show-black-screen' ); 325 | 326 | soundMixer.animStart(); 327 | 328 | setTimeout( function() { 329 | 330 | if ( atlas.getSceneGraph() != sceneGraphs.mountain ) { 331 | 332 | atlas.switchGraph( 'mountain', null, respawn ); 333 | 334 | } else { 335 | 336 | setTimeout( respawn, 1300 ); 337 | 338 | }; 339 | 340 | }, 250 ); 341 | 342 | }; 343 | 344 | // 345 | 346 | function respawn() { 347 | 348 | charaAnim.respawn(); 349 | soundMixer.animEnd(); 350 | 351 | atlas.player.position.copy( respawnPos ); 352 | cameraControl.resetCameraPos(); 353 | 354 | controler.setSpeedUp( 0 ); 355 | 356 | params.isCrashing = false ; 357 | params.isDying = false ; 358 | 359 | domBlackScreen.classList.remove( 'show-black-screen' ); 360 | domBlackScreen.classList.add( 'hide-black-screen' ); 361 | 362 | }; 363 | 364 | // 365 | 366 | function switchMapGraph( gateName ) { 367 | 368 | if ( params.isGamePaused ) return ; 369 | 370 | params.isGamePaused = true ; 371 | 372 | let graphName = getDestinationFromGate( gateName ) ; 373 | 374 | soundMixer.animStart(); 375 | 376 | domBlackScreen.classList.remove( 'hide-black-screen' ); 377 | domBlackScreen.classList.add( 'show-black-screen' ); 378 | 379 | enterGateTime = Date.now(); 380 | 381 | setTimeout( ()=> { 382 | 383 | if ( !sceneGraphs[ graphName ] ) { 384 | 385 | load( `https://edelweiss-game.s3.eu-west-3.amazonaws.com/${ graphName }.json` ); 386 | 387 | function load( url ) { 388 | 389 | fileLoader.load( url, ( file )=> { 390 | 391 | var sceneGraph = parseJSON( file ); 392 | 393 | sceneGraphs[ graphName ] = sceneGraph ; 394 | 395 | atlas.switchGraph( graphName, gateName ); 396 | 397 | soundMixer.animEnd(); 398 | 399 | }); 400 | 401 | }; 402 | 403 | } else { 404 | 405 | atlas.switchGraph( graphName, gateName ); 406 | 407 | soundMixer.animEnd(); 408 | 409 | }; 410 | 411 | }, 220); 412 | 413 | }; 414 | 415 | // 416 | 417 | function endPassGateAnim() { 418 | 419 | if ( Date.now() > enterGateTime + ENTER_GATE_DURATION ) { 420 | 421 | show(); 422 | 423 | } else { 424 | 425 | setTimeout( ()=> { 426 | 427 | show(); 428 | 429 | }, (enterGateTime + ENTER_GATE_DURATION) - Date.now() ); 430 | 431 | }; 432 | 433 | function show() { 434 | 435 | resetPlayerPos( gateTilePos ); 436 | gameState.params.isGamePaused = false ; 437 | 438 | domBlackScreen.classList.remove( 'show-black-screen' ); 439 | domBlackScreen.classList.add( 'hide-black-screen' ); 440 | 441 | }; 442 | 443 | }; 444 | 445 | // 446 | 447 | function resetPlayerPos( vec ) { 448 | 449 | atlas.player.position.copy( typeof vec != 'undefined' ? vec : respawnPos ); 450 | 451 | cameraControl.resetCameraPos(); 452 | 453 | }; 454 | 455 | // 456 | 457 | function getDestinationFromGate( gateName ) { 458 | 459 | switch( gateName ) { 460 | 461 | // village 462 | case 'cave-0' : 463 | 464 | if ( atlas.getSceneGraph() == sceneGraphs.mountain ) { 465 | 466 | return 'cave-A'; 467 | 468 | } else { 469 | 470 | return 'mountain'; 471 | 472 | }; 473 | 474 | // cow field 475 | case 'cave-1' : 476 | 477 | if ( atlas.getSceneGraph() == sceneGraphs.mountain ) { 478 | 479 | return 'cave-B'; 480 | 481 | } else { 482 | 483 | return 'mountain'; 484 | 485 | }; 486 | 487 | // dash 488 | case 'cave-2' : 489 | 490 | if ( atlas.getSceneGraph() == sceneGraphs.mountain ) { 491 | 492 | return 'cave-B'; 493 | 494 | } else { 495 | 496 | return 'mountain'; 497 | 498 | }; 499 | 500 | // forest 501 | case 'cave-3' : 502 | 503 | if ( atlas.getSceneGraph() == sceneGraphs.mountain ) { 504 | 505 | return 'cave-C'; 506 | 507 | } else { 508 | 509 | return 'mountain'; 510 | 511 | }; 512 | 513 | // end forest 514 | case 'cave-4' : 515 | 516 | if ( atlas.getSceneGraph() == sceneGraphs.mountain ) { 517 | 518 | return 'cave-C'; 519 | 520 | } else { 521 | 522 | return 'mountain'; 523 | 524 | }; 525 | 526 | // checkpoint bottom cliff 527 | case 'cave-5' : 528 | 529 | if ( atlas.getSceneGraph() == sceneGraphs.mountain ) { 530 | 531 | return 'cave-D'; 532 | 533 | } else { 534 | 535 | return 'mountain'; 536 | 537 | }; 538 | 539 | // behind pillar in the cliff 540 | case 'cave-6' : 541 | 542 | if ( atlas.getSceneGraph() == sceneGraphs.mountain ) { 543 | 544 | return 'cave-D'; 545 | 546 | } else { 547 | 548 | return 'mountain'; 549 | 550 | }; 551 | 552 | // middle of the cliff bottom 553 | case 'cave-7' : 554 | 555 | if ( atlas.getSceneGraph() == sceneGraphs.mountain ) { 556 | 557 | return 'cave-E'; 558 | 559 | } else { 560 | 561 | return 'mountain'; 562 | 563 | }; 564 | 565 | // middle of the cliff top (lead to other side) 566 | case 'cave-8' : 567 | 568 | if ( atlas.getSceneGraph() == sceneGraphs.mountain ) { 569 | 570 | return 'cave-F'; 571 | 572 | } else { 573 | 574 | return 'mountain'; 575 | 576 | }; 577 | 578 | // other side cliff 579 | case 'cave-9' : 580 | 581 | if ( atlas.getSceneGraph() == sceneGraphs.mountain ) { 582 | 583 | return 'cave-F'; 584 | 585 | } else { 586 | 587 | return 'mountain'; 588 | 589 | }; 590 | 591 | // checkpoint peak 592 | case 'cave-10' : 593 | 594 | if ( atlas.getSceneGraph() == sceneGraphs.mountain ) { 595 | 596 | return 'cave-E'; 597 | 598 | } else { 599 | 600 | return 'mountain'; 601 | 602 | }; 603 | 604 | // dev home 605 | case 'cave-11' : 606 | 607 | if ( atlas.getSceneGraph() == sceneGraphs.mountain ) { 608 | 609 | return 'dev-home'; 610 | 611 | } else { 612 | 613 | return 'mountain'; 614 | 615 | }; 616 | 617 | }; 618 | 619 | }; 620 | 621 | // 622 | 623 | function setSavedPosition( respawnID ) { 624 | 625 | if ( atlas.getSceneGraph() == sceneGraphs.mountain ) { 626 | 627 | checkStage( Math.floor( atlas.player.position.y ) ); 628 | checkStage( Math.floor( atlas.player.position.y ) -1 ); 629 | checkStage( Math.floor( atlas.player.position.y ) +1 ); 630 | 631 | function checkStage( stage ) { 632 | 633 | if ( !sceneGraphs.mountain.tilesGraph[ stage ] ) return ; 634 | 635 | sceneGraphs.mountain.tilesGraph[ stage ].forEach( (logicTile)=> { 636 | 637 | if ( logicTile.tag && logicTile.tag == 'respawn-' + respawnID ) { 638 | 639 | setTileAsRespawn( logicTile ); 640 | 641 | }; 642 | 643 | }); 644 | 645 | }; 646 | 647 | } else { 648 | 649 | sceneGraphs.mountain.tilesGraph.forEach( ( stage )=> { 650 | 651 | if ( !stage ) return ; 652 | 653 | stage.forEach( ( logicTile )=> { 654 | 655 | if ( logicTile.tag && logicTile.tag == 'respawn-' + respawnID ) { 656 | 657 | setTileAsRespawn( logicTile ); 658 | 659 | }; 660 | 661 | }); 662 | 663 | }); 664 | 665 | }; 666 | 667 | function setTileAsRespawn( logicTile ) { 668 | 669 | respawnPos.set( 670 | (logicTile.points[0].x + logicTile.points[1].x) / 2, 671 | (logicTile.points[0].y + logicTile.points[1].y) / 2, 672 | (logicTile.points[0].z + logicTile.points[1].z) / 2 673 | ); 674 | 675 | }; 676 | 677 | }; 678 | 679 | // 680 | 681 | function update( mustUpdate ) { 682 | 683 | if ( !mustUpdate ) return ; 684 | 685 | if ( !loadingFinished ) { 686 | 687 | if ( domStartLoaded.clientWidth / domStartBack.clientWidth < 0.3 ) { 688 | 689 | domStartLoaded.style.width = ( domStartLoaded.clientWidth + 1 ) + 'px' ; 690 | 691 | }; 692 | 693 | }; 694 | 695 | }; 696 | 697 | // 698 | 699 | return { 700 | die, 701 | params, 702 | sceneGraphs, 703 | switchMapGraph, 704 | resetPlayerPos, 705 | respawnPos, 706 | gateTilePos, 707 | endPassGateAnim, 708 | setSavedPosition, 709 | debugLoadGraph, 710 | update 711 | }; 712 | 713 | }; 714 | -------------------------------------------------------------------------------- /public/js/init.js: -------------------------------------------------------------------------------- 1 | 2 | function init() { 3 | 4 | scene = new THREE.Scene(); 5 | 6 | camera = new THREE.PerspectiveCamera( 60, window.innerWidth/window.innerHeight, 0.01, 23.5 ); 7 | 8 | // a directional light is later added on the CameraControl module, 9 | // since this latter will follow the camera movements 10 | ambientLight = new THREE.AmbientLight( 0xffffff, 0.48 ); 11 | scene.add( ambientLight ); 12 | 13 | ////////////// 14 | /// RENDERER 15 | ////////////// 16 | 17 | renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('world') }); 18 | 19 | renderer.autoClear = false; 20 | renderer.setPixelRatio( window.devicePixelRatio ); 21 | renderer.setSize( window.innerWidth, window.innerHeight ); 22 | renderer.shadowMap.enabled = true ; 23 | 24 | // anti-aliasing setup 25 | 26 | var renderPass = new THREE.RenderPass( scene, camera ); 27 | 28 | fxaaPass = new THREE.ShaderPass( THREE.FXAAShader ); 29 | 30 | var pixelRatio = renderer.getPixelRatio(); 31 | 32 | fxaaPass.material.uniforms[ 'resolution' ].value.x = 1 / ( window.innerWidth * pixelRatio ); 33 | fxaaPass.material.uniforms[ 'resolution' ].value.y = 1 / ( window.innerHeight * pixelRatio ); 34 | 35 | composer = new THREE.EffectComposer( renderer ); 36 | composer.addPass( renderPass ); 37 | composer.addPass( fxaaPass ); 38 | 39 | ///////////////////// 40 | /// MISC 41 | ///////////////////// 42 | 43 | clock = new THREE.Clock(); 44 | 45 | var manager = new THREE.LoadingManager(); 46 | 47 | gltfLoader = new THREE.GLTFLoader(manager); 48 | var dracoLoader = new THREE.DRACOLoader(); 49 | 50 | dracoLoader.setDecoderPath( 'libs/draco/' ) 51 | gltfLoader.setDRACOLoader( dracoLoader ); 52 | 53 | textureLoader = new THREE.TextureLoader(); 54 | fileLoader = new THREE.FileLoader() 55 | 56 | // 57 | 58 | assetManager = AssetManager(); 59 | 60 | // event handlers for multiplayer 61 | 62 | var updateCharacters = function( data ) { 63 | 64 | var animation = characterAnimations[ data.id ]; 65 | 66 | // Handle the case when a new player is sending their data. 67 | // A new character will be added to the scene. 68 | if ( !animation ) { 69 | 70 | var character = assetManager.createCharacter( utils.stringHash( data.id ), data.name ); 71 | character.model.name = data.id; // for removal 72 | scene.add( character.model ); 73 | 74 | animation = CharaAnim({ 75 | actions: character.actions, 76 | charaGroup: character.model, 77 | target: new THREE.Vector3(), 78 | position: character.model.position 79 | }); 80 | 81 | characterAnimations[ data.id ] = animation; 82 | }; 83 | 84 | animation.setPlayerState( data ); 85 | }; 86 | 87 | // 88 | 89 | var removeCharacters = function( id ) { 90 | 91 | var group = scene.getObjectByName( id ); 92 | 93 | if( group ) scene.remove( group ) && assetManager.releaseCharacter( group ); 94 | 95 | delete characterAnimations[ id ]; 96 | 97 | }; 98 | 99 | // 100 | 101 | manager.onLoad = function() { 102 | 103 | manager.onLoad = function() {}; 104 | 105 | uaParser = new UAParser(); 106 | socketIO = SocketIO(); 107 | atlas = Atlas(); 108 | input = Input(); 109 | stamina = Stamina(); 110 | interaction = Interaction(); 111 | dynamicItems = DynamicItems(); 112 | mapManager = MapManager(); 113 | optimizer = Optimizer(); 114 | gameState = GameState(); 115 | soundMixer = SoundMixer(); 116 | 117 | socketIO.onPlayerUpdates( updateCharacters ); 118 | socketIO.onPlayerDisconnects( removeCharacters ); 119 | 120 | loop(); 121 | 122 | }; 123 | 124 | }; 125 | -------------------------------------------------------------------------------- /public/js/input.js: -------------------------------------------------------------------------------- 1 | 2 | function Input() { 3 | 4 | const STICK_TRAVEL_RADIUS = 50 ; 5 | 6 | const domWorldCheap = document.getElementById('worldCheap'); 7 | const domWorldHigh = document.getElementById('worldHigh'); 8 | 9 | const domCharContainer = document.getElementById('char-container'); 10 | const domTalkContainer = document.getElementById('talk-container'); 11 | const domTalkSubcontainer = document.getElementById('talk-subcontainer'); 12 | 13 | const domActionButton = document.getElementById('action-button'); 14 | 15 | // Movement 16 | var moveKeys = []; 17 | var tempDirArray ; 18 | 19 | var params = { 20 | isSpacePressed : false, 21 | // is set to true once and for all whenever a touch event occurs 22 | isTouchScreen : false 23 | }; 24 | 25 | var touches = {}; 26 | 27 | var blockAction = false ; 28 | 29 | var joystick, domCross, moveVec, joystickAngle, joystickState ; 30 | 31 | /// JOYSTICK 32 | 33 | function initJoystick() { 34 | 35 | var domBase = document.createElement('IMG'); 36 | domBase.src = 'assets/base.png'; 37 | domBase.id = 'base' ; 38 | 39 | var domStick = document.createElement('IMG'); 40 | domStick.src = 'assets/stick.png'; 41 | domStick.id = 'stick' ; 42 | 43 | domCross = document.createElement('IMG'); 44 | domCross.src = 'assets/cross.png'; 45 | domCross.id = 'cross' ; 46 | domCross.style.top = `${ window.innerHeight - 127.5 }px` ; 47 | 48 | document.getElementById('joystick-container').appendChild( domCross ); 49 | 50 | // get joystick angle 51 | moveVec = new THREE.Vector2(); // vec moved by joystick 52 | 53 | joystick = new VirtualJoystick({ 54 | container : document.getElementById('joystick-container'), 55 | stickElement : domStick, 56 | baseElement : domBase, 57 | stationaryBase : true, 58 | baseX : 90, 59 | baseY : window.innerHeight - 90, 60 | limitStickTravel: true, 61 | stickRadius : STICK_TRAVEL_RADIUS 62 | }); 63 | 64 | api.joystick = joystick ; 65 | 66 | params.isTouchScreen = true ; 67 | 68 | }; 69 | 70 | // 71 | 72 | function update( delta ) { 73 | 74 | if ( input.params.isTouchScreen ) checkJoystickDelta(); 75 | 76 | }; 77 | 78 | //////////////////// 79 | ///// GAME KEYS 80 | //////////////////// 81 | 82 | // TOUCHSCREEN 83 | 84 | function checkJoystickDelta() { 85 | 86 | // show/hide cross blinking animation 87 | if ( joystick._pressed ) { 88 | 89 | domCross.classList.remove( 'blink-cross' ); 90 | 91 | } else { 92 | 93 | domCross.classList.add( 'blink-cross' ); 94 | 95 | }; 96 | 97 | if ( joystick._pressed && 98 | ( Math.abs( joystick.deltaX() ) > 10 || 99 | Math.abs( joystick.deltaY() ) > 10 ) ) { 100 | 101 | if ( moveKeys.length == 0 ) { 102 | moveKeys.push( 'joystick' ); 103 | }; 104 | 105 | // Set the vector we will measure the angle of with the 106 | // virtual joystick's position deltas 107 | moveVec.set( joystick.deltaY(), joystick.deltaX() ); 108 | 109 | joystickAngle = ( Math.round( ( moveVec.angle() / 6 ) * 4 ) / 4 ) * ( Math.PI * 2 ) ; 110 | 111 | if ( joystickState != joystickAngle ) { 112 | 113 | if ( window.navigator.vibrate) { 114 | 115 | window.navigator.vibrate( 20 ); 116 | 117 | }; 118 | 119 | joystickState = joystickAngle ; 120 | 121 | }; 122 | 123 | controler.setMoveAngle( true, utils.toPiRange( joystickAngle ) ); 124 | 125 | } else { 126 | 127 | joystickState = undefined ; 128 | 129 | // Reset moveKeys array 130 | if ( moveKeys.length > 0 && 131 | moveKeys.indexOf('joystick') > -1 ) { 132 | 133 | moveKeys.splice( 0, 1 ); 134 | 135 | }; 136 | 137 | }; 138 | 139 | }; 140 | 141 | // 142 | 143 | domActionButton.addEventListener( 'touchstart', (e)=> { 144 | 145 | if (e.cancelable) { 146 | e.preventDefault(); 147 | }; 148 | 149 | if ( !params.isSpacePressed && 150 | !blockAction ) { 151 | 152 | params.isSpacePressed = true ; 153 | 154 | pressAction(); 155 | 156 | }; 157 | 158 | // cosmetic feedback 159 | domActionButton.style.opacity = '1.0' ; 160 | domActionButton.classList.remove( 'release-button' ); 161 | domActionButton.classList.add( 'push-button' ); 162 | if ( window.navigator.vibrate) { 163 | 164 | window.navigator.vibrate( 20 ); 165 | 166 | }; 167 | 168 | }); 169 | 170 | // 171 | 172 | domActionButton.addEventListener( 'touchend', (e)=> { 173 | 174 | if (e.cancelable) { 175 | e.preventDefault(); 176 | }; 177 | 178 | if ( !blockAction ) { 179 | 180 | releaseAction(); 181 | params.isSpacePressed = false ; 182 | 183 | } else { 184 | 185 | blockAction = false ; 186 | 187 | }; 188 | 189 | domActionButton.style.opacity = '0.5' ; 190 | domActionButton.classList.remove( 'push-button' ); 191 | domActionButton.classList.add( 'release-button' ); 192 | if ( window.navigator.vibrate) { 193 | 194 | window.navigator.vibrate( 20 ); 195 | 196 | }; 197 | 198 | }); 199 | 200 | // 201 | 202 | // request next line if the touch action was not for scrolling 203 | 204 | domCharContainer.addEventListener( 'touchend', (e)=> { 205 | 206 | if ( !interaction.questionTree.isQuestionAsked && 207 | interaction.isInDialogue() ) { 208 | 209 | interaction.requestNextLine(); 210 | 211 | }; 212 | 213 | }); 214 | 215 | domTalkContainer.addEventListener( 'touchend', (e)=> { 216 | 217 | if ( !interaction.questionTree.isQuestionAsked && 218 | interaction.isInDialogue() ) { 219 | 220 | interaction.requestNextLine(); 221 | 222 | }; 223 | 224 | }); 225 | 226 | //KEYBOARD 227 | 228 | window.addEventListener( 'keydown', (e)=> { 229 | 230 | switch( e.code ) { 231 | 232 | case 'Escape' : 233 | // console.log('press escape'); 234 | break; 235 | 236 | case 'KeyA': 237 | case 'ArrowLeft': 238 | addMoveKey( 'left' ); 239 | break; 240 | 241 | case 'KeyW': 242 | case 'ArrowUp' : 243 | addMoveKey( 'up' ); 244 | break; 245 | 246 | case 'KeyD': 247 | case 'ArrowRight' : 248 | addMoveKey( 'right' ); 249 | break; 250 | 251 | case 'KeyS': 252 | case 'ArrowDown' : 253 | addMoveKey( 'down' ); 254 | break; 255 | 256 | case 'Space' : 257 | 258 | if ( !params.isSpacePressed && 259 | !blockAction ) { 260 | 261 | params.isSpacePressed = true ; 262 | 263 | pressAction(); 264 | 265 | }; 266 | 267 | break; 268 | 269 | }; 270 | 271 | }, false); 272 | 273 | // 274 | 275 | window.addEventListener( 'keyup', (e)=> { 276 | 277 | switch( e.code ) { 278 | 279 | case 'KeyA': 280 | case 'ArrowLeft' : 281 | removeMoveKey( 'left' ); 282 | break; 283 | 284 | case 'KeyW': 285 | case 'ArrowUp' : 286 | removeMoveKey( 'up' ); 287 | break; 288 | 289 | case 'KeyD': 290 | case 'ArrowRight' : 291 | removeMoveKey( 'right' ); 292 | break; 293 | 294 | case 'KeyS': 295 | case 'ArrowDown' : 296 | removeMoveKey( 'down' ); 297 | break; 298 | 299 | case 'Space' : 300 | 301 | if ( !blockAction ) { 302 | 303 | releaseAction(); 304 | params.isSpacePressed = false ; 305 | 306 | } else { 307 | 308 | blockAction = false ; 309 | 310 | }; 311 | 312 | break; 313 | 314 | }; 315 | 316 | }); 317 | 318 | // 319 | 320 | function removeMoveKey( keyString ) { 321 | 322 | moveKeys.splice( moveKeys.indexOf( keyString ), 1 ); 323 | 324 | sendMoveDirection(); 325 | 326 | }; 327 | 328 | // 329 | 330 | function addMoveKey( keyString ) { 331 | 332 | if ( gameState.params.isGamePaused ) { 333 | 334 | // console.log( 'navigate in menu' ); 335 | 336 | } else if ( interaction.isInDialogue() ) { 337 | 338 | interaction.chooseAnswer( keyString ); 339 | 340 | } else if ( moveKeys.indexOf( keyString ) < 0 ) { 341 | 342 | moveKeys.unshift( keyString ); 343 | sendMoveDirection(); 344 | 345 | }; 346 | 347 | }; 348 | 349 | // 350 | 351 | function sendMoveDirection() { 352 | 353 | tempDirArray = [ moveKeys[0], moveKeys[1] ]; 354 | 355 | if ( !tempDirArray[0] ) { // no movement 356 | 357 | controler.setMoveAngle( false ); 358 | 359 | } else if ( !tempDirArray[1] ) { // orthogonal movement 360 | 361 | if ( tempDirArray[0] == 'left' ) { 362 | 363 | controler.setMoveAngle( true, -Math.PI / 2 ); 364 | }; 365 | 366 | if ( tempDirArray[0] == 'up' ) { 367 | 368 | controler.setMoveAngle( true, Math.PI ); 369 | }; 370 | 371 | if ( tempDirArray[0] == 'right' ) { 372 | 373 | controler.setMoveAngle( true, Math.PI / 2 ); 374 | }; 375 | 376 | if ( tempDirArray[0] == 'down' ) { 377 | 378 | controler.setMoveAngle( true, 0 ); 379 | }; 380 | 381 | } else { // diagonal movement 382 | 383 | if ( tempDirArray.indexOf( 'left' ) > -1 && 384 | tempDirArray.indexOf( 'up' ) > -1 ) { 385 | 386 | controler.setMoveAngle( true, (-Math.PI / 4) * 3 ); 387 | }; 388 | 389 | if ( tempDirArray.indexOf( 'right' ) > -1 && 390 | tempDirArray.indexOf( 'up' ) > -1 ) { 391 | 392 | controler.setMoveAngle( true, (Math.PI / 4) * 3 ); 393 | }; 394 | 395 | if ( tempDirArray.indexOf( 'right' ) > -1 && 396 | tempDirArray.indexOf( 'down' ) > -1 ) { 397 | 398 | controler.setMoveAngle( true, Math.PI / 4 ); 399 | }; 400 | 401 | if ( tempDirArray.indexOf( 'left' ) > -1 && 402 | tempDirArray.indexOf( 'down' ) > -1 ) { 403 | 404 | controler.setMoveAngle( true, -Math.PI / 4 ); 405 | }; 406 | 407 | // Contradictory inputs : 408 | // the last input is sent to atlas : 409 | 410 | if ( tempDirArray.indexOf( 'up' ) > -1 && 411 | tempDirArray.indexOf( 'down' ) > -1 ) { 412 | 413 | controler.setMoveAngle( true, tempDirArray[0] == 'up' ? Math.PI : 0 ); 414 | }; 415 | 416 | if ( tempDirArray.indexOf( 'left' ) > -1 && 417 | tempDirArray.indexOf( 'right' ) > -1 ) { 418 | 419 | controler.setMoveAngle( true, tempDirArray[0] == 'right' ? Math.PI / 2 : -Math.PI / 2 ); 420 | }; 421 | 422 | }; 423 | 424 | }; 425 | 426 | // 427 | 428 | function pressAction() { 429 | 430 | interaction.hideMessage(); 431 | 432 | if ( gameState.params.isGamePaused ) { 433 | 434 | // console.log( 'validate in menu' ); 435 | 436 | } else if ( interaction.isInDialogue() ) { 437 | 438 | interaction.requestNextLine(); 439 | 440 | } else { 441 | 442 | controler.pressAction(); 443 | 444 | }; 445 | 446 | }; 447 | 448 | // 449 | 450 | function releaseAction() { 451 | 452 | if ( !gameState.params.isGamePaused && 453 | !interaction.isInDialogue() ) { 454 | 455 | controler.releaseAction(); 456 | 457 | }; 458 | 459 | }; 460 | 461 | // 462 | 463 | function blockPressAction() { 464 | 465 | blockAction = true ; 466 | params.isSpacePressed = false ; 467 | 468 | }; 469 | 470 | // 471 | 472 | var api = { 473 | params, 474 | moveKeys, 475 | update, 476 | initJoystick, 477 | blockPressAction 478 | }; 479 | 480 | return api ; 481 | 482 | }; 483 | -------------------------------------------------------------------------------- /public/js/loop.js: -------------------------------------------------------------------------------- 1 | 2 | var loopCount = 0 ; 3 | var ticks, clockDelta; 4 | 5 | function loop() { 6 | 7 | loopCount += 1 ; 8 | 9 | clockDelta = clock.getDelta(); 10 | 11 | requestAnimationFrame( loop ); 12 | 13 | // If performances are low, 14 | // reduce graphic quality to get at least 45FPS 15 | if ( !gameState.params.isGamePaused && optimizer ) { 16 | 17 | optimizer.update( clockDelta ); 18 | 19 | }; 20 | 21 | // 22 | 23 | if ( optimizer && 24 | optimizer.params.level <= 1 ) { 25 | 26 | composer.render(); 27 | 28 | } else if ( optimizer ) { 29 | 30 | renderer.render( scene, camera ); 31 | 32 | }; 33 | 34 | // UPDATE LOGIC 35 | 36 | if ( controler && cameraControl && atlas.getSceneGraph() ) { 37 | 38 | ticks = Math.round( ( clockDelta / ( 1 / 60 ) ) * 2 ); 39 | 40 | for ( let i = 0 ; i < ticks ; i++ ) { 41 | 42 | controler.update( clockDelta / ticks ); 43 | 44 | }; 45 | 46 | cameraControl.update( clockDelta / ( 1 / 60 ) ); 47 | 48 | }; 49 | 50 | // MISC UPDATES 51 | 52 | for ( let key in characterAnimations ) characterAnimations[ key ].update( clockDelta ); 53 | if ( assetManager ) assetManager.update( clockDelta ); 54 | 55 | if ( charaAnim ) charaAnim.update( clockDelta ); 56 | if ( input ) input.update( clockDelta ); 57 | if ( stamina ) stamina.update( loopCount % 10 == 0 ); 58 | if ( mapManager ) mapManager.update( loopCount % 10 == 0 ); 59 | 60 | if ( gameState ) { 61 | 62 | gameState.update( loopCount % 15 == 0 ); 63 | 64 | if ( !gameState.params.isGamePaused ) { 65 | if ( soundMixer ) soundMixer.update( loopCount % 15 == 0, clockDelta ); 66 | if ( atlas ) atlas.debugUpdate( loopCount % 15 == 0 ); 67 | }; 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /public/js/main.js: -------------------------------------------------------------------------------- 1 | 2 | var scene, camera, input, atlas, 3 | controler, clock, charaAnim, 4 | gltfLoader, mixer, cameraControl, stamina, interaction, 5 | dynamicItems, textureLoader, fileLoader, mapManager, 6 | socketIO, optimizer, gameState, ambientLight, 7 | assetManager, soundMixer, renderer, composer, fxaaPass ; 8 | 9 | var actions = []; 10 | 11 | var characterAnimations = {}; 12 | 13 | var utils = Utils(); 14 | var easing = Easing(); 15 | 16 | // 17 | 18 | window.addEventListener('load', ()=> { 19 | init(); 20 | }); 21 | 22 | window.addEventListener( 'resize', ()=> { 23 | 24 | if ( cameraControl ) cameraControl.adaptFOV() ; 25 | 26 | if ( camera ) { 27 | 28 | let world = document.getElementById( 'black-screen' ); 29 | 30 | camera.aspect = world.clientWidth / world.clientHeight; 31 | camera.updateProjectionMatrix(); 32 | 33 | renderer.setSize( world.clientWidth, world.clientHeight ); 34 | 35 | // 36 | 37 | composer.setSize( world.clientWidth, world.clientHeight ); 38 | 39 | var pixelRatio = renderer.getPixelRatio(); 40 | 41 | fxaaPass.material.uniforms[ 'resolution' ].value.x = 1 / ( world.clientWidth * pixelRatio ); 42 | fxaaPass.material.uniforms[ 'resolution' ].value.y = 1 / ( world.clientHeight * pixelRatio ); 43 | 44 | // 45 | 46 | if ( input && input.joystick ) { 47 | 48 | document.getElementById( 'cross' ).style.top = 49 | `${ world.clientHeight - 127.5 }px` ; 50 | 51 | 52 | input.joystick._baseX = 90 ; 53 | input.joystick._baseY = world.clientHeight - 90 ; 54 | 55 | input.joystick._baseEl.style.top = 56 | `${ world.clientHeight - ( 90 + ( input.joystick._baseEl.clientHeight / 2 ) ) }px` ; 57 | 58 | }; 59 | }; 60 | }); 61 | -------------------------------------------------------------------------------- /public/js/soundMixer.js: -------------------------------------------------------------------------------- 1 | 2 | function SoundMixer() { 3 | 4 | var domMusic = document.getElementById('music'); 5 | 6 | var currentMusic; 7 | var lastMusicSet; 8 | var isInAnimation; 9 | var musicVolume = 0 ; 10 | 11 | const FAST_FADE_SPEED = 0.15 ; 12 | const NORMAL_FADE_SPEED = 0.004 ; 13 | var fadeSpeed = NORMAL_FADE_SPEED ; 14 | 15 | const SFXURL = 'https://edelweiss-game.s3.eu-west-3.amazonaws.com/sounds/sfx/' ; 16 | 17 | const MUSIC_URL = "https://edelweiss-game.s3.eu-west-3.amazonaws.com/sounds/music/" ; 18 | 19 | const TRACKS_URLS = { 20 | AK_Pulses: MUSIC_URL + "AK+-+Pulses.ogg", 21 | AndrewOdd_Elysium: MUSIC_URL + "Andrew+Odd+-+Elysium.ogg", 22 | KylePreston_BrknPhoto: MUSIC_URL + "Kyle+Preston+-+Broken+Photosynthesis.ogg", 23 | SimonBainton_Hurtstone: MUSIC_URL + "Simon+Bainton+-+Hurtstone.ogg", 24 | SimonBainton_Porlock: MUSIC_URL + "Simon+Bainton+-+Porlock.ogg", 25 | SimonBainton_Tankah: MUSIC_URL + "Simon+Bainton+-+Tankah.ogg", 26 | StromNoir_Hollow: MUSIC_URL + "Strom+Noir+-+Hollow.ogg", 27 | StromNoir_WinterDay: MUSIC_URL + "Strom+Noir+-+Such+a+Beautiful+Winter+Day.ogg", 28 | StromNoir_Spring: MUSIC_URL + "Strom+Noir+-+The+beginning+of+spring.ogg", 29 | TobiasHellkvist_HearYou: MUSIC_URL + "Tobias+Hellkvist+-+Where+No+One+Can+Hear+You.ogg" 30 | }; 31 | 32 | const TRACKS_ORDER = [ 33 | TRACKS_URLS.StromNoir_WinterDay, 34 | TRACKS_URLS.SimonBainton_Tankah, 35 | TRACKS_URLS.TobiasHellkvist_HearYou, 36 | TRACKS_URLS.AndrewOdd_Elysium, 37 | TRACKS_URLS.KylePreston_BrknPhoto, 38 | TRACKS_URLS.SimonBainton_Hurtstone 39 | ]; 40 | 41 | // hold the times of each music to set it when change back to it. 42 | var MUSIC_TIMES = [ 0, 0, 0, 0, 0, 0 ]; 43 | 44 | const SFX_PARAMS = { 45 | faint_waves: { 46 | volume: 1.2, 47 | distance: 1, 48 | maxDistance: 12 49 | }, 50 | cricket: { 51 | volume: 0.5, 52 | distance: 1, 53 | maxDistance: 15, 54 | playSpeedVarying: 0.1 55 | }, 56 | cricket2: { 57 | volume: 0.55, 58 | distance: 1.1, 59 | maxDistance: 15, 60 | playSpeedVarying: 0.05 61 | }, 62 | fly: { 63 | volume: 1, 64 | distance: 1, 65 | maxDistance: 4, 66 | playSpeedVarying: 0.1 67 | }, 68 | robin: { 69 | volume: 0.7, 70 | distance: 1.3, 71 | maxDistance: 16 72 | }, 73 | nutcracker: { 74 | volume: 0.6, 75 | distance: 1.3, 76 | maxDistance: 16, 77 | playSpeedVarying: 0.1 78 | }, 79 | stream: { 80 | volume: 1, 81 | distance: 0.5, 82 | maxDistance: 13, 83 | playSpeedVarying: 0.1 84 | }, 85 | waterfall: { 86 | volume: 1.4, 87 | distance: 0.4, 88 | maxDistance: 25 89 | }, 90 | small_waterfall: { 91 | volume: 1.3, 92 | distance: 0.5, 93 | maxDistance: 15 94 | }, 95 | gust: { 96 | volume: 1.3, 97 | distance: 1.1, 98 | maxDistance: 20, 99 | playSpeedVarying: 0.1 100 | } 101 | }; 102 | 103 | var sfxs = []; 104 | var sfxCanPlay = true ; 105 | 106 | var listener; 107 | var audioLoader = new THREE.AudioLoader(); 108 | 109 | // 110 | 111 | function setMusic( musicName ) { 112 | 113 | if ( musicName != 'track-' + currentMusic ) { 114 | 115 | if ( typeof currentMusic != 'undefined' ) { 116 | 117 | MUSIC_TIMES[ currentMusic ] = domMusic.currentTime; 118 | 119 | }; 120 | 121 | let musicID = musicName.slice( -1 ); 122 | currentMusic = musicID ; 123 | 124 | domMusic.src = TRACKS_ORDER[ musicID ] ; 125 | domMusic.load(); 126 | domMusic.currentTime = MUSIC_TIMES[ currentMusic ]; 127 | domMusic.play(); 128 | 129 | } else { 130 | 131 | lastMusicSet = Date.now(); 132 | 133 | }; 134 | 135 | }; 136 | 137 | // 138 | 139 | function start() { 140 | 141 | // setMusic( 'track-0' ); 142 | 143 | listener = new THREE.AudioListener(); 144 | camera.add( listener ); 145 | 146 | // 147 | 148 | var cubesGraph = atlas.getSceneGraph().cubesGraph; 149 | 150 | for ( let i of Object.keys( cubesGraph ) ) { 151 | 152 | if ( cubesGraph[i] ) { 153 | 154 | cubesGraph[i].forEach( (logicCube)=> { 155 | 156 | if ( logicCube.type == 'cube-anchor' ) { 157 | 158 | createSFX( logicCube.tag, logicCube.position ); 159 | 160 | }; 161 | 162 | }); 163 | 164 | }; 165 | 166 | }; 167 | 168 | }; 169 | 170 | // 171 | 172 | function createSFX( sfxName, position ) { 173 | 174 | // create the PositionalAudio object (passing in the listener) 175 | var sound = new THREE.PositionalAudio( listener ); 176 | 177 | // load a sound and set it as the PositionalAudio object's buffer 178 | audioLoader.load( SFXURL + sfxName + '.ogg' , function( buffer ) { 179 | 180 | sound.setBuffer( buffer ); 181 | 182 | sound.setRefDistance( SFX_PARAMS[ sfxName ].distance ); 183 | sound.setVolume( SFX_PARAMS[ sfxName ].volume ); 184 | sound.maxDistance = SFX_PARAMS[ sfxName ].maxDistance 185 | sound.setLoop( true ); 186 | sound.sfxName = sfxName ; 187 | 188 | if ( SFX_PARAMS[ sfxName ].playSpeedVarying ) { 189 | sound.setPlaybackRate( 190 | ( 1 - SFX_PARAMS[ sfxName ].playSpeedVarying ) - 191 | ( Math.random() * SFX_PARAMS[ sfxName ].playSpeedVarying ) 192 | ); 193 | }; 194 | 195 | sound.autoplay = true; 196 | 197 | sound.position.copy( position ); 198 | 199 | scene.add( sound ); 200 | 201 | sfxs.push( sound ); 202 | 203 | }); 204 | 205 | }; 206 | 207 | // 208 | 209 | function switchGraph( graphName ) { 210 | 211 | if ( graphName == 'mountain' ) { 212 | 213 | sfxCanPlay = true ; 214 | 215 | } else { 216 | 217 | sfxCanPlay = false ; 218 | 219 | setMusic( 'track-5' ); 220 | 221 | for ( let sound of sfxs ) { 222 | 223 | if ( sound.isPlaying ) sound.stop(); 224 | 225 | }; 226 | 227 | }; 228 | 229 | }; 230 | 231 | // 232 | 233 | function update( mustCheck, delta ) { 234 | 235 | let speedRatio = delta / ( 1 / 60 ) ; 236 | 237 | // fade out 238 | if ( (lastMusicSet + 80 < Date.now() && sfxCanPlay) || 239 | isInAnimation ) { 240 | 241 | musicVolume = Math.max( 0, musicVolume - ( fadeSpeed * speedRatio ) ); 242 | 243 | // fade in 244 | } else { 245 | 246 | musicVolume = Math.min( 1, musicVolume + ( fadeSpeed * speedRatio ) ); 247 | 248 | }; 249 | 250 | domMusic.volume = musicVolume ; 251 | 252 | if ( mustCheck && sfxCanPlay ) { 253 | 254 | for ( let sound of sfxs ) { 255 | 256 | // Check that the sound emitter is in the range to be heard 257 | 258 | if ( sound.position.distanceTo( camera.position ) > sound.maxDistance ) { 259 | 260 | if ( sound.isPlaying ) sound.stop(); 261 | 262 | } else { 263 | 264 | if ( !sound.isPlaying ) { 265 | 266 | sound.play(); 267 | 268 | }; 269 | 270 | }; 271 | 272 | }; 273 | 274 | }; 275 | 276 | }; 277 | 278 | // 279 | 280 | function animStart() { 281 | 282 | isInAnimation = true ; 283 | 284 | fadeSpeed = FAST_FADE_SPEED ; 285 | 286 | musicVolume = Math.max( 0, musicVolume - fadeSpeed ); 287 | 288 | domMusic.volume = musicVolume ; 289 | }; 290 | 291 | // 292 | 293 | function animEnd() { 294 | 295 | isInAnimation = false ; 296 | 297 | musicVolume = 0 ; 298 | 299 | domMusic.volume = musicVolume ; 300 | 301 | fadeSpeed = NORMAL_FADE_SPEED ; 302 | 303 | }; 304 | 305 | // 306 | 307 | return { 308 | start, 309 | setMusic, 310 | update, 311 | switchGraph, 312 | animStart, 313 | animEnd 314 | }; 315 | 316 | }; -------------------------------------------------------------------------------- /public/libs/CopyShader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | * 4 | * Full-screen textured quad shader 5 | */ 6 | 7 | THREE.CopyShader = { 8 | 9 | uniforms: { 10 | 11 | "tDiffuse": { value: null }, 12 | "opacity": { value: 1.0 } 13 | 14 | }, 15 | 16 | vertexShader: [ 17 | 18 | "varying vec2 vUv;", 19 | 20 | "void main() {", 21 | 22 | " vUv = uv;", 23 | " gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", 24 | 25 | "}" 26 | 27 | ].join( "\n" ), 28 | 29 | fragmentShader: [ 30 | 31 | "uniform float opacity;", 32 | 33 | "uniform sampler2D tDiffuse;", 34 | 35 | "varying vec2 vUv;", 36 | 37 | "void main() {", 38 | 39 | " vec4 texel = texture2D( tDiffuse, vUv );", 40 | " gl_FragColor = opacity * texel;", 41 | 42 | "}" 43 | 44 | ].join( "\n" ) 45 | 46 | }; 47 | -------------------------------------------------------------------------------- /public/libs/DRACOLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Don McCurdy / https://www.donmccurdy.com 3 | */ 4 | 5 | THREE.DRACOLoader = function ( manager ) { 6 | 7 | THREE.Loader.call( this, manager ); 8 | 9 | this.decoderPath = ''; 10 | this.decoderConfig = {}; 11 | this.decoderBinary = null; 12 | this.decoderPending = null; 13 | 14 | this.workerLimit = 4; 15 | this.workerPool = []; 16 | this.workerNextTaskID = 1; 17 | this.workerSourceURL = ''; 18 | 19 | this.defaultAttributeIDs = { 20 | position: 'POSITION', 21 | normal: 'NORMAL', 22 | color: 'COLOR', 23 | uv: 'TEX_COORD' 24 | }; 25 | this.defaultAttributeTypes = { 26 | position: 'Float32Array', 27 | normal: 'Float32Array', 28 | color: 'Float32Array', 29 | uv: 'Float32Array' 30 | }; 31 | 32 | }; 33 | 34 | THREE.DRACOLoader.prototype = Object.assign( Object.create( THREE.Loader.prototype ), { 35 | 36 | constructor: THREE.DRACOLoader, 37 | 38 | setDecoderPath: function ( path ) { 39 | 40 | this.decoderPath = path; 41 | 42 | return this; 43 | 44 | }, 45 | 46 | setDecoderConfig: function ( config ) { 47 | 48 | this.decoderConfig = config; 49 | 50 | return this; 51 | 52 | }, 53 | 54 | setWorkerLimit: function ( workerLimit ) { 55 | 56 | this.workerLimit = workerLimit; 57 | 58 | return this; 59 | 60 | }, 61 | 62 | /** @deprecated */ 63 | setVerbosity: function () { 64 | 65 | console.warn( 'THREE.DRACOLoader: The .setVerbosity() method has been removed.' ); 66 | 67 | }, 68 | 69 | /** @deprecated */ 70 | setDrawMode: function () { 71 | 72 | console.warn( 'THREE.DRACOLoader: The .setDrawMode() method has been removed.' ); 73 | 74 | }, 75 | 76 | /** @deprecated */ 77 | setSkipDequantization: function () { 78 | 79 | console.warn( 'THREE.DRACOLoader: The .setSkipDequantization() method has been removed.' ); 80 | 81 | }, 82 | 83 | load: function ( url, onLoad, onProgress, onError ) { 84 | 85 | var loader = new THREE.FileLoader( this.manager ); 86 | 87 | loader.setPath( this.path ); 88 | loader.setResponseType( 'arraybuffer' ); 89 | 90 | if ( this.crossOrigin === 'use-credentials' ) { 91 | 92 | loader.setWithCredentials( true ); 93 | 94 | } 95 | 96 | loader.load( url, ( buffer ) => { 97 | 98 | var taskConfig = { 99 | attributeIDs: this.defaultAttributeIDs, 100 | attributeTypes: this.defaultAttributeTypes, 101 | useUniqueIDs: false 102 | }; 103 | 104 | this.decodeGeometry( buffer, taskConfig ) 105 | .then( onLoad ) 106 | .catch( onError ); 107 | 108 | }, onProgress, onError ); 109 | 110 | }, 111 | 112 | /** @deprecated Kept for backward-compatibility with previous DRACOLoader versions. */ 113 | decodeDracoFile: function ( buffer, callback, attributeIDs, attributeTypes ) { 114 | 115 | var taskConfig = { 116 | attributeIDs: attributeIDs || this.defaultAttributeIDs, 117 | attributeTypes: attributeTypes || this.defaultAttributeTypes, 118 | useUniqueIDs: !! attributeIDs 119 | }; 120 | 121 | this.decodeGeometry( buffer, taskConfig ).then( callback ); 122 | 123 | }, 124 | 125 | decodeGeometry: function ( buffer, taskConfig ) { 126 | 127 | var worker; 128 | var taskID = this.workerNextTaskID ++; 129 | var taskCost = buffer.byteLength; 130 | 131 | // TODO: For backward-compatibility, support 'attributeTypes' objects containing 132 | // references (rather than names) to typed array constructors. These must be 133 | // serialized before sending them to the worker. 134 | for ( var attribute in taskConfig.attributeTypes ) { 135 | 136 | var type = taskConfig.attributeTypes[ attribute ]; 137 | 138 | if ( type.BYTES_PER_ELEMENT !== undefined ) { 139 | 140 | taskConfig.attributeTypes[ attribute ] = type.name; 141 | 142 | } 143 | 144 | } 145 | 146 | // Obtain a worker and assign a task, and construct a geometry instance 147 | // when the task completes. 148 | var geometryPending = this._getWorker( taskID, taskCost ) 149 | .then( ( _worker ) => { 150 | 151 | worker = _worker; 152 | 153 | return new Promise( ( resolve, reject ) => { 154 | 155 | worker._callbacks[ taskID ] = { resolve, reject }; 156 | 157 | worker.postMessage( { type: 'decode', id: taskID, taskConfig, buffer }, [ buffer ] ); 158 | 159 | // this.debug(); 160 | 161 | } ); 162 | 163 | } ) 164 | .then( ( message ) => this._createGeometry( message.geometry ) ); 165 | 166 | // Remove task from the task list. 167 | geometryPending 168 | .finally( () => { 169 | 170 | if ( worker && taskID ) { 171 | 172 | this._releaseTask( worker, taskID ); 173 | 174 | // this.debug(); 175 | 176 | } 177 | 178 | } ); 179 | 180 | return geometryPending; 181 | 182 | }, 183 | 184 | _createGeometry: function ( geometryData ) { 185 | 186 | var geometry = new THREE.BufferGeometry(); 187 | 188 | if ( geometryData.index ) { 189 | 190 | geometry.setIndex( new THREE.BufferAttribute( geometryData.index.array, 1 ) ); 191 | 192 | } 193 | 194 | for ( var i = 0; i < geometryData.attributes.length; i ++ ) { 195 | 196 | var attribute = geometryData.attributes[ i ]; 197 | var name = attribute.name; 198 | var array = attribute.array; 199 | var itemSize = attribute.itemSize; 200 | 201 | geometry.addAttribute( name, new THREE.BufferAttribute( array, itemSize ) ); 202 | 203 | } 204 | 205 | return geometry; 206 | 207 | }, 208 | 209 | _loadLibrary: function ( url, responseType ) { 210 | 211 | var loader = new THREE.FileLoader( this.manager ); 212 | loader.setPath( this.decoderPath ); 213 | loader.setResponseType( responseType ); 214 | 215 | return new Promise( ( resolve, reject ) => { 216 | 217 | loader.load( url, resolve, undefined, reject ); 218 | 219 | } ); 220 | 221 | }, 222 | 223 | _initDecoder: function () { 224 | 225 | if ( this.decoderPending ) return this.decoderPending; 226 | 227 | var useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js'; 228 | var librariesPending = []; 229 | 230 | if ( useJS ) { 231 | 232 | librariesPending.push( this._loadLibrary( 'draco_decoder.js', 'text' ) ); 233 | 234 | } else { 235 | 236 | librariesPending.push( this._loadLibrary( 'draco_wasm_wrapper.js', 'text' ) ); 237 | librariesPending.push( this._loadLibrary( 'draco_decoder.wasm', 'arraybuffer' ) ); 238 | 239 | } 240 | 241 | this.decoderPending = Promise.all( librariesPending ) 242 | .then( ( libraries ) => { 243 | 244 | var jsContent = libraries[ 0 ]; 245 | 246 | if ( ! useJS ) { 247 | 248 | this.decoderConfig.wasmBinary = libraries[ 1 ]; 249 | 250 | } 251 | 252 | var fn = THREE.DRACOLoader.DRACOWorker.toString(); 253 | 254 | var body = [ 255 | '/* draco decoder */', 256 | jsContent, 257 | '', 258 | '/* worker */', 259 | fn.substring( fn.indexOf( '{' ) + 1, fn.lastIndexOf( '}' ) ) 260 | ].join( '\n' ); 261 | 262 | this.workerSourceURL = URL.createObjectURL( new Blob( [ body ] ) ); 263 | 264 | } ); 265 | 266 | return this.decoderPending; 267 | 268 | }, 269 | 270 | _getWorker: function ( taskID, taskCost ) { 271 | 272 | return this._initDecoder().then( () => { 273 | 274 | if ( this.workerPool.length < this.workerLimit ) { 275 | 276 | var worker = new Worker( this.workerSourceURL ); 277 | 278 | worker._callbacks = {}; 279 | worker._taskCosts = {}; 280 | worker._taskLoad = 0; 281 | 282 | worker.postMessage( { type: 'init', decoderConfig: this.decoderConfig } ); 283 | 284 | worker.onmessage = function ( e ) { 285 | 286 | var message = e.data; 287 | 288 | switch ( message.type ) { 289 | 290 | case 'decode': 291 | worker._callbacks[ message.id ].resolve( message ); 292 | break; 293 | 294 | case 'error': 295 | worker._callbacks[ message.id ].reject( message ); 296 | break; 297 | 298 | default: 299 | console.error( 'THREE.DRACOLoader: Unexpected message, "' + message.type + '"' ); 300 | 301 | } 302 | 303 | }; 304 | 305 | this.workerPool.push( worker ); 306 | 307 | } else { 308 | 309 | this.workerPool.sort( function ( a, b ) { 310 | 311 | return a._taskLoad > b._taskLoad ? - 1 : 1; 312 | 313 | } ); 314 | 315 | } 316 | 317 | var worker = this.workerPool[ this.workerPool.length - 1 ]; 318 | worker._taskCosts[ taskID ] = taskCost; 319 | worker._taskLoad += taskCost; 320 | return worker; 321 | 322 | } ); 323 | 324 | }, 325 | 326 | _releaseTask: function ( worker, taskID ) { 327 | 328 | worker._taskLoad -= worker._taskCosts[ taskID ]; 329 | delete worker._callbacks[ taskID ]; 330 | delete worker._taskCosts[ taskID ]; 331 | 332 | }, 333 | 334 | debug: function () { 335 | 336 | console.log( 'Task load: ', this.workerPool.map( ( worker ) => worker._taskLoad ) ); 337 | 338 | }, 339 | 340 | dispose: function () { 341 | 342 | for ( var i = 0; i < this.workerPool.length; ++ i ) { 343 | 344 | this.workerPool[ i ].terminate(); 345 | 346 | } 347 | 348 | this.workerPool.length = 0; 349 | 350 | return this; 351 | 352 | } 353 | 354 | } ); 355 | 356 | /* WEB WORKER */ 357 | 358 | THREE.DRACOLoader.DRACOWorker = function () { 359 | 360 | var decoderConfig; 361 | var decoderPending; 362 | 363 | onmessage = function ( e ) { 364 | 365 | var message = e.data; 366 | 367 | switch ( message.type ) { 368 | 369 | case 'init': 370 | decoderConfig = message.decoderConfig; 371 | decoderPending = new Promise( function ( resolve/*, reject*/ ) { 372 | 373 | decoderConfig.onModuleLoaded = function ( draco ) { 374 | 375 | // Module is Promise-like. Wrap before resolving to avoid loop. 376 | resolve( { draco: draco } ); 377 | 378 | }; 379 | 380 | DracoDecoderModule( decoderConfig ); 381 | 382 | } ); 383 | break; 384 | 385 | case 'decode': 386 | var buffer = message.buffer; 387 | var taskConfig = message.taskConfig; 388 | decoderPending.then( ( module ) => { 389 | 390 | var draco = module.draco; 391 | var decoder = new draco.Decoder(); 392 | var decoderBuffer = new draco.DecoderBuffer(); 393 | decoderBuffer.Init( new Int8Array( buffer ), buffer.byteLength ); 394 | 395 | try { 396 | 397 | var geometry = decodeGeometry( draco, decoder, decoderBuffer, taskConfig ); 398 | 399 | var buffers = geometry.attributes.map( ( attr ) => attr.array.buffer ); 400 | 401 | if ( geometry.index ) buffers.push( geometry.index.array.buffer ); 402 | 403 | self.postMessage( { type: 'decode', id: message.id, geometry }, buffers ); 404 | 405 | } catch ( error ) { 406 | 407 | console.error( error ); 408 | 409 | self.postMessage( { type: 'error', id: message.id, error: error.message } ); 410 | 411 | } finally { 412 | 413 | draco.destroy( decoderBuffer ); 414 | draco.destroy( decoder ); 415 | 416 | } 417 | 418 | } ); 419 | break; 420 | 421 | } 422 | 423 | }; 424 | 425 | function decodeGeometry( draco, decoder, decoderBuffer, taskConfig ) { 426 | 427 | var attributeIDs = taskConfig.attributeIDs; 428 | var attributeTypes = taskConfig.attributeTypes; 429 | 430 | var dracoGeometry; 431 | var decodingStatus; 432 | 433 | var geometryType = decoder.GetEncodedGeometryType( decoderBuffer ); 434 | 435 | if ( geometryType === draco.TRIANGULAR_MESH ) { 436 | 437 | dracoGeometry = new draco.Mesh(); 438 | decodingStatus = decoder.DecodeBufferToMesh( decoderBuffer, dracoGeometry ); 439 | 440 | } else if ( geometryType === draco.POINT_CLOUD ) { 441 | 442 | dracoGeometry = new draco.PointCloud(); 443 | decodingStatus = decoder.DecodeBufferToPointCloud( decoderBuffer, dracoGeometry ); 444 | 445 | } else { 446 | 447 | throw new Error( 'THREE.DRACOLoader: Unexpected geometry type.' ); 448 | 449 | } 450 | 451 | if ( ! decodingStatus.ok() || dracoGeometry.ptr === 0 ) { 452 | 453 | throw new Error( 'THREE.DRACOLoader: Decoding failed: ' + decodingStatus.error_msg() ); 454 | 455 | } 456 | 457 | var geometry = { index: null, attributes: [] }; 458 | 459 | // Gather all vertex attributes. 460 | for ( var attributeName in attributeIDs ) { 461 | 462 | var attributeType = self[ attributeTypes[ attributeName ] ]; 463 | 464 | var attribute; 465 | var attributeID; 466 | 467 | // A Draco file may be created with default vertex attributes, whose attribute IDs 468 | // are mapped 1:1 from their semantic name (POSITION, NORMAL, ...). Alternatively, 469 | // a Draco file may contain a custom set of attributes, identified by known unique 470 | // IDs. glTF files always do the latter, and `.drc` files typically do the former. 471 | if ( taskConfig.useUniqueIDs ) { 472 | 473 | attributeID = attributeIDs[ attributeName ]; 474 | attribute = decoder.GetAttributeByUniqueId( dracoGeometry, attributeID ); 475 | 476 | } else { 477 | 478 | attributeID = decoder.GetAttributeId( dracoGeometry, draco[ attributeIDs[ attributeName ] ] ); 479 | 480 | if ( attributeID === - 1 ) continue; 481 | 482 | attribute = decoder.GetAttribute( dracoGeometry, attributeID ); 483 | 484 | } 485 | 486 | geometry.attributes.push( decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) ); 487 | 488 | } 489 | 490 | // Add index. 491 | if ( geometryType === draco.TRIANGULAR_MESH ) { 492 | 493 | // Generate mesh faces. 494 | var numFaces = dracoGeometry.num_faces(); 495 | var numIndices = numFaces * 3; 496 | var index = new Uint32Array( numIndices ); 497 | var indexArray = new draco.DracoInt32Array(); 498 | 499 | for ( var i = 0; i < numFaces; ++ i ) { 500 | 501 | decoder.GetFaceFromMesh( dracoGeometry, i, indexArray ); 502 | 503 | for ( var j = 0; j < 3; ++ j ) { 504 | 505 | index[ i * 3 + j ] = indexArray.GetValue( j ); 506 | 507 | } 508 | 509 | } 510 | 511 | geometry.index = { array: index, itemSize: 1 }; 512 | 513 | draco.destroy( indexArray ); 514 | 515 | } 516 | 517 | draco.destroy( dracoGeometry ); 518 | 519 | return geometry; 520 | 521 | } 522 | 523 | function decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) { 524 | 525 | var numComponents = attribute.num_components(); 526 | var numPoints = dracoGeometry.num_points(); 527 | var numValues = numPoints * numComponents; 528 | var dracoArray; 529 | 530 | var array; 531 | 532 | switch ( attributeType ) { 533 | 534 | case Float32Array: 535 | dracoArray = new draco.DracoFloat32Array(); 536 | decoder.GetAttributeFloatForAllPoints( dracoGeometry, attribute, dracoArray ); 537 | array = new Float32Array( numValues ); 538 | break; 539 | 540 | case Int8Array: 541 | dracoArray = new draco.DracoInt8Array(); 542 | decoder.GetAttributeInt8ForAllPoints( dracoGeometry, attribute, dracoArray ); 543 | array = new Int8Array( numValues ); 544 | break; 545 | 546 | case Int16Array: 547 | dracoArray = new draco.DracoInt16Array(); 548 | decoder.GetAttributeInt16ForAllPoints( dracoGeometry, attribute, dracoArray ); 549 | array = new Int16Array( numValues ); 550 | break; 551 | 552 | case Int32Array: 553 | dracoArray = new draco.DracoInt32Array(); 554 | decoder.GetAttributeInt32ForAllPoints( dracoGeometry, attribute, dracoArray ); 555 | array = new Int32Array( numValues ); 556 | break; 557 | 558 | case Uint8Array: 559 | dracoArray = new draco.DracoUInt8Array(); 560 | decoder.GetAttributeUInt8ForAllPoints( dracoGeometry, attribute, dracoArray ); 561 | array = new Uint8Array( numValues ); 562 | break; 563 | 564 | case Uint16Array: 565 | dracoArray = new draco.DracoUInt16Array(); 566 | decoder.GetAttributeUInt16ForAllPoints( dracoGeometry, attribute, dracoArray ); 567 | array = new Uint16Array( numValues ); 568 | break; 569 | 570 | case Uint32Array: 571 | dracoArray = new draco.DracoUInt32Array(); 572 | decoder.GetAttributeUInt32ForAllPoints( dracoGeometry, attribute, dracoArray ); 573 | array = new Uint32Array( numValues ); 574 | break; 575 | 576 | default: 577 | throw new Error( 'THREE.DRACOLoader: Unexpected attribute type.' ); 578 | 579 | } 580 | 581 | for ( var i = 0; i < numValues; i ++ ) { 582 | 583 | array[ i ] = dracoArray.GetValue( i ); 584 | 585 | } 586 | 587 | draco.destroy( dracoArray ); 588 | 589 | return { 590 | name: attributeName, 591 | array: array, 592 | itemSize: numComponents 593 | }; 594 | 595 | } 596 | 597 | }; 598 | 599 | /** Deprecated static methods */ 600 | 601 | /** @deprecated */ 602 | THREE.DRACOLoader.setDecoderPath = function () { 603 | 604 | console.warn( 'THREE.DRACOLoader: The .setDecoderPath() method has been removed. Use instance methods.' ); 605 | 606 | }; 607 | 608 | /** @deprecated */ 609 | THREE.DRACOLoader.setDecoderConfig = function () { 610 | 611 | console.warn( 'THREE.DRACOLoader: The .setDecoderConfig() method has been removed. Use instance methods.' ); 612 | 613 | }; 614 | 615 | /** @deprecated */ 616 | THREE.DRACOLoader.releaseDecoderModule = function () { 617 | 618 | console.warn( 'THREE.DRACOLoader: The .releaseDecoderModule() method has been removed. Use instance methods.' ); 619 | 620 | }; 621 | 622 | /** @deprecated */ 623 | THREE.DRACOLoader.getDecoderModule = function () { 624 | 625 | console.warn( 'THREE.DRACOLoader: The .getDecoderModule() method has been removed. Use instance methods.' ); 626 | 627 | }; 628 | -------------------------------------------------------------------------------- /public/libs/EffectComposer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | */ 4 | 5 | THREE.EffectComposer = function ( renderer, renderTarget ) { 6 | 7 | this.renderer = renderer; 8 | 9 | if ( renderTarget === undefined ) { 10 | 11 | var parameters = { 12 | minFilter: THREE.LinearFilter, 13 | magFilter: THREE.LinearFilter, 14 | format: THREE.RGBAFormat, 15 | stencilBuffer: false 16 | }; 17 | 18 | var size = renderer.getSize( new THREE.Vector2() ); 19 | this._pixelRatio = renderer.getPixelRatio(); 20 | this._width = size.width; 21 | this._height = size.height; 22 | 23 | renderTarget = new THREE.WebGLRenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, parameters ); 24 | renderTarget.texture.name = 'EffectComposer.rt1'; 25 | 26 | } else { 27 | 28 | this._pixelRatio = 1; 29 | this._width = renderTarget.width; 30 | this._height = renderTarget.height; 31 | 32 | } 33 | 34 | this.renderTarget1 = renderTarget; 35 | this.renderTarget2 = renderTarget.clone(); 36 | this.renderTarget2.texture.name = 'EffectComposer.rt2'; 37 | 38 | this.writeBuffer = this.renderTarget1; 39 | this.readBuffer = this.renderTarget2; 40 | 41 | this.renderToScreen = true; 42 | 43 | this.passes = []; 44 | 45 | // dependencies 46 | 47 | if ( THREE.CopyShader === undefined ) { 48 | 49 | console.error( 'THREE.EffectComposer relies on THREE.CopyShader' ); 50 | 51 | } 52 | 53 | if ( THREE.ShaderPass === undefined ) { 54 | 55 | console.error( 'THREE.EffectComposer relies on THREE.ShaderPass' ); 56 | 57 | } 58 | 59 | this.copyPass = new THREE.ShaderPass( THREE.CopyShader ); 60 | 61 | this.clock = new THREE.Clock(); 62 | 63 | }; 64 | 65 | Object.assign( THREE.EffectComposer.prototype, { 66 | 67 | swapBuffers: function () { 68 | 69 | var tmp = this.readBuffer; 70 | this.readBuffer = this.writeBuffer; 71 | this.writeBuffer = tmp; 72 | 73 | }, 74 | 75 | addPass: function ( pass ) { 76 | 77 | this.passes.push( pass ); 78 | pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio ); 79 | 80 | }, 81 | 82 | insertPass: function ( pass, index ) { 83 | 84 | this.passes.splice( index, 0, pass ); 85 | 86 | }, 87 | 88 | isLastEnabledPass: function ( passIndex ) { 89 | 90 | for ( var i = passIndex + 1; i < this.passes.length; i ++ ) { 91 | 92 | if ( this.passes[ i ].enabled ) { 93 | 94 | return false; 95 | 96 | } 97 | 98 | } 99 | 100 | return true; 101 | 102 | }, 103 | 104 | render: function ( deltaTime ) { 105 | 106 | // deltaTime value is in seconds 107 | 108 | if ( deltaTime === undefined ) { 109 | 110 | deltaTime = this.clock.getDelta(); 111 | 112 | } 113 | 114 | var currentRenderTarget = this.renderer.getRenderTarget(); 115 | 116 | var maskActive = false; 117 | 118 | var pass, i, il = this.passes.length; 119 | 120 | for ( i = 0; i < il; i ++ ) { 121 | 122 | pass = this.passes[ i ]; 123 | 124 | if ( pass.enabled === false ) continue; 125 | 126 | pass.renderToScreen = ( this.renderToScreen && this.isLastEnabledPass( i ) ); 127 | pass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive ); 128 | 129 | if ( pass.needsSwap ) { 130 | 131 | if ( maskActive ) { 132 | 133 | var context = this.renderer.getContext(); 134 | var stencil = this.renderer.state.buffers.stencil; 135 | 136 | //context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff ); 137 | stencil.setFunc( context.NOTEQUAL, 1, 0xffffffff ); 138 | 139 | this.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime ); 140 | 141 | //context.stencilFunc( context.EQUAL, 1, 0xffffffff ); 142 | stencil.setFunc( context.EQUAL, 1, 0xffffffff ); 143 | 144 | } 145 | 146 | this.swapBuffers(); 147 | 148 | } 149 | 150 | if ( THREE.MaskPass !== undefined ) { 151 | 152 | if ( pass instanceof THREE.MaskPass ) { 153 | 154 | maskActive = true; 155 | 156 | } else if ( pass instanceof THREE.ClearMaskPass ) { 157 | 158 | maskActive = false; 159 | 160 | } 161 | 162 | } 163 | 164 | } 165 | 166 | this.renderer.setRenderTarget( currentRenderTarget ); 167 | 168 | }, 169 | 170 | reset: function ( renderTarget ) { 171 | 172 | if ( renderTarget === undefined ) { 173 | 174 | var size = this.renderer.getSize( new THREE.Vector2() ); 175 | this._pixelRatio = this.renderer.getPixelRatio(); 176 | this._width = size.width; 177 | this._height = size.height; 178 | 179 | renderTarget = this.renderTarget1.clone(); 180 | renderTarget.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio ); 181 | 182 | } 183 | 184 | this.renderTarget1.dispose(); 185 | this.renderTarget2.dispose(); 186 | this.renderTarget1 = renderTarget; 187 | this.renderTarget2 = renderTarget.clone(); 188 | 189 | this.writeBuffer = this.renderTarget1; 190 | this.readBuffer = this.renderTarget2; 191 | 192 | }, 193 | 194 | setSize: function ( width, height ) { 195 | 196 | this._width = width; 197 | this._height = height; 198 | 199 | var effectiveWidth = this._width * this._pixelRatio; 200 | var effectiveHeight = this._height * this._pixelRatio; 201 | 202 | this.renderTarget1.setSize( effectiveWidth, effectiveHeight ); 203 | this.renderTarget2.setSize( effectiveWidth, effectiveHeight ); 204 | 205 | for ( var i = 0; i < this.passes.length; i ++ ) { 206 | 207 | this.passes[ i ].setSize( effectiveWidth, effectiveHeight ); 208 | 209 | } 210 | 211 | }, 212 | 213 | setPixelRatio: function ( pixelRatio ) { 214 | 215 | this._pixelRatio = pixelRatio; 216 | 217 | this.setSize( this._width, this._height ); 218 | 219 | } 220 | 221 | } ); 222 | 223 | 224 | THREE.Pass = function () { 225 | 226 | // if set to true, the pass is processed by the composer 227 | this.enabled = true; 228 | 229 | // if set to true, the pass indicates to swap read and write buffer after rendering 230 | this.needsSwap = true; 231 | 232 | // if set to true, the pass clears its buffer before rendering 233 | this.clear = false; 234 | 235 | // if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer. 236 | this.renderToScreen = false; 237 | 238 | }; 239 | 240 | Object.assign( THREE.Pass.prototype, { 241 | 242 | setSize: function ( /* width, height */ ) {}, 243 | 244 | render: function ( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) { 245 | 246 | console.error( 'THREE.Pass: .render() must be implemented in derived pass.' ); 247 | 248 | } 249 | 250 | } ); 251 | 252 | // Helper for passes that need to fill the viewport with a single quad. 253 | THREE.Pass.FullScreenQuad = ( function () { 254 | 255 | var camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 ); 256 | var geometry = new THREE.PlaneBufferGeometry( 2, 2 ); 257 | 258 | var FullScreenQuad = function ( material ) { 259 | 260 | this._mesh = new THREE.Mesh( geometry, material ); 261 | 262 | }; 263 | 264 | Object.defineProperty( FullScreenQuad.prototype, 'material', { 265 | 266 | get: function () { 267 | 268 | return this._mesh.material; 269 | 270 | }, 271 | 272 | set: function ( value ) { 273 | 274 | this._mesh.material = value; 275 | 276 | } 277 | 278 | } ); 279 | 280 | Object.assign( FullScreenQuad.prototype, { 281 | 282 | render: function ( renderer ) { 283 | 284 | renderer.render( this._mesh, camera ); 285 | 286 | } 287 | 288 | } ); 289 | 290 | return FullScreenQuad; 291 | 292 | } )(); 293 | -------------------------------------------------------------------------------- /public/libs/RenderPass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | */ 4 | 5 | THREE.RenderPass = function ( scene, camera, overrideMaterial, clearColor, clearAlpha ) { 6 | 7 | THREE.Pass.call( this ); 8 | 9 | this.scene = scene; 10 | this.camera = camera; 11 | 12 | this.overrideMaterial = overrideMaterial; 13 | 14 | this.clearColor = clearColor; 15 | this.clearAlpha = ( clearAlpha !== undefined ) ? clearAlpha : 0; 16 | 17 | this.clear = true; 18 | this.clearDepth = false; 19 | this.needsSwap = false; 20 | 21 | }; 22 | 23 | THREE.RenderPass.prototype = Object.assign( Object.create( THREE.Pass.prototype ), { 24 | 25 | constructor: THREE.RenderPass, 26 | 27 | render: function ( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) { 28 | 29 | var oldAutoClear = renderer.autoClear; 30 | renderer.autoClear = false; 31 | 32 | this.scene.overrideMaterial = this.overrideMaterial; 33 | 34 | var oldClearColor, oldClearAlpha; 35 | 36 | if ( this.clearColor ) { 37 | 38 | oldClearColor = renderer.getClearColor().getHex(); 39 | oldClearAlpha = renderer.getClearAlpha(); 40 | 41 | renderer.setClearColor( this.clearColor, this.clearAlpha ); 42 | 43 | } 44 | 45 | if ( this.clearDepth ) { 46 | 47 | renderer.clearDepth(); 48 | 49 | } 50 | 51 | renderer.setRenderTarget( this.renderToScreen ? null : readBuffer ); 52 | 53 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600 54 | if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil ); 55 | renderer.render( this.scene, this.camera ); 56 | 57 | if ( this.clearColor ) { 58 | 59 | renderer.setClearColor( oldClearColor, oldClearAlpha ); 60 | 61 | } 62 | 63 | this.scene.overrideMaterial = null; 64 | renderer.autoClear = oldAutoClear; 65 | 66 | } 67 | 68 | } ); 69 | -------------------------------------------------------------------------------- /public/libs/ShaderPass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | */ 4 | 5 | THREE.ShaderPass = function ( shader, textureID ) { 6 | 7 | THREE.Pass.call( this ); 8 | 9 | this.textureID = ( textureID !== undefined ) ? textureID : "tDiffuse"; 10 | 11 | if ( shader instanceof THREE.ShaderMaterial ) { 12 | 13 | this.uniforms = shader.uniforms; 14 | 15 | this.material = shader; 16 | 17 | } else if ( shader ) { 18 | 19 | this.uniforms = THREE.UniformsUtils.clone( shader.uniforms ); 20 | 21 | this.material = new THREE.ShaderMaterial( { 22 | 23 | defines: Object.assign( {}, shader.defines ), 24 | uniforms: this.uniforms, 25 | vertexShader: shader.vertexShader, 26 | fragmentShader: shader.fragmentShader 27 | 28 | } ); 29 | 30 | } 31 | 32 | this.fsQuad = new THREE.Pass.FullScreenQuad( this.material ); 33 | 34 | }; 35 | 36 | THREE.ShaderPass.prototype = Object.assign( Object.create( THREE.Pass.prototype ), { 37 | 38 | constructor: THREE.ShaderPass, 39 | 40 | render: function ( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) { 41 | 42 | if ( this.uniforms[ this.textureID ] ) { 43 | 44 | this.uniforms[ this.textureID ].value = readBuffer.texture; 45 | 46 | } 47 | 48 | this.fsQuad.material = this.material; 49 | 50 | if ( this.renderToScreen ) { 51 | 52 | renderer.setRenderTarget( null ); 53 | this.fsQuad.render( renderer ); 54 | 55 | } else { 56 | 57 | renderer.setRenderTarget( writeBuffer ); 58 | // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600 59 | if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil ); 60 | this.fsQuad.render( renderer ); 61 | 62 | } 63 | 64 | } 65 | 66 | } ); 67 | -------------------------------------------------------------------------------- /public/libs/SkeletonUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author sunag / http://www.sunag.com.br 3 | */ 4 | 5 | THREE.SkeletonUtils = { 6 | 7 | retarget: function () { 8 | 9 | var pos = new THREE.Vector3(), 10 | quat = new THREE.Quaternion(), 11 | scale = new THREE.Vector3(), 12 | bindBoneMatrix = new THREE.Matrix4(), 13 | relativeMatrix = new THREE.Matrix4(), 14 | globalMatrix = new THREE.Matrix4(); 15 | 16 | return function ( target, source, options ) { 17 | 18 | options = options || {}; 19 | options.preserveMatrix = options.preserveMatrix !== undefined ? options.preserveMatrix : true; 20 | options.preservePosition = options.preservePosition !== undefined ? options.preservePosition : true; 21 | options.preserveHipPosition = options.preserveHipPosition !== undefined ? options.preserveHipPosition : false; 22 | options.useTargetMatrix = options.useTargetMatrix !== undefined ? options.useTargetMatrix : false; 23 | options.hip = options.hip !== undefined ? options.hip : "hip"; 24 | options.names = options.names || {}; 25 | 26 | var sourceBones = source.isObject3D ? source.skeleton.bones : this.getBones( source ), 27 | bones = target.isObject3D ? target.skeleton.bones : this.getBones( target ), 28 | bindBones, 29 | bone, name, boneTo, 30 | bonesPosition, i; 31 | 32 | // reset bones 33 | 34 | if ( target.isObject3D ) { 35 | 36 | target.skeleton.pose(); 37 | 38 | } else { 39 | 40 | options.useTargetMatrix = true; 41 | options.preserveMatrix = false; 42 | 43 | } 44 | 45 | if ( options.preservePosition ) { 46 | 47 | bonesPosition = []; 48 | 49 | for ( i = 0; i < bones.length; i ++ ) { 50 | 51 | bonesPosition.push( bones[ i ].position.clone() ); 52 | 53 | } 54 | 55 | } 56 | 57 | if ( options.preserveMatrix ) { 58 | 59 | // reset matrix 60 | 61 | target.updateMatrixWorld(); 62 | 63 | target.matrixWorld.identity(); 64 | 65 | // reset children matrix 66 | 67 | for ( i = 0; i < target.children.length; ++ i ) { 68 | 69 | target.children[ i ].updateMatrixWorld( true ); 70 | 71 | } 72 | 73 | } 74 | 75 | if ( options.offsets ) { 76 | 77 | bindBones = []; 78 | 79 | for ( i = 0; i < bones.length; ++ i ) { 80 | 81 | bone = bones[ i ]; 82 | name = options.names[ bone.name ] || bone.name; 83 | 84 | if ( options.offsets && options.offsets[ name ] ) { 85 | 86 | bone.matrix.multiply( options.offsets[ name ] ); 87 | 88 | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale ); 89 | 90 | bone.updateMatrixWorld(); 91 | 92 | } 93 | 94 | bindBones.push( bone.matrixWorld.clone() ); 95 | 96 | } 97 | 98 | } 99 | 100 | for ( i = 0; i < bones.length; ++ i ) { 101 | 102 | bone = bones[ i ]; 103 | name = options.names[ bone.name ] || bone.name; 104 | 105 | boneTo = this.getBoneByName( name, sourceBones ); 106 | 107 | globalMatrix.copy( bone.matrixWorld ); 108 | 109 | if ( boneTo ) { 110 | 111 | boneTo.updateMatrixWorld(); 112 | 113 | if ( options.useTargetMatrix ) { 114 | 115 | relativeMatrix.copy( boneTo.matrixWorld ); 116 | 117 | } else { 118 | 119 | relativeMatrix.getInverse( target.matrixWorld ); 120 | relativeMatrix.multiply( boneTo.matrixWorld ); 121 | 122 | } 123 | 124 | // ignore scale to extract rotation 125 | 126 | scale.setFromMatrixScale( relativeMatrix ); 127 | relativeMatrix.scale( scale.set( 1 / scale.x, 1 / scale.y, 1 / scale.z ) ); 128 | 129 | // apply to global matrix 130 | 131 | globalMatrix.makeRotationFromQuaternion( quat.setFromRotationMatrix( relativeMatrix ) ); 132 | 133 | if ( target.isObject3D ) { 134 | 135 | var boneIndex = bones.indexOf( bone ), 136 | wBindMatrix = bindBones ? bindBones[ boneIndex ] : bindBoneMatrix.getInverse( target.skeleton.boneInverses[ boneIndex ] ); 137 | 138 | globalMatrix.multiply( wBindMatrix ); 139 | 140 | } 141 | 142 | globalMatrix.copyPosition( relativeMatrix ); 143 | 144 | } 145 | 146 | if ( bone.parent && bone.parent.isBone ) { 147 | 148 | bone.matrix.getInverse( bone.parent.matrixWorld ); 149 | bone.matrix.multiply( globalMatrix ); 150 | 151 | } else { 152 | 153 | bone.matrix.copy( globalMatrix ); 154 | 155 | } 156 | 157 | if ( options.preserveHipPosition && name === options.hip ) { 158 | 159 | bone.matrix.setPosition( pos.set( 0, bone.position.y, 0 ) ); 160 | 161 | } 162 | 163 | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale ); 164 | 165 | bone.updateMatrixWorld(); 166 | 167 | } 168 | 169 | if ( options.preservePosition ) { 170 | 171 | for ( i = 0; i < bones.length; ++ i ) { 172 | 173 | bone = bones[ i ]; 174 | name = options.names[ bone.name ] || bone.name; 175 | 176 | if ( name !== options.hip ) { 177 | 178 | bone.position.copy( bonesPosition[ i ] ); 179 | 180 | } 181 | 182 | } 183 | 184 | } 185 | 186 | if ( options.preserveMatrix ) { 187 | 188 | // restore matrix 189 | 190 | target.updateMatrixWorld( true ); 191 | 192 | } 193 | 194 | }; 195 | 196 | }(), 197 | 198 | retargetClip: function ( target, source, clip, options ) { 199 | 200 | options = options || {}; 201 | options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false; 202 | options.fps = options.fps !== undefined ? options.fps : 30; 203 | options.names = options.names || []; 204 | 205 | if ( ! source.isObject3D ) { 206 | 207 | source = this.getHelperFromSkeleton( source ); 208 | 209 | } 210 | 211 | var numFrames = Math.round( clip.duration * ( options.fps / 1000 ) * 1000 ), 212 | delta = 1 / options.fps, 213 | convertedTracks = [], 214 | mixer = new THREE.AnimationMixer( source ), 215 | bones = this.getBones( target.skeleton ), 216 | boneDatas = [], 217 | positionOffset, 218 | bone, boneTo, boneData, 219 | name, i, j; 220 | 221 | mixer.clipAction( clip ).play(); 222 | mixer.update( 0 ); 223 | 224 | source.updateMatrixWorld(); 225 | 226 | for ( i = 0; i < numFrames; ++ i ) { 227 | 228 | var time = i * delta; 229 | 230 | this.retarget( target, source, options ); 231 | 232 | for ( j = 0; j < bones.length; ++ j ) { 233 | 234 | name = options.names[ bones[ j ].name ] || bones[ j ].name; 235 | 236 | boneTo = this.getBoneByName( name, source.skeleton ); 237 | 238 | if ( boneTo ) { 239 | 240 | bone = bones[ j ]; 241 | boneData = boneDatas[ j ] = boneDatas[ j ] || { bone: bone }; 242 | 243 | if ( options.hip === name ) { 244 | 245 | if ( ! boneData.pos ) { 246 | 247 | boneData.pos = { 248 | times: new Float32Array( numFrames ), 249 | values: new Float32Array( numFrames * 3 ) 250 | }; 251 | 252 | } 253 | 254 | if ( options.useFirstFramePosition ) { 255 | 256 | if ( i === 0 ) { 257 | 258 | positionOffset = bone.position.clone(); 259 | 260 | } 261 | 262 | bone.position.sub( positionOffset ); 263 | 264 | } 265 | 266 | boneData.pos.times[ i ] = time; 267 | 268 | bone.position.toArray( boneData.pos.values, i * 3 ); 269 | 270 | } 271 | 272 | if ( ! boneData.quat ) { 273 | 274 | boneData.quat = { 275 | times: new Float32Array( numFrames ), 276 | values: new Float32Array( numFrames * 4 ) 277 | }; 278 | 279 | } 280 | 281 | boneData.quat.times[ i ] = time; 282 | 283 | bone.quaternion.toArray( boneData.quat.values, i * 4 ); 284 | 285 | } 286 | 287 | } 288 | 289 | mixer.update( delta ); 290 | 291 | source.updateMatrixWorld(); 292 | 293 | } 294 | 295 | for ( i = 0; i < boneDatas.length; ++ i ) { 296 | 297 | boneData = boneDatas[ i ]; 298 | 299 | if ( boneData ) { 300 | 301 | if ( boneData.pos ) { 302 | 303 | convertedTracks.push( new THREE.VectorKeyframeTrack( 304 | ".bones[" + boneData.bone.name + "].position", 305 | boneData.pos.times, 306 | boneData.pos.values 307 | ) ); 308 | 309 | } 310 | 311 | convertedTracks.push( new THREE.QuaternionKeyframeTrack( 312 | ".bones[" + boneData.bone.name + "].quaternion", 313 | boneData.quat.times, 314 | boneData.quat.values 315 | ) ); 316 | 317 | } 318 | 319 | } 320 | 321 | mixer.uncacheAction( clip ); 322 | 323 | return new THREE.AnimationClip( clip.name, - 1, convertedTracks ); 324 | 325 | }, 326 | 327 | getHelperFromSkeleton: function ( skeleton ) { 328 | 329 | var source = new THREE.SkeletonHelper( skeleton.bones[ 0 ] ); 330 | source.skeleton = skeleton; 331 | 332 | return source; 333 | 334 | }, 335 | 336 | getSkeletonOffsets: function () { 337 | 338 | var targetParentPos = new THREE.Vector3(), 339 | targetPos = new THREE.Vector3(), 340 | sourceParentPos = new THREE.Vector3(), 341 | sourcePos = new THREE.Vector3(), 342 | targetDir = new THREE.Vector2(), 343 | sourceDir = new THREE.Vector2(); 344 | 345 | return function ( target, source, options ) { 346 | 347 | options = options || {}; 348 | options.hip = options.hip !== undefined ? options.hip : "hip"; 349 | options.names = options.names || {}; 350 | 351 | if ( ! source.isObject3D ) { 352 | 353 | source = this.getHelperFromSkeleton( source ); 354 | 355 | } 356 | 357 | var nameKeys = Object.keys( options.names ), 358 | nameValues = Object.values( options.names ), 359 | sourceBones = source.isObject3D ? source.skeleton.bones : this.getBones( source ), 360 | bones = target.isObject3D ? target.skeleton.bones : this.getBones( target ), 361 | offsets = [], 362 | bone, boneTo, 363 | name, i; 364 | 365 | target.skeleton.pose(); 366 | 367 | for ( i = 0; i < bones.length; ++ i ) { 368 | 369 | bone = bones[ i ]; 370 | name = options.names[ bone.name ] || bone.name; 371 | 372 | boneTo = this.getBoneByName( name, sourceBones ); 373 | 374 | if ( boneTo && name !== options.hip ) { 375 | 376 | var boneParent = this.getNearestBone( bone.parent, nameKeys ), 377 | boneToParent = this.getNearestBone( boneTo.parent, nameValues ); 378 | 379 | boneParent.updateMatrixWorld(); 380 | boneToParent.updateMatrixWorld(); 381 | 382 | targetParentPos.setFromMatrixPosition( boneParent.matrixWorld ); 383 | targetPos.setFromMatrixPosition( bone.matrixWorld ); 384 | 385 | sourceParentPos.setFromMatrixPosition( boneToParent.matrixWorld ); 386 | sourcePos.setFromMatrixPosition( boneTo.matrixWorld ); 387 | 388 | targetDir.subVectors( 389 | new THREE.Vector2( targetPos.x, targetPos.y ), 390 | new THREE.Vector2( targetParentPos.x, targetParentPos.y ) 391 | ).normalize(); 392 | 393 | sourceDir.subVectors( 394 | new THREE.Vector2( sourcePos.x, sourcePos.y ), 395 | new THREE.Vector2( sourceParentPos.x, sourceParentPos.y ) 396 | ).normalize(); 397 | 398 | var laterialAngle = targetDir.angle() - sourceDir.angle(); 399 | 400 | var offset = new THREE.Matrix4().makeRotationFromEuler( 401 | new THREE.Euler( 402 | 0, 403 | 0, 404 | laterialAngle 405 | ) 406 | ); 407 | 408 | bone.matrix.multiply( offset ); 409 | 410 | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale ); 411 | 412 | bone.updateMatrixWorld(); 413 | 414 | offsets[ name ] = offset; 415 | 416 | } 417 | 418 | } 419 | 420 | return offsets; 421 | 422 | }; 423 | 424 | }(), 425 | 426 | renameBones: function ( skeleton, names ) { 427 | 428 | var bones = this.getBones( skeleton ); 429 | 430 | for ( var i = 0; i < bones.length; ++ i ) { 431 | 432 | var bone = bones[ i ]; 433 | 434 | if ( names[ bone.name ] ) { 435 | 436 | bone.name = names[ bone.name ]; 437 | 438 | } 439 | 440 | } 441 | 442 | return this; 443 | 444 | }, 445 | 446 | getBones: function ( skeleton ) { 447 | 448 | return Array.isArray( skeleton ) ? skeleton : skeleton.bones; 449 | 450 | }, 451 | 452 | getBoneByName: function ( name, skeleton ) { 453 | 454 | for ( var i = 0, bones = this.getBones( skeleton ); i < bones.length; i ++ ) { 455 | 456 | if ( name === bones[ i ].name ) 457 | 458 | return bones[ i ]; 459 | 460 | } 461 | 462 | }, 463 | 464 | getNearestBone: function ( bone, names ) { 465 | 466 | while ( bone.isBone ) { 467 | 468 | if ( names.indexOf( bone.name ) !== - 1 ) { 469 | 470 | return bone; 471 | 472 | } 473 | 474 | bone = bone.parent; 475 | 476 | } 477 | 478 | }, 479 | 480 | findBoneTrackData: function ( name, tracks ) { 481 | 482 | var regexp = /\[(.*)\]\.(.*)/, 483 | result = { name: name }; 484 | 485 | for ( var i = 0; i < tracks.length; ++ i ) { 486 | 487 | // 1 is track name 488 | // 2 is track type 489 | var trackData = regexp.exec( tracks[ i ].name ); 490 | 491 | if ( trackData && name === trackData[ 1 ] ) { 492 | 493 | result[ trackData[ 2 ] ] = i; 494 | 495 | } 496 | 497 | } 498 | 499 | return result; 500 | 501 | }, 502 | 503 | getEqualsBonesNames: function ( skeleton, targetSkeleton ) { 504 | 505 | var sourceBones = this.getBones( skeleton ), 506 | targetBones = this.getBones( targetSkeleton ), 507 | bones = []; 508 | 509 | search : for ( var i = 0; i < sourceBones.length; i ++ ) { 510 | 511 | var boneName = sourceBones[ i ].name; 512 | 513 | for ( var j = 0; j < targetBones.length; j ++ ) { 514 | 515 | if ( boneName === targetBones[ j ].name ) { 516 | 517 | bones.push( boneName ); 518 | 519 | continue search; 520 | 521 | } 522 | 523 | } 524 | 525 | } 526 | 527 | return bones; 528 | 529 | }, 530 | 531 | clone: function ( source ) { 532 | 533 | var sourceLookup = new Map(); 534 | var cloneLookup = new Map(); 535 | 536 | var clone = source.clone(); 537 | 538 | parallelTraverse( source, clone, function ( sourceNode, clonedNode ) { 539 | 540 | sourceLookup.set( clonedNode, sourceNode ); 541 | cloneLookup.set( sourceNode, clonedNode ); 542 | 543 | } ); 544 | 545 | clone.traverse( function ( node ) { 546 | 547 | if ( ! node.isSkinnedMesh ) return; 548 | 549 | var clonedMesh = node; 550 | var sourceMesh = sourceLookup.get( node ); 551 | var sourceBones = sourceMesh.skeleton.bones; 552 | 553 | clonedMesh.skeleton = sourceMesh.skeleton.clone(); 554 | clonedMesh.bindMatrix.copy( sourceMesh.bindMatrix ); 555 | 556 | clonedMesh.skeleton.bones = sourceBones.map( function ( bone ) { 557 | 558 | return cloneLookup.get( bone ); 559 | 560 | } ); 561 | 562 | clonedMesh.bind( clonedMesh.skeleton, clonedMesh.bindMatrix ); 563 | 564 | } ); 565 | 566 | return clone; 567 | 568 | } 569 | 570 | }; 571 | 572 | 573 | function parallelTraverse( a, b, callback ) { 574 | 575 | callback( a, b ); 576 | 577 | for ( var i = 0; i < a.children.length; i ++ ) { 578 | 579 | parallelTraverse( a.children[ i ], b.children[ i ], callback ); 580 | 581 | } 582 | 583 | } 584 | -------------------------------------------------------------------------------- /public/libs/draco/draco_decoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixmariotto/Edelweiss/4181238d5a50011309f7be714ffe60105d06dd9b/public/libs/draco/draco_decoder.wasm -------------------------------------------------------------------------------- /public/libs/ua-parser.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * UAParser.js v0.7.21 3 | * Lightweight JavaScript-based User-Agent string parser 4 | * https://github.com/faisalman/ua-parser-js 5 | * 6 | * Copyright © 2012-2019 Faisal Salman 7 | * Licensed under MIT License 8 | */ 9 | (function(window,undefined){"use strict";var LIBVERSION="0.7.21",EMPTY="",UNKNOWN="?",FUNC_TYPE="function",UNDEF_TYPE="undefined",OBJ_TYPE="object",STR_TYPE="string",MAJOR="major",MODEL="model",NAME="name",TYPE="type",VENDOR="vendor",VERSION="version",ARCHITECTURE="architecture",CONSOLE="console",MOBILE="mobile",TABLET="tablet",SMARTTV="smarttv",WEARABLE="wearable",EMBEDDED="embedded";var util={extend:function(regexes,extensions){var mergedRegexes={};for(var i in regexes){if(extensions[i]&&extensions[i].length%2===0){mergedRegexes[i]=extensions[i].concat(regexes[i])}else{mergedRegexes[i]=regexes[i]}}return mergedRegexes},has:function(str1,str2){if(typeof str1==="string"){return str2.toLowerCase().indexOf(str1.toLowerCase())!==-1}else{return false}},lowerize:function(str){return str.toLowerCase()},major:function(version){return typeof version===STR_TYPE?version.replace(/[^\d\.]/g,"").split(".")[0]:undefined},trim:function(str){return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}};var mapper={rgx:function(ua,arrays){var i=0,j,k,p,q,matches,match;while(i0){if(q.length==2){if(typeof q[1]==FUNC_TYPE){this[q[0]]=q[1].call(this,match)}else{this[q[0]]=q[1]}}else if(q.length==3){if(typeof q[1]===FUNC_TYPE&&!(q[1].exec&&q[1].test)){this[q[0]]=match?q[1].call(this,match,q[2]):undefined}else{this[q[0]]=match?match.replace(q[1],q[2]):undefined}}else if(q.length==4){this[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}}else{this[q]=match?match:undefined}}}}i+=2}},str:function(str,map){for(var i in map){if(typeof map[i]===OBJ_TYPE&&map[i].length>0){for(var j=0;j= 0 ) return false; 117 | if( Math.abs(deltaX) > 2*Math.abs(deltaY) ) return false; 118 | return true; 119 | } 120 | VirtualJoystick.prototype.down = function(){ 121 | if( this._pressed === false ) return false; 122 | var deltaX = this.deltaX(); 123 | var deltaY = this.deltaY(); 124 | if( deltaY <= 0 ) return false; 125 | if( Math.abs(deltaX) > 2*Math.abs(deltaY) ) return false; 126 | return true; 127 | } 128 | VirtualJoystick.prototype.right = function(){ 129 | if( this._pressed === false ) return false; 130 | var deltaX = this.deltaX(); 131 | var deltaY = this.deltaY(); 132 | if( deltaX <= 0 ) return false; 133 | if( Math.abs(deltaY) > 2*Math.abs(deltaX) ) return false; 134 | return true; 135 | } 136 | VirtualJoystick.prototype.left = function(){ 137 | if( this._pressed === false ) return false; 138 | var deltaX = this.deltaX(); 139 | var deltaY = this.deltaY(); 140 | if( deltaX >= 0 ) return false; 141 | if( Math.abs(deltaY) > 2*Math.abs(deltaX) ) return false; 142 | return true; 143 | } 144 | 145 | ////////////////////////////////////////////////////////////////////////////////// 146 | // // 147 | ////////////////////////////////////////////////////////////////////////////////// 148 | 149 | VirtualJoystick.prototype._onUp = function() 150 | { 151 | this._pressed = false; 152 | this._stickEl.style.display = "none"; 153 | 154 | if(this._stationaryBase == false){ 155 | this._baseEl.style.display = "none"; 156 | 157 | this._baseX = this._baseY = 0; 158 | this._stickX = this._stickY = 0; 159 | } 160 | } 161 | 162 | VirtualJoystick.prototype._onDown = function(x, y) 163 | { 164 | this._pressed = true; 165 | if(this._stationaryBase == false){ 166 | this._baseX = x; 167 | this._baseY = y; 168 | this._baseEl.style.display = ""; 169 | this._move(this._baseEl.style, (this._baseX - this._baseEl.width /2), (this._baseY - this._baseEl.height/2)); 170 | } 171 | 172 | this._stickX = x; 173 | this._stickY = y; 174 | 175 | if(this._limitStickTravel === true){ 176 | var deltaX = this.deltaX(); 177 | var deltaY = this.deltaY(); 178 | var stickDistance = Math.sqrt( (deltaX * deltaX) + (deltaY * deltaY) ); 179 | if(stickDistance > this._stickRadius){ 180 | var stickNormalizedX = deltaX / stickDistance; 181 | var stickNormalizedY = deltaY / stickDistance; 182 | 183 | this._stickX = stickNormalizedX * this._stickRadius + this._baseX; 184 | this._stickY = stickNormalizedY * this._stickRadius + this._baseY; 185 | } 186 | } 187 | 188 | this._stickEl.style.display = ""; 189 | this._move(this._stickEl.style, (this._stickX - this._stickEl.width /2), (this._stickY - this._stickEl.height/2)); 190 | } 191 | 192 | VirtualJoystick.prototype._onMove = function(x, y) 193 | { 194 | if( this._pressed === true ){ 195 | this._stickX = x; 196 | this._stickY = y; 197 | 198 | if(this._limitStickTravel === true){ 199 | var deltaX = this.deltaX(); 200 | var deltaY = this.deltaY(); 201 | var stickDistance = Math.sqrt( (deltaX * deltaX) + (deltaY * deltaY) ); 202 | if(stickDistance > this._stickRadius){ 203 | var stickNormalizedX = deltaX / stickDistance; 204 | var stickNormalizedY = deltaY / stickDistance; 205 | 206 | this._stickX = stickNormalizedX * this._stickRadius + this._baseX; 207 | this._stickY = stickNormalizedY * this._stickRadius + this._baseY; 208 | } 209 | } 210 | 211 | this._move(this._stickEl.style, (this._stickX - this._stickEl.width /2), (this._stickY - this._stickEl.height/2)); 212 | } 213 | } 214 | 215 | 216 | ////////////////////////////////////////////////////////////////////////////////// 217 | // bind touch events (and mouse events for debug) // 218 | ////////////////////////////////////////////////////////////////////////////////// 219 | 220 | VirtualJoystick.prototype._onMouseUp = function(event) 221 | { 222 | return this._onUp(); 223 | } 224 | 225 | VirtualJoystick.prototype._onMouseDown = function(event) 226 | { 227 | event.preventDefault(); 228 | var x = event.clientX; 229 | var y = event.clientY; 230 | return this._onDown(x, y); 231 | } 232 | 233 | VirtualJoystick.prototype._onMouseMove = function(event) 234 | { 235 | var x = event.clientX; 236 | var y = event.clientY; 237 | return this._onMove(x, y); 238 | } 239 | 240 | ////////////////////////////////////////////////////////////////////////////////// 241 | // comment // 242 | ////////////////////////////////////////////////////////////////////////////////// 243 | 244 | VirtualJoystick.prototype._onTouchStart = function(event) 245 | { 246 | // if there is already a touch inprogress do nothing 247 | if( this._touchIdx !== null ) return; 248 | 249 | // notify event for validation 250 | var isValid = this.dispatchEvent('touchStartValidation', event); 251 | if( isValid === false ) return; 252 | 253 | // dispatch touchStart 254 | this.dispatchEvent('touchStart', event); 255 | 256 | event.preventDefault(); 257 | // get the first who changed 258 | var touch = event.changedTouches[0]; 259 | // set the touchIdx of this joystick 260 | this._touchIdx = touch.identifier; 261 | 262 | // forward the action 263 | var x = touch.pageX; 264 | var y = touch.pageY; 265 | return this._onDown(x, y) 266 | } 267 | 268 | VirtualJoystick.prototype._onTouchEnd = function(event) 269 | { 270 | // if there is no touch in progress, do nothing 271 | if( this._touchIdx === null ) return; 272 | 273 | // dispatch touchEnd 274 | this.dispatchEvent('touchEnd', event); 275 | 276 | // try to find our touch event 277 | var touchList = event.changedTouches; 278 | for(var i = 0; i < touchList.length && touchList[i].identifier !== this._touchIdx; i++); 279 | // if touch event isnt found, 280 | if( i === touchList.length) return; 281 | 282 | // reset touchIdx - mark it as no-touch-in-progress 283 | this._touchIdx = null; 284 | 285 | //?????? 286 | // no preventDefault to get click event on ios 287 | event.preventDefault(); 288 | 289 | return this._onUp() 290 | } 291 | 292 | VirtualJoystick.prototype._onTouchMove = function(event) 293 | { 294 | // if there is no touch in progress, do nothing 295 | if( this._touchIdx === null ) return; 296 | 297 | // try to find our touch event 298 | var touchList = event.changedTouches; 299 | for(var i = 0; i < touchList.length && touchList[i].identifier !== this._touchIdx; i++ ); 300 | // if touch event with the proper identifier isnt found, do nothing 301 | if( i === touchList.length) return; 302 | var touch = touchList[i]; 303 | 304 | event.preventDefault(); 305 | 306 | var x = touch.pageX; 307 | var y = touch.pageY; 308 | return this._onMove(x, y) 309 | } 310 | 311 | 312 | ////////////////////////////////////////////////////////////////////////////////// 313 | // build default stickEl and baseEl // 314 | ////////////////////////////////////////////////////////////////////////////////// 315 | 316 | /** 317 | * build the canvas for joystick base 318 | */ 319 | VirtualJoystick.prototype._buildJoystickBase = function() 320 | { 321 | var canvas = document.createElement( 'canvas' ); 322 | canvas.width = 126; 323 | canvas.height = 126; 324 | 325 | var ctx = canvas.getContext('2d'); 326 | ctx.beginPath(); 327 | ctx.strokeStyle = this._strokeStyle; 328 | ctx.lineWidth = 6; 329 | ctx.arc( canvas.width/2, canvas.width/2, 40, 0, Math.PI*2, true); 330 | ctx.stroke(); 331 | 332 | ctx.beginPath(); 333 | ctx.strokeStyle = this._strokeStyle; 334 | ctx.lineWidth = 2; 335 | ctx.arc( canvas.width/2, canvas.width/2, 60, 0, Math.PI*2, true); 336 | ctx.stroke(); 337 | 338 | return canvas; 339 | } 340 | 341 | /** 342 | * build the canvas for joystick stick 343 | */ 344 | VirtualJoystick.prototype._buildJoystickStick = function() 345 | { 346 | var canvas = document.createElement( 'canvas' ); 347 | canvas.width = 86; 348 | canvas.height = 86; 349 | var ctx = canvas.getContext('2d'); 350 | ctx.beginPath(); 351 | ctx.strokeStyle = this._strokeStyle; 352 | ctx.lineWidth = 6; 353 | ctx.arc( canvas.width/2, canvas.width/2, 40, 0, Math.PI*2, true); 354 | ctx.stroke(); 355 | return canvas; 356 | } 357 | 358 | ////////////////////////////////////////////////////////////////////////////////// 359 | // move using translate3d method with fallback to translate > 'top' and 'left' 360 | // modified from https://github.com/component/translate and dependents 361 | ////////////////////////////////////////////////////////////////////////////////// 362 | 363 | VirtualJoystick.prototype._move = function(style, x, y) 364 | { 365 | if (this._transform) { 366 | if (this._has3d) { 367 | style[this._transform] = 'translate3d(' + x + 'px,' + y + 'px, 0)'; 368 | } else { 369 | style[this._transform] = 'translate(' + x + 'px,' + y + 'px)'; 370 | } 371 | } else { 372 | style.left = x + 'px'; 373 | style.top = y + 'px'; 374 | } 375 | } 376 | 377 | VirtualJoystick.prototype._getTransformProperty = function() 378 | { 379 | var styles = [ 380 | 'webkitTransform', 381 | 'MozTransform', 382 | 'msTransform', 383 | 'OTransform', 384 | 'transform' 385 | ]; 386 | 387 | var el = document.createElement('p'); 388 | var style; 389 | 390 | for (var i = 0; i < styles.length; i++) { 391 | style = styles[i]; 392 | if (null != el.style[style]) { 393 | return style; 394 | } 395 | } 396 | } 397 | 398 | VirtualJoystick.prototype._check3D = function() 399 | { 400 | var prop = this._getTransformProperty(); 401 | // IE8<= doesn't have `getComputedStyle` 402 | if (!prop || !window.getComputedStyle) return module.exports = false; 403 | 404 | var map = { 405 | webkitTransform: '-webkit-transform', 406 | OTransform: '-o-transform', 407 | msTransform: '-ms-transform', 408 | MozTransform: '-moz-transform', 409 | transform: 'transform' 410 | }; 411 | 412 | // from: https://gist.github.com/lorenzopolidori/3794226 413 | var el = document.createElement('div'); 414 | el.style[prop] = 'translate3d(1px,1px,1px)'; 415 | document.body.insertBefore(el, null); 416 | var val = getComputedStyle(el).getPropertyValue(map[prop]); 417 | document.body.removeChild(el); 418 | var exports = null != val && val.length && 'none' != val; 419 | return exports; 420 | } 421 | --------------------------------------------------------------------------------