wco_models 3.1.0.324 → 3.1.0.326

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/production_theme.scss +4 -1
  3. data/app/assets/stylesheets/wco/main.scss +4 -0
  4. data/app/assets/stylesheets/wco/studio.scss +12 -4
  5. data/app/controllers/wco/api/newspartials_controller.rb +1 -1
  6. data/app/controllers/wco/api/newsproducer_controller.rb +5 -0
  7. data/app/controllers/wco/newspartials_controller.rb +1 -0
  8. data/app/models/iro/datapoint.rb +1 -1
  9. data/app/models/iro/option.rb +16 -2
  10. data/app/models/iro/position.rb +4 -6
  11. data/app/models/iro/priceitem.rb +42 -4
  12. data/app/models/iro/purse.rb +2 -0
  13. data/app/models/iro/stock.rb +1 -2
  14. data/app/models/tda/option.rb +4 -9
  15. data/app/models/wco/newspartial.rb +12 -4
  16. data/app/views/wco/api/newsproducer/README_newsproducer.txt +12 -0
  17. data/app/views/wco/api/newsproducer/_scenes.js-bk +20 -0
  18. data/app/views/wco/api/newsproducer/_scenes.js.erb +15 -0
  19. data/app/views/wco/api/newsproducer/{_studio_blue.js → _studio_blue1.js} +4 -1
  20. data/app/views/wco/api/newsproducer/_studio_blue2.js +912 -0
  21. data/app/views/wco/api/newsproducer/_studio_coc.js +769 -0
  22. data/app/views/wco/api/newsproducer/_studio_coc.js-bk +389 -0
  23. data/app/views/wco/api/newsproducer/_studio_coc.js-cannon +677 -0
  24. data/app/views/wco/api/newsproducer/_studio_fps.js +1018 -0
  25. data/app/views/wco/api/newsproducer/_studio_ra2.js +255 -0
  26. data/app/views/wco/api/newsproducer/_touch_controls.js +708 -0
  27. data/app/views/wco/api/newsproducer/_touch_pad.css +200 -0
  28. data/app/views/wco/api/newsproducer/games_fps.html +504 -0
  29. data/app/views/wco/api/newsproducer/{studio_green.html.erb → studio_blue0.html.erb} +1 -1
  30. data/app/views/wco/api/newsproducer/{studio_blue.html.erb → studio_blue1.html.erb} +3 -3
  31. data/app/views/wco/api/newsproducer/studio_blue2.html.erb +146 -0
  32. data/app/views/wco/api/newsproducer/studio_coc.html.erb +95 -0
  33. data/app/views/wco/api/newsproducer/studio_fps.html.erb +199 -0
  34. data/app/views/wco/api/newsproducer/studio_ra2.html.erb +95 -0
  35. data/app/views/wco/newspartials/_show_in_newsvideo.haml +6 -7
  36. data/app/views/wco/newspartials/index.haml-trash +6 -0
  37. data/config/initializers/08_integrations.rb +14 -1
  38. data/config/initializers/08_integrations.rb-example +0 -2
  39. data/config/routes.rb +3 -0
  40. data/lib/shortcuts.rb +1 -1
  41. data/lib/tasks/office_tasks.rake +7 -5
  42. metadata +23 -7
  43. data/app/views/wco/api/newsproducer/README.txt +0 -2
  44. /data/app/views/wco/api/newsproducer/{_studio_green.js → _studio_blue0.js} +0 -0
@@ -0,0 +1,1018 @@
1
+
2
+ /*
3
+ **/
4
+ const logg = (a, b="", c=null) => {
5
+ if ('undefined' === typeof window) { return }
6
+ c = "string" === typeof c ? c : b.replace(/\W/g, '')
7
+ if (c.length) { window[c] = a }
8
+ console.log(`+++ ${b}:`, a) // eslint-disable-line no-console
9
+ }
10
+
11
+ document.addEventListener("keydown", e => {
12
+ if (e.code === "Space" && !["INPUT", "TEXTAREA"].includes(e.target.tagName)) {
13
+ e.preventDefault()
14
+ }
15
+ })
16
+
17
+ // const MODELS_ROOT = '/vendor/models'
18
+ // const MODELS_ROOT = "https://cdn.jsdelivr.net/gh/wasya-co/ishlib3js@0.4.0/public/vendor/models"
19
+ const SCENE_STOR = 'studio'
20
+ const scenes = await fetch('https://cdn.jsdelivr.net/gh/wasya-co/ishlib3js@0.4.3/public/vendor/models/scenes/index.json').then(r => r.json())
21
+ $.each(scenes, (name) => {
22
+ $('<option>', { value: name, text: name }).appendTo($('select.studio'))
23
+ })
24
+ try {
25
+ const saved = localStorage.getItem( SCENE_STOR )
26
+ if (saved && scenes[saved]) $('select.studio').val(saved)
27
+ } catch (error) {
28
+ console.log(error)
29
+ }
30
+
31
+ let scene_url = scenes[$('select.studio').val()] || scenes.collision_world
32
+ let specsheet = null
33
+
34
+ let width = 50 // 854
35
+ let height = 50 // 480
36
+ function is_mobile() {
37
+ return window.matchMedia('(max-width: 768px)').matches || /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)
38
+ }
39
+ function size_canvas() {
40
+ if (is_mobile()) {
41
+ width = Math.round(window.innerWidth * 0.8)
42
+ height = Math.round(window.innerHeight * 0.8)
43
+ } else {
44
+ width = 854
45
+ height = 480
46
+ }
47
+ }
48
+ size_canvas()
49
+ let slug = '<ccapture>'
50
+ const fps = 30
51
+
52
+
53
+ import * as THREE from 'three';
54
+
55
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
56
+
57
+ import { Octree } from 'three/addons/math/Octree.js';
58
+ import { OctreeHelper } from 'three/addons/helpers/OctreeHelper.js';
59
+
60
+ import { Capsule } from 'three/addons/math/Capsule.js';
61
+ import { TransformControls } from 'three/addons/controls/TransformControls.js';
62
+
63
+ window.THREE = THREE
64
+ const TouchControls = window.TouchControls
65
+ const MovementPad = window.MovementPad
66
+ const RotationPad = window.RotationPad
67
+
68
+ const timer = new THREE.Timer();
69
+ timer.connect( document );
70
+
71
+ const scene = new THREE.Scene();
72
+ scene.background = new THREE.Color( 0x88ccee );
73
+ scene.fog = new THREE.Fog( 0x88ccee, 0, 50 );
74
+
75
+ const camera = new THREE.PerspectiveCamera( 70, width / height, 0.1, 1000 );
76
+ camera.rotation.order = 'YXZ';
77
+
78
+ const fillLight1 = new THREE.HemisphereLight( 0x8dc1de, 0x00668d, 1.5 );
79
+ fillLight1.position.set( 2, 1, 1 );
80
+ scene.add( fillLight1 );
81
+
82
+ const directionalLight = new THREE.DirectionalLight( 0xffffff, 2.5 );
83
+ directionalLight.position.set( - 5, 25, - 1 );
84
+ directionalLight.castShadow = true;
85
+ directionalLight.shadow.camera.near = 0.01;
86
+ directionalLight.shadow.camera.far = 500;
87
+ directionalLight.shadow.camera.right = 30;
88
+ directionalLight.shadow.camera.left = - 30;
89
+ directionalLight.shadow.camera.top = 30;
90
+ directionalLight.shadow.camera.bottom = - 30;
91
+ directionalLight.shadow.mapSize.width = 1024;
92
+ directionalLight.shadow.mapSize.height = 1024;
93
+ directionalLight.shadow.radius = 4;
94
+ directionalLight.shadow.bias = - 0.00006;
95
+ scene.add( directionalLight );
96
+
97
+ const container = document.getElementById( 'rotatingC' );
98
+ container.style.touchAction = 'none'
99
+ container.style.width = width + 'px'
100
+ container.style.height = height + 'px'
101
+
102
+ const renderer = new THREE.WebGLRenderer( { antialias: true } );
103
+ renderer.setPixelRatio( window.devicePixelRatio );
104
+ renderer.setSize( width, height );
105
+ renderer.setAnimationLoop( animate );
106
+ renderer.shadowMap.enabled = true;
107
+ renderer.shadowMap.type = THREE.VSMShadowMap;
108
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
109
+ renderer.domElement.style.touchAction = 'none'
110
+ container.appendChild( renderer.domElement );
111
+
112
+ let touch_controls = null
113
+
114
+ const GRAVITY = 30
115
+ let walk_speed = 12
116
+ const AIR_SPEED_FACTOR = 0.32
117
+ let strafe_factor = 0.5
118
+ let strafe_deadzone = 0.35
119
+
120
+ const NUM_SPHERES = 100;
121
+ const SPHERE_RADIUS = 0.2;
122
+
123
+ const STEPS_PER_FRAME = 5;
124
+
125
+ const sphereGeometry = new THREE.IcosahedronGeometry( SPHERE_RADIUS, 5 );
126
+ const sphereMaterial = new THREE.MeshLambertMaterial( { color: 0xdede8d } );
127
+
128
+ const spheres = [];
129
+ let sphereIdx = 0;
130
+
131
+ for ( let i = 0; i < NUM_SPHERES; i ++ ) {
132
+
133
+ const sphere = new THREE.Mesh( sphereGeometry, sphereMaterial );
134
+ sphere.castShadow = true;
135
+ sphere.receiveShadow = true;
136
+
137
+ scene.add( sphere );
138
+
139
+ spheres.push( {
140
+ mesh: sphere,
141
+ collider: new THREE.Sphere( new THREE.Vector3( 0, - 100, 0 ), SPHERE_RADIUS ),
142
+ velocity: new THREE.Vector3()
143
+ } );
144
+
145
+ }
146
+
147
+ function weapons_on() {
148
+ return $('input[name=weaponCtrl]').is(':checked')
149
+ }
150
+
151
+ function set_weapons( on ) {
152
+ spheres.forEach( sphere => {
153
+ sphere.mesh.visible = on
154
+ if ( !on ) {
155
+ sphere.collider.center.set( 0, -100, 0 )
156
+ sphere.velocity.set( 0, 0, 0 )
157
+ sphere.mesh.position.copy( sphere.collider.center )
158
+ }
159
+ } )
160
+ }
161
+
162
+ let worldOctree = new Octree()
163
+ let octree_helper = null
164
+ let studio = null
165
+
166
+ const playerCollider = new Capsule( new THREE.Vector3( 0, 0.35, 0 ), new THREE.Vector3( 0, 1, 0 ), 0.35 );
167
+
168
+ const playerVelocity = new THREE.Vector3();
169
+ const playerDirection = new THREE.Vector3();
170
+
171
+ let playerOnFloor = false;
172
+ let mouseTime = 0;
173
+
174
+ const keyStates = {};
175
+
176
+ const vector1 = new THREE.Vector3();
177
+ const vector2 = new THREE.Vector3();
178
+ const vector3 = new THREE.Vector3();
179
+
180
+ const DOOR_W = 0.9
181
+ const DOOR_H = 2.1
182
+ const DOOR_D = 0.1
183
+ const door_color = new THREE.Color( 0x8a6a4a )
184
+ const door_aim_color = new THREE.Color( 0xffff00 )
185
+ const door_geo = new THREE.BoxGeometry( DOOR_W, DOOR_H, DOOR_D )
186
+ const door_label_geo = new THREE.PlaneGeometry( DOOR_W, DOOR_H )
187
+ const doors = []
188
+ let door_inside = null
189
+ let last_created_door = null
190
+
191
+ function wrap_text( ctx, text, max_width ) {
192
+ const words = String( text || '' ).replace( /_/g, ' ' ).split( /\s+/ ).filter( Boolean )
193
+ const lines = []
194
+ let line = ''
195
+ words.forEach( word => {
196
+ const test = line ? ( line + ' ' + word ) : word
197
+ if ( ctx.measureText( test ).width <= max_width ) {
198
+ line = test
199
+ } else {
200
+ if ( line ) lines.push( line )
201
+ if ( ctx.measureText( word ).width <= max_width ) {
202
+ line = word
203
+ } else {
204
+ let chunk = ''
205
+ for ( let i = 0; i < word.length; i++ ) {
206
+ const next = chunk + word[i]
207
+ if ( ctx.measureText( next ).width <= max_width ) {
208
+ chunk = next
209
+ } else {
210
+ if ( chunk ) lines.push( chunk )
211
+ chunk = word[i]
212
+ }
213
+ }
214
+ line = chunk
215
+ }
216
+ }
217
+ } )
218
+ if ( line ) lines.push( line )
219
+ return lines
220
+ }
221
+
222
+ function door_label_texture( text ) {
223
+ const canvas = document.createElement( 'canvas' )
224
+ canvas.width = 512
225
+ canvas.height = Math.round( 512 * DOOR_H / DOOR_W )
226
+ const ctx = canvas.getContext( '2d' )
227
+ const pad = 36
228
+ const font_size = 72
229
+ ctx.fillStyle = '#ffffff'
230
+ ctx.font = 'bold ' + font_size + 'px sans-serif'
231
+ ctx.textAlign = 'center'
232
+ ctx.textBaseline = 'middle'
233
+ const lines = wrap_text( ctx, text, canvas.width - pad * 2 )
234
+ const line_h = font_size * 1.15
235
+ const start_y = canvas.height / 2 - ( ( lines.length - 1 ) * line_h ) / 2
236
+ lines.forEach( ( line, i ) => {
237
+ ctx.fillText( line, canvas.width / 2, start_y + i * line_h )
238
+ } )
239
+ const tex = new THREE.CanvasTexture( canvas )
240
+ tex.needsUpdate = true
241
+ return tex
242
+ }
243
+
244
+ function make_door( marker ) {
245
+ const name = marker.label || marker.name
246
+ const pos = marker.position || {}
247
+ const rot = marker.rotation || {}
248
+ const mat = new THREE.MeshLambertMaterial( { color: door_color } )
249
+ const mesh = new THREE.Mesh( door_geo, mat )
250
+ mesh.position.set( pos.x || 0, pos.y || 0, pos.z || 0 )
251
+ mesh.rotation.set(
252
+ THREE.MathUtils.degToRad(rot.x || 0),
253
+ THREE.MathUtils.degToRad(rot.y || 0),
254
+ THREE.MathUtils.degToRad(rot.z || 0)
255
+ )
256
+ mesh.castShadow = true
257
+ mesh.receiveShadow = true
258
+ mesh.userData.scene_name = name
259
+ mesh.userData.door_mat = mat
260
+
261
+ const label_mat = new THREE.MeshBasicMaterial( {
262
+ map: door_label_texture( name ),
263
+ transparent: true,
264
+ side: THREE.DoubleSide,
265
+ depthWrite: false
266
+ } )
267
+ const front = new THREE.Mesh( door_label_geo, label_mat )
268
+ front.position.set( 0, 0, DOOR_D / 2 + 0.006 )
269
+ mesh.add( front )
270
+ const back = new THREE.Mesh( door_label_geo, label_mat )
271
+ back.position.set( 0, 0, -( DOOR_D / 2 + 0.006 ) )
272
+ back.rotation.y = Math.PI
273
+ mesh.add( back )
274
+
275
+ scene.add( mesh )
276
+ doors.push( mesh )
277
+ return mesh
278
+ }
279
+
280
+ function clear_doors() {
281
+ if ( door_helper ) {
282
+ door_helper.detach()
283
+ if ( door_helper_root ) door_helper_root.visible = false
284
+ }
285
+ doors.forEach( d => {
286
+ if ( d.parent ) d.parent.remove( d )
287
+ } )
288
+ doors.length = 0
289
+ door_inside = null
290
+ last_created_door = null
291
+ }
292
+
293
+ function place_doors_from_specsheet() {
294
+ clear_doors()
295
+ const markers = specsheet && specsheet.markers
296
+ if ( !markers || !markers.length ) return
297
+ markers.forEach( make_door )
298
+ if ( door_helper_on() ) set_door_helper( true )
299
+ }
300
+
301
+ let door_helper = null
302
+ let door_helper_root = null
303
+
304
+ function door_helper_on() {
305
+ return $('input[name=doorHelper]').is(':checked') || $('input[name=doorRotate]').is(':checked')
306
+ }
307
+
308
+ function door_helper_mode() {
309
+ if ($('input[name=doorRotate]').is(':checked')) return 'rotate'
310
+ if ($('input[name=doorHelper]').is(':checked')) return 'translate'
311
+ return null
312
+ }
313
+
314
+ function log_door_position() {
315
+ const obj = door_helper && door_helper.object
316
+ if ( !obj ) return
317
+ logg( {
318
+ x: obj.position.x,
319
+ y: obj.position.y,
320
+ z: obj.position.z,
321
+ name: obj.userData.scene_name
322
+ }, 'door position' )
323
+ }
324
+
325
+ function log_door_rotation() {
326
+ const obj = door_helper && door_helper.object
327
+ if ( !obj ) return
328
+ logg( {
329
+ x: THREE.MathUtils.radToDeg(obj.rotation.x),
330
+ y: THREE.MathUtils.radToDeg(obj.rotation.y),
331
+ z: THREE.MathUtils.radToDeg(obj.rotation.z),
332
+ name: obj.userData.scene_name
333
+ }, 'door rotation' )
334
+ }
335
+
336
+ function log_door_transform() {
337
+ if ( door_helper_mode() === 'rotate' ) log_door_rotation()
338
+ else log_door_position()
339
+ }
340
+
341
+ function ensure_door_helper() {
342
+ if (door_helper) return
343
+ door_helper = new TransformControls(camera, renderer.domElement)
344
+ door_helper.addEventListener('objectChange', log_door_transform)
345
+ door_helper_root = door_helper.getHelper ? door_helper.getHelper() : door_helper
346
+ scene.add(door_helper_root)
347
+ }
348
+
349
+ function set_door_helper(on) {
350
+ if (on) {
351
+ if (document.pointerLockElement) document.exitPointerLock()
352
+ ensure_door_helper()
353
+ const target = last_created_door || doors[doors.length - 1]
354
+ if (!target) {
355
+ door_helper.detach()
356
+ door_helper.enabled = false
357
+ if (door_helper_root) door_helper_root.visible = false
358
+ return
359
+ }
360
+ door_helper.enabled = true
361
+ door_helper.setMode(door_helper_mode() || 'translate')
362
+ door_helper.attach(target)
363
+ if (door_helper_root) door_helper_root.visible = true
364
+ log_door_transform()
365
+ } else if (door_helper) {
366
+ door_helper.detach()
367
+ door_helper.enabled = false
368
+ if (door_helper_root) door_helper_root.visible = false
369
+ }
370
+ }
371
+
372
+ $('input[name=doorHelper]').on('change', function() {
373
+ if (this.checked) $('input[name=doorRotate]').prop('checked', false)
374
+ set_door_helper(door_helper_on())
375
+ })
376
+
377
+ $('input[name=doorRotate]').on('change', function() {
378
+ if (this.checked) $('input[name=doorHelper]').prop('checked', false)
379
+ set_door_helper(door_helper_on())
380
+ })
381
+
382
+ $('#createDoor').on('click', function() {
383
+ last_created_door = make_door({ name: '', position: { x: 0, y: 0, z: 0 } })
384
+ if (door_helper_on()) {
385
+ if (document.pointerLockElement) document.exitPointerLock()
386
+ ensure_door_helper()
387
+ door_helper.enabled = true
388
+ door_helper.setMode(door_helper_mode() || 'translate')
389
+ door_helper.attach(last_created_door)
390
+ if (door_helper_root) door_helper_root.visible = true
391
+ log_door_transform()
392
+ }
393
+ })
394
+
395
+ const aim_ray = new THREE.Raycaster()
396
+ const aim_dir = new THREE.Vector3()
397
+ const door_box = new THREE.Box3()
398
+ const player_box = new THREE.Box3()
399
+ let studio_loading = false
400
+
401
+ function player_in_door( mesh ) {
402
+ mesh.updateMatrixWorld(true)
403
+ door_box.setFromObject(mesh)
404
+ door_box.expandByScalar(0.3)
405
+ player_box.makeEmpty()
406
+ player_box.expandByPoint(playerCollider.start)
407
+ player_box.expandByPoint(playerCollider.end)
408
+ player_box.expandByScalar(playerCollider.radius)
409
+ return player_box.intersectsBox(door_box)
410
+ }
411
+
412
+ function check_door_portal() {
413
+ const hit = doors.find( d => player_in_door( d ) )
414
+ if ( hit && hit !== door_inside && !studio_loading ) {
415
+ const dest = hit.userData.scene_name
416
+ if ( dest && scenes[dest] && $('select.studio').val() !== dest ) {
417
+ $('select.studio').val( dest )
418
+ select_studio( dest )
419
+ }
420
+ }
421
+ door_inside = hit || null
422
+ }
423
+
424
+ function door_from_hit( obj ) {
425
+ while ( obj ) {
426
+ if ( doors.indexOf( obj ) !== -1 ) return obj
427
+ obj = obj.parent
428
+ }
429
+ return null
430
+ }
431
+
432
+ function update_door_aim() {
433
+ camera.getWorldDirection( aim_dir )
434
+ aim_ray.set( camera.position, aim_dir )
435
+ const hits = aim_ray.intersectObjects( doors, true )
436
+ const aimed = hits.length ? door_from_hit( hits[0].object ) : null
437
+ doors.forEach( d => {
438
+ const mat = d.userData.door_mat
439
+ if ( d === aimed ) {
440
+ mat.color.copy( door_aim_color )
441
+ mat.emissive.setHex( 0x333300 )
442
+ } else {
443
+ mat.color.copy( door_color )
444
+ mat.emissive.setHex( 0x000000 )
445
+ }
446
+ } )
447
+ }
448
+
449
+ document.addEventListener( 'keydown', ( event ) => {
450
+
451
+ keyStates[ event.code ] = true;
452
+
453
+ } );
454
+
455
+ document.addEventListener( 'keyup', ( event ) => {
456
+
457
+ keyStates[ event.code ] = false;
458
+
459
+ } );
460
+
461
+ container.addEventListener( 'mousedown', () => {
462
+
463
+ if ( current_ctrl_type() !== 'fps' ) return
464
+ if ( door_helper_on() ) return
465
+ document.body.requestPointerLock();
466
+
467
+ mouseTime = performance.now();
468
+
469
+ } );
470
+
471
+ document.addEventListener( 'mouseup', () => {
472
+
473
+ if ( current_ctrl_type() !== 'fps' ) return
474
+ if ( door_helper_on() ) return
475
+ if ( document.pointerLockElement !== null ) throwBall();
476
+
477
+ } );
478
+
479
+ document.body.addEventListener( 'mousemove', ( event ) => {
480
+
481
+ if ( current_ctrl_type() !== 'fps' ) return
482
+ if ( document.pointerLockElement === document.body ) {
483
+
484
+ camera.rotation.y -= event.movementX / 500;
485
+ camera.rotation.x -= event.movementY / 500;
486
+
487
+ }
488
+
489
+ } );
490
+
491
+ window.addEventListener( 'resize', onWindowResize );
492
+
493
+ function onWindowResize() {
494
+
495
+ size_canvas()
496
+ container.style.width = width + 'px'
497
+ container.style.height = height + 'px'
498
+ camera.aspect = width / height
499
+ camera.updateProjectionMatrix()
500
+ renderer.setSize( width, height )
501
+
502
+ }
503
+
504
+ function throwBall() {
505
+
506
+ if ( !weapons_on() ) return
507
+
508
+ const sphere = spheres[ sphereIdx ];
509
+
510
+ camera.getWorldDirection( playerDirection );
511
+
512
+ sphere.collider.center.copy( playerCollider.end ).addScaledVector( playerDirection, playerCollider.radius * 1.5 );
513
+
514
+ // throw the ball with more force if we hold the button longer, and if we move forward
515
+
516
+ const impulse = 15 + 30 * ( 1 - Math.exp( ( mouseTime - performance.now() ) * 0.001 ) );
517
+
518
+ sphere.velocity.copy( playerDirection ).multiplyScalar( impulse );
519
+ sphere.velocity.addScaledVector( playerVelocity, 2 );
520
+
521
+ sphereIdx = ( sphereIdx + 1 ) % spheres.length;
522
+
523
+ }
524
+
525
+ function playerCollisions() {
526
+
527
+ const result = worldOctree.capsuleIntersect( playerCollider );
528
+
529
+ playerOnFloor = false;
530
+
531
+ if ( result ) {
532
+
533
+ // determine if the surface we bumped into is something we can stand on
534
+
535
+ playerOnFloor = result.normal.y >= 0.15; // allow slopes up to ~81° but ignore sheer vertical walls
536
+
537
+ if ( ! playerOnFloor ) {
538
+
539
+ playerVelocity.addScaledVector( result.normal, - result.normal.dot( playerVelocity ) );
540
+
541
+ }
542
+
543
+ if ( result.depth >= 1e-10 ) {
544
+
545
+ playerCollider.translate( result.normal.multiplyScalar( result.depth ) );
546
+
547
+ }
548
+
549
+ }
550
+
551
+ }
552
+
553
+ function updatePlayer( deltaTime ) {
554
+
555
+ let damping = Math.exp( - 4 * deltaTime ) - 1;
556
+
557
+ if ( ! playerOnFloor ) {
558
+
559
+ playerVelocity.y -= GRAVITY * deltaTime;
560
+
561
+ // small air resistance
562
+ damping *= 0.1;
563
+
564
+ }
565
+
566
+ playerVelocity.addScaledVector( playerVelocity, damping );
567
+
568
+ const deltaPosition = playerVelocity.clone().multiplyScalar( deltaTime );
569
+ playerCollider.translate( deltaPosition );
570
+
571
+ playerCollisions();
572
+
573
+ camera.position.copy( playerCollider.end );
574
+
575
+ }
576
+
577
+ function playerSphereCollision( sphere ) {
578
+
579
+ const center = vector1.addVectors( playerCollider.start, playerCollider.end ).multiplyScalar( 0.5 );
580
+
581
+ const sphere_center = sphere.collider.center;
582
+
583
+ const r = playerCollider.radius + sphere.collider.radius;
584
+ const r2 = r * r;
585
+
586
+ // approximation: player = 3 spheres
587
+
588
+ for ( const point of [ playerCollider.start, playerCollider.end, center ] ) {
589
+
590
+ const d2 = point.distanceToSquared( sphere_center );
591
+
592
+ if ( d2 < r2 ) {
593
+
594
+ const normal = vector1.subVectors( point, sphere_center ).normalize();
595
+ const v1 = vector2.copy( normal ).multiplyScalar( normal.dot( playerVelocity ) );
596
+ const v2 = vector3.copy( normal ).multiplyScalar( normal.dot( sphere.velocity ) );
597
+
598
+ playerVelocity.add( v2 ).sub( v1 );
599
+ sphere.velocity.add( v1 ).sub( v2 );
600
+
601
+ const d = ( r - Math.sqrt( d2 ) ) / 2;
602
+ sphere_center.addScaledVector( normal, - d );
603
+
604
+ }
605
+
606
+ }
607
+
608
+ }
609
+
610
+ function spheresCollisions() {
611
+
612
+ for ( let i = 0, length = spheres.length; i < length; i ++ ) {
613
+
614
+ const s1 = spheres[ i ];
615
+
616
+ for ( let j = i + 1; j < length; j ++ ) {
617
+
618
+ const s2 = spheres[ j ];
619
+
620
+ const d2 = s1.collider.center.distanceToSquared( s2.collider.center );
621
+ const r = s1.collider.radius + s2.collider.radius;
622
+ const r2 = r * r;
623
+
624
+ if ( d2 < r2 ) {
625
+
626
+ const normal = vector1.subVectors( s1.collider.center, s2.collider.center ).normalize();
627
+ const v1 = vector2.copy( normal ).multiplyScalar( normal.dot( s1.velocity ) );
628
+ const v2 = vector3.copy( normal ).multiplyScalar( normal.dot( s2.velocity ) );
629
+
630
+ s1.velocity.add( v2 ).sub( v1 );
631
+ s2.velocity.add( v1 ).sub( v2 );
632
+
633
+ const d = ( r - Math.sqrt( d2 ) ) / 2;
634
+
635
+ s1.collider.center.addScaledVector( normal, d );
636
+ s2.collider.center.addScaledVector( normal, - d );
637
+
638
+ }
639
+
640
+ }
641
+
642
+ }
643
+
644
+ }
645
+
646
+ function updateSpheres( deltaTime ) {
647
+
648
+ if ( !weapons_on() ) return
649
+
650
+ spheres.forEach( sphere => {
651
+
652
+ sphere.collider.center.addScaledVector( sphere.velocity, deltaTime );
653
+
654
+ const result = worldOctree.sphereIntersect( sphere.collider );
655
+
656
+ if ( result ) {
657
+
658
+ sphere.velocity.addScaledVector( result.normal, - result.normal.dot( sphere.velocity ) * 1.5 );
659
+ sphere.collider.center.add( result.normal.multiplyScalar( result.depth ) );
660
+
661
+ } else {
662
+
663
+ sphere.velocity.y -= GRAVITY * deltaTime;
664
+
665
+ }
666
+
667
+ const damping = Math.exp( - 1.5 * deltaTime ) - 1;
668
+ sphere.velocity.addScaledVector( sphere.velocity, damping );
669
+
670
+ playerSphereCollision( sphere );
671
+
672
+ } );
673
+
674
+ spheresCollisions();
675
+
676
+ for ( const sphere of spheres ) {
677
+
678
+ sphere.mesh.position.copy( sphere.collider.center );
679
+
680
+ }
681
+
682
+ }
683
+
684
+ function getForwardVector() {
685
+
686
+ camera.getWorldDirection( playerDirection );
687
+ playerDirection.y = 0;
688
+ playerDirection.normalize();
689
+
690
+ return playerDirection;
691
+
692
+ }
693
+
694
+ function getSideVector() {
695
+
696
+ camera.getWorldDirection( playerDirection );
697
+ playerDirection.y = 0;
698
+ playerDirection.normalize();
699
+ playerDirection.cross( camera.up );
700
+
701
+ return playerDirection;
702
+
703
+ }
704
+
705
+ function controls( deltaTime ) {
706
+
707
+ // gives a bit of air control
708
+ const speedDelta = deltaTime * ( playerOnFloor ? walk_speed : walk_speed * AIR_SPEED_FACTOR )
709
+
710
+ if ( keyStates[ 'KeyW' ] ) {
711
+
712
+ playerVelocity.add( getForwardVector().multiplyScalar( speedDelta ) );
713
+
714
+ }
715
+
716
+ if ( keyStates[ 'KeyS' ] ) {
717
+
718
+ playerVelocity.add( getForwardVector().multiplyScalar( - speedDelta ) );
719
+
720
+ }
721
+
722
+ if ( keyStates[ 'KeyA' ] ) {
723
+
724
+ playerVelocity.add( getSideVector().multiplyScalar( - speedDelta ) );
725
+
726
+ }
727
+
728
+ if ( keyStates[ 'KeyD' ] ) {
729
+
730
+ playerVelocity.add( getSideVector().multiplyScalar( speedDelta ) );
731
+
732
+ }
733
+
734
+ if ( playerOnFloor ) {
735
+
736
+ if ( keyStates[ 'Space' ] ) {
737
+
738
+ playerVelocity.y = 15;
739
+
740
+ }
741
+
742
+ }
743
+
744
+ }
745
+
746
+ function rescale(model, config) {
747
+ const box = new THREE.Box3().setFromObject(model)
748
+ const size = box.getSize(new THREE.Vector3())
749
+ const currentHeight = size.y
750
+ const scale = config.height / currentHeight
751
+ model.scale.setScalar(scale)
752
+ }
753
+
754
+ function scene_cfg(s) {
755
+ logg(s, 'scene_cfg()')
756
+
757
+ if (typeof s === 'string') {
758
+ return { url: s } // , height: 3.3 }
759
+ }
760
+ if (typeof s.url === 'string') {
761
+ return { ...s, url: s.url }
762
+ }
763
+ logg(s, 'out')
764
+ return s
765
+ }
766
+
767
+ function apply_studio_mesh(root) {
768
+ root.traverse(child => {
769
+ if (child.isMesh) {
770
+ child.castShadow = true
771
+ child.receiveShadow = true
772
+ if (child.material && child.material.map) {
773
+ child.material.map.anisotropy = 4
774
+ }
775
+ }
776
+ })
777
+ }
778
+
779
+ function reset_player() {
780
+ playerCollider.start.set(0, 0.35, 0)
781
+ playerCollider.end.set(0, 1, 0)
782
+ playerCollider.radius = 0.35
783
+ playerVelocity.set(0, 0, 0)
784
+ camera.position.copy(playerCollider.end)
785
+ camera.rotation.set(0, 0, 0)
786
+ if (touch_controls) {
787
+ touch_controls.setPosition(0, 1, 0)
788
+ touch_controls.setRotation(0, 0)
789
+ }
790
+ door_inside = doors.find( d => player_in_door( d ) ) || null
791
+ }
792
+
793
+ const loader = new GLTFLoader()
794
+
795
+ function specsheet_url(glb_url) {
796
+ return glb_url.replace(/[^/]+$/, 'specsheet.json')
797
+ }
798
+
799
+ async function load_specsheet(glb_url) {
800
+ specsheet = null
801
+ try {
802
+ const res = await fetch(specsheet_url(glb_url))
803
+ if (!res.ok) return
804
+ specsheet = await res.json()
805
+ logg(specsheet, 'specsheet')
806
+ } catch (error) {
807
+ console.log(error)
808
+ }
809
+ }
810
+
811
+ async function load_studio(s) {
812
+ const cfg = scene_cfg(s)
813
+ if (studio && studio.parent) studio.parent.remove(studio)
814
+ if (octree_helper && octree_helper.parent) octree_helper.parent.remove(octree_helper)
815
+ const gltf = await loader.loadAsync(cfg.url)
816
+ await load_specsheet(cfg.url)
817
+ studio = gltf.scene
818
+ if (cfg.height) rescale(studio, { height: cfg.height })
819
+ apply_studio_mesh(studio)
820
+ const pos = specsheet && specsheet.position
821
+ if (pos) studio.position.set(pos.x || 0, pos.y || 0, pos.z || 0)
822
+ scene.add(studio)
823
+ worldOctree = new Octree()
824
+ worldOctree.fromGraphNode(studio)
825
+ octree_helper = new OctreeHelper(worldOctree)
826
+ octree_helper.visible = false
827
+ scene.add(octree_helper)
828
+ place_doors_from_specsheet()
829
+ }
830
+
831
+ async function select_studio(name) {
832
+ if (!scenes[name] || studio_loading) return
833
+ studio_loading = true
834
+ localStorage.setItem(SCENE_STOR, name)
835
+ scene_url = scenes[name]
836
+ $('#status').text('Loading...')
837
+ $('#loading').text('Loading...')
838
+ try {
839
+ await load_studio(scene_url)
840
+ reset_player()
841
+ $('#status').text('loaded')
842
+ $('#loading').text('loaded')
843
+ } catch (error) {
844
+ console.log(error)
845
+ $('#status').text(error.toString())
846
+ $('#loading').text(error.toString())
847
+ }
848
+ studio_loading = false
849
+ }
850
+
851
+ $('select.studio').on('change', function() {
852
+ select_studio($(this).val())
853
+ })
854
+ const initial_studio = $('select.studio').val() || 'collision_world'
855
+ if (!$('select.studio').val()) $('select.studio').val(initial_studio)
856
+ select_studio(initial_studio)
857
+
858
+ function teleportPlayerIfOob() {
859
+
860
+ if ( camera.position.y <= - 25 ) {
861
+
862
+ playerCollider.start.set( 0, 0.35, 0 );
863
+ playerCollider.end.set( 0, 1, 0 );
864
+ playerCollider.radius = 0.35;
865
+ camera.position.copy( playerCollider.end );
866
+ camera.rotation.set( 0, 0, 0 );
867
+ if ( touch_controls ) {
868
+ touch_controls.setPosition( 0, 1, 0 )
869
+ touch_controls.setRotation( 0, 0 )
870
+ }
871
+
872
+ }
873
+
874
+ }
875
+
876
+ function touch_pad_controls( deltaTime ) {
877
+
878
+ const speedDelta = deltaTime * ( playerOnFloor ? walk_speed : walk_speed * AIR_SPEED_FACTOR )
879
+ const fwd = touch_controls.stick_y
880
+ let side = touch_controls.stick_x
881
+ if ( Math.abs( side ) < strafe_deadzone ) side = 0
882
+ side *= strafe_factor
883
+
884
+ if ( fwd ) {
885
+ playerVelocity.add( getForwardVector().multiplyScalar( speedDelta * fwd ) )
886
+ }
887
+
888
+ if ( side ) {
889
+ playerVelocity.add( getSideVector().multiplyScalar( speedDelta * side ) )
890
+ }
891
+
892
+ }
893
+
894
+
895
+ function animate() {
896
+
897
+ timer.update();
898
+
899
+ const deltaTime = Math.min( 0.05, timer.getDelta() ) / STEPS_PER_FRAME;
900
+
901
+ // we look for collisions in substeps to mitigate the risk of
902
+ // an object traversing another too quickly for detection.
903
+
904
+ for ( let i = 0; i < STEPS_PER_FRAME; i ++ ) {
905
+
906
+ if ( current_ctrl_type() === 'touch' && touch_controls ) {
907
+ touch_pad_controls( deltaTime )
908
+ } else {
909
+ controls( deltaTime )
910
+ }
911
+
912
+ updatePlayer( deltaTime )
913
+ check_door_portal()
914
+ teleportPlayerIfOob()
915
+ updateSpheres( deltaTime )
916
+
917
+ }
918
+
919
+ if ( current_ctrl_type() === 'touch' && touch_controls ) {
920
+ touch_controls.setPosition( camera.position.x, camera.position.y, camera.position.z )
921
+ apply_touch_pose()
922
+ }
923
+
924
+ update_door_aim()
925
+ renderer.render( scene, camera );
926
+
927
+ }
928
+
929
+ $('#fullScreen').on('click', () => {
930
+ document.body.requestFullscreen()
931
+ })
932
+
933
+ const CTRL_TYPE_STOR = 'ctrl-type'
934
+
935
+ function current_ctrl_type() {
936
+ return $('input[name=ctrl-type]:checked').val() || 'fps'
937
+ }
938
+
939
+ function apply_touch_pose() {
940
+ const holder = touch_controls.fpsBody.getObjectByName( 'cameraHolder' )
941
+ camera.rotation.set( holder.rotation.x, touch_controls.fpsBody.rotation.y, 0 )
942
+ }
943
+
944
+ function enable_touch_controls() {
945
+ if ( document.pointerLockElement ) document.exitPointerLock()
946
+
947
+ const pos = new THREE.Vector3()
948
+ camera.getWorldPosition( pos )
949
+ if ( pos.y < 0.2 ) pos.copy( playerCollider.end )
950
+ const rx = camera.rotation.x
951
+ const ry = camera.rotation.y
952
+
953
+ if ( !touch_controls ) {
954
+ camera.position.set( 0, 0, 0 )
955
+ camera.rotation.set( 0, 0, 0 )
956
+ touch_controls = new TouchControls( $( container ), camera, {
957
+ speedFactor: 0.08,
958
+ delta: 1,
959
+ rotationFactor: 0.002,
960
+ maxPitch: 55,
961
+ hitTest: false,
962
+ hitTestDistance: 1
963
+ } )
964
+ if ( camera.parent ) camera.parent.remove( camera )
965
+ touch_controls.addToScene( scene )
966
+ }
967
+
968
+ touch_controls.enabled = true
969
+ touch_controls.setPosition( pos.x, pos.y, pos.z )
970
+ touch_controls.setRotation( rx, ry )
971
+ camera.position.copy( pos )
972
+ playerCollider.end.copy( pos )
973
+ playerCollider.start.set( pos.x, pos.y - 0.65, pos.z )
974
+ apply_touch_pose()
975
+ $( '.movement-pad' ).show()
976
+ $( '.rotation-pad' ).removeClass('is-active').css('display', '')
977
+ }
978
+
979
+ function disable_touch_controls() {
980
+ if ( !touch_controls ) return
981
+ touch_controls.enabled = false
982
+ $( '.movement-pad' ).hide()
983
+ $( '.rotation-pad' ).removeClass('is-active').css('display', '')
984
+ playerCollider.end.copy( camera.position )
985
+ playerCollider.start.set( camera.position.x, camera.position.y - 0.65, camera.position.z )
986
+ }
987
+
988
+ function set_ctrl_type( type ) {
989
+ if ( type === 'touch' ) enable_touch_controls()
990
+ else disable_touch_controls()
991
+ }
992
+
993
+ try {
994
+ const saved = localStorage.getItem(CTRL_TYPE_STOR)
995
+ if (saved) $(`input[name=ctrl-type][value="${saved}"]`).prop('checked', true)
996
+ } catch (error) {
997
+ console.log(error)
998
+ }
999
+ $('input[name=ctrl-type]').on('change', function() {
1000
+ if (!this.checked) return
1001
+ localStorage.setItem(CTRL_TYPE_STOR, this.value)
1002
+ set_ctrl_type(this.value)
1003
+ })
1004
+ set_ctrl_type(current_ctrl_type())
1005
+
1006
+ const WEAPON_CTRL_STOR = 'weaponCtrl'
1007
+ try {
1008
+ const saved = localStorage.getItem(WEAPON_CTRL_STOR)
1009
+ if (saved !== null) $('input[name=weaponCtrl]').prop('checked', saved === 'on')
1010
+ } catch (error) {
1011
+ console.log(error)
1012
+ }
1013
+ $('input[name=weaponCtrl]').on('change', function() {
1014
+ localStorage.setItem(WEAPON_CTRL_STOR, this.checked ? 'on' : 'off')
1015
+ set_weapons(this.checked)
1016
+ })
1017
+ set_weapons(weapons_on())
1018
+ set_weapons(weapons_on())