├── .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 | 
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