-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlabyrinth.js
3000 lines (2701 loc) · 116 KB
/
labyrinth.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//------------------Labyrinth---------------------------------------------------------------
// This is a simple example of a multi-player 3D shooter.
// Actually, it isn't just a shooter. It is a strategy game.
// Think of it as "Go" with guns.
//
// Labyrinth has elements from a number of different games.
// It is loosely based upon the early Maze War game created at NASA Ames in 1973.
// https://en.wikipedia.org/wiki/Maze_War
// It also has elements of Go, Pacman,
// The Colony - https://en.wikipedia.org/wiki/The_Colony_(video_game)
// and Dodgeball.
//
//------------------------------------------------------------------------------------------
// Changes:
// Minimal world - showing we exist. We get an alert when a new user joins.
// Add simple avatars w/ mouselook interface
// Add missiles and collision detection
// Fix textures, add powerup with fake glow, add wobble shader
// Better missiles. Maze walls are instanced. Generate the maze.
// Collision detection with walls for avatars and missiles.
// Columns, CSM lighting
// Added floor reflections, enhance lighting, fixed disappearing columns
// Added the horse weenie
// Changed sky, added uv coordinates to hexasphere
// Avatar & missile tests collision with columns
// Seasonal trees weenies
// Made the missiles glow, slowed it down
// Place player at random location when spawned
// Sound effects:
// Missile bounce sound
// - missile fire sound
// - ready to shoot sound and click when not ready
// - player enter/exit game
// - missile whoosh when it goes by
// Cell collected tone
// Fixed sounds not playing after a while
// Fixed getting stuck under the horse
// Restructured loaded instance management
// Added ivy to some walls
// Added color to instances
// Break the floor model into a grid of floor tiles
// You can only claim cells from one of your own cells
// You can't claim a corner - they are fixed to the season color.
// Preloaded assets (sounds, textures, models). This is done before the main
// render loop starts. It is still taking too long to complete loading.
// Display the claimed cells on a 2D minimap.
// Track your avatar's location on the minimap.
// Missiles are color coded by the player's season color.
// Rotated the minimap so that my season color is at the bottom.
// Made the corner cells darker to make them more obvious.
// The players spawn and respawn in their own corners.
// Resize the minimap when the window is resized.
// Players cannot be harmed while on those four tiles, but they can shoot back. Projectiles bounce.
// You can only shoot if you are on your own season color.
// You move 1.5 times faster when you are on your own color.
// Missile/missile collision works.
// Added credit info to be added to credits screen.
// Added effects when you capture a cell.
// Reusable fireball - hide it when not in use.
// If a user slices off a section of cells so that it is no longer connected to
// your tree, those cells revert to their original, null color. Use flood fill:
// https://www.geeksforgeeks.org/flood-fill-algorithm-implement-fill-paint/
// When you lose territory, players can actually see and hear it go away.
// Hook up the clock - start with 15 minutes.
// Added a sorted scoring display.
// Moved the timer and the score box to make room for mobile controls.
// Shrank the avatar radius to 3.7 so that they can't block a corridor.
// Fixed the fake glow material complaints - at least in local.
// Fixed the scoreboard so that it doesn't get larger.
// Added a version display to the bottom of the screen.
// Boxscore now resizes and doesn't get larger.
// Determine if we are on a mobile device.
// Restructured the code so that web objects are in separate files.
// If you fire when you first start, you get an error. Fixed.
// Joystick controls.
// Fix mobile testing.
// Broke up the big file into smaller files.
// Compass is in the minimap (removed).
// Added a full screen button.
// Warmed up floor shader.
// Use '/' to turn sound on and off.
// Snap the floor shader to the floor.
// Destroy the glow when the avatar is destroyed.
// Play the shock sound when another player captures a cell.
// Shock and awe are working.
// Volume is working.
// Added text display for volume and sound and other things as required.
// Compass is now centered on the white square (removed).
// The iris of the eyes matches the season color.
// Removed the compass - minimap now rotates.
// Season icons are now displayed in the center of the screen.
// View the rules screen.
// Fixed respawn to update lastX and lastY. Could not shoot until you moved.
// Added color blindness mode for minimap.
// We don't go off the map anymore, but we can tunnel through walls or jump 2 cells.
// Missiles are warmed up.
// Fixed the floor glow objects that became visible when a reload occurs.
// Tuned mobile controls. May need more work based on testing.
// Sounds are now warmed up.
// Added season color to avatar.
// Only generate the number of instances that are actually needed.
// Turned the WallActor into an InstanceActor.
// Safari pointer lock support.
// Scrollable window displays an arrow to show that it is scrollable.
// Mobile controls - copy Call of Duty mobile.
// - right is look around
// - left is move (strafe and forward)
// - tap is shoot
// - rules window close button is on top of the full screen button - move it to the left side
// - rules window close button gets scaled in the y direction when the window is resized
// - rules window is too narrow when the window is rotated
// - score board needs to be larger
// Switch to emoji for season display
// Switch to emoji for victory display
// Winning display displays the season and name.
// Fixed the rules window so the arrow at the bottom is visible in various orientations and ratios on all devices.
// Added a win sound
// Added text display for recharged.
// Reset the world to start a new game.
// Emoji display text has stronger shadow.
// Added a "Start Game" button to restart the game.
// Resized textures so that it works on iPhone. So far so good.
// Use left/right arrows to turn for people who don't have a mouse.
// Resized the ivy model. Seems to be working on iOS now.
// Place new users in free spots. Note that this only occurs if the user gets around the lobby and joins the game directly.
// Ignore users who do not join the game from the lobby and there is no room.
// Fixed the goes to sleep and restart problem.
// Added the lobby.
// Fixed lobby layout. Added stats to the banner.
// Added "photo mode" to remove all of the overlay elements for screenshots and videos.
// Added background image (from photo mode) to the lobby.
// Fixed the maze generation so that it doesn't have dead ends around corners.
// Removed the back arrow to return to the lobby.
// Added the 30 second countdown sound.
// Use the color blind colors for the cells.
// Display the winner's season or "It is a tie!"
// Removed the missile glow and upped the point light intensity.
// Moved the horse to the center of the maze.
// Changed the horse and trees to static models. Removed plants and horse actors/pawns.
// Added sculptures for location awareness.
// Sound off stays off.
// We don't render until the user is assigned a season.
// Hide the rules window when the player joins.
// Mobile shooting works better.
// Flipped the map so the maze is the same for everyone.
// Made the maze visible in the minimap.
// Prevent objects from being selected.
// Added a big compass to the minimap - probably too big and will switch to mini on avatar location.
// Avatar in minimap is now a circle.
// Fixed the line rendering on the minimap so it is consistent.
// Click the minimap to save it as an image.
// Reworked loading of eyeballs. Need to force a render of each though.
// Shrink the sky texture to 2048x1024.
// iPad PRO must be a considered to be a mobile device. Removed pointer lock. Use keyboard.
// Added strafe with Q/E.
// Added the Christmas tree.
// Added the back to lobby button - this is needed for the Telegram deployment.
// Set countdown to red at start in case the game is already completed.
// Added Telegram API.
// Added the Telegram full screen.
// Added Telegram user name.
// Increase the throttle to 1.5 and 3.0.
// Warm up the 3D models when a player joins.
// Your missile no longer kills you.
//------------------------------------------------------------------------------------------
// Bugs:
// We don't go off the map anymore, but we can tunnel through walls or jump 2 cells.
//------------------------------------------------------------------------------------------
// Priority To do:
// Create and deliver the NFT.
// Maze with no walls?
// Need a menu for mobile:
// - switch controls left/right
// - color blindness mode
// - sound on/off
// Add the coins.
//------------------------------------------------------------------------------------------
// Consider:
// Claiming another player's cell should take longer than claiming a free cell.
// Rooms (Brian Upton suggestion)?
// Chat -broadcast messages to all players, colors are their team color. This is difficult, as we
// are in mouse look mode. Perhaps press "c" to type a message, hit enter and then you are back.
// Music is streamed to the game from the web. Players can turn it on and off - or play along
// and vote for the songs they like.
// Ask the AI to take the source code for labyrinth and document the entire thing so that it could be nicely formatted as a book.
//------------------------------------------------------------------------------------------
// Need artist:
// The ivy needs to be cleaned up at the top.
//------------------------------------------------------------------------------------------
// Education:
// - Anything that can stay in the view, keep in the view. If no one else needs to
// see it or know it, don't share it.
// - Sending messages from the view to the model is expensive. Try to avoid it.
// - Sending messages from the model to view is very cheap. Send as much as you want.
// - The purpose of the model is to provide shared computations. This is particularly
// true for simulations.
//------------------------------------------------------------------------------------------
import { App, StartWorldcore, Constants, ViewService, ModelRoot, ViewRoot,Actor, mix, toRad,
InputManager, AM_Spatial, PM_Spatial, PM_Smoothed, Pawn, AM_Avatar, PM_Avatar, UserManager, User,
q_yaw, q_euler, q_axisAngle, v3_add, v3_sub, v3_normalize, v3_rotate, v3_scale, v3_distanceSqr,
THREE, ADDONS, PM_ThreeVisible, ThreeRenderManager, PM_ThreeCamera, PM_ThreeInstanced, ThreeInstanceManager
} from '@croquet/worldcore';
import FullscreenButton from './src/Fullscreen.js';
import FakeGlowMaterial from './src/FakeGlowMaterial.js';
import DeviceDetector from './src/DeviceDetector.js';
import BoxScore from './src/BoxScore.js';
// import Joystick from './src/Joystick.js';
import Countdown from './src/Countdown.js';
import MazeActor from './src/MazeActor.js';
import {InstanceActor, instances, materials, geometries} from './src/Instance.js';
import { showRules, isRulesVisible } from './src/rules.js';
import EmojiDisplay from './src/EmojiDisplay.js';
import GameButton from './src/GameButton.js';
// import getLocation from './src/geolocation.js';
import apiKey from "./src/apiKey.js";
// Determine if we are mobile or desktop
const device = new DeviceDetector();
// Textures
//------------------------------------------------------------------------------------------
/*let sky;
async function loadAsset() {
if (deviceisIOS) {
sky = await import("./assets/textures/aboveClouds.jpg");
} else {
sky = await import("./assets/textures/aboveClouds.jpg");
}
}*/
import sky from "./assets/textures/aboveClouds1024.jpg";
// import eyeball_summer from "./assets/textures/EyeSummer.png";
import eyeball_autumn from "./assets/textures/EyeAutumn_05k.png";
import eyeball_winter from "./assets/textures/EyeWinter_05k.png";
import eyeball_spring from "./assets/textures/EyeSpring_05k.png";
/*
import missile_color from "./assets/textures/metal_gold_vein/metal_0080_color_1k.jpg";
import missile_normal from "./assets/textures/metal_gold_vein/metal_0080_normal_opengl_1k.png";
import missile_roughness from "./assets/textures/metal_gold_vein/metal_0080_roughness_1k.jpg";
import missile_displacement from "./assets/textures/metal_gold_vein/metal_0080_height_1k.png";
import missile_metalness from "./assets/textures/metal_gold_vein/metal_0080_metallic_1k.jpg";
import marble_color from "./assets/textures/marble_checker/marble_0013_color_1k.jpg";
import marble_normal from "./assets/textures/marble_checker/marble_0013_normal_opengl_1k.png";
import marble_roughness from "./assets/textures/marble_checker/marble_0013_roughness_1k.jpg";
//import marble_displacement from "./assets/textures/marble_checker/marble_0013_height_1k.png";
import corinthian_color from "./assets/textures/corinthian/concrete_0014_color_1k.jpg";
import corinthian_normal from "./assets/textures/corinthian/concrete_0014_normal_opengl_1k.png";
import corinthian_roughness from "./assets/textures/corinthian/concrete_0014_roughness_1k.jpg";
import corinthian_displacement from "./assets/textures/corinthian/concrete_0014_height_1k.png";
*/
import missile_color from "./assets/textures/metal_gold_vein/metal_0080_color_05k.jpg";
import missile_normal from "./assets/textures/metal_gold_vein/metal_0080_normal_opengl_05k.png";
//import missile_roughness from "./assets/textures/metal_gold_vein/metal_0080_roughness_05k.jpg";
//import missile_displacement from "./assets/textures/metal_gold_vein/metal_0080_height_05k.png";
//import missile_metalness from "./assets/textures/metal_gold_vein/metal_0080_metallic_05k.jpg";
import marble_color from "./assets/textures/marble_checker/marble_0013_color_05k.jpg";
// import marble_normal from "./assets/textures/marble_checker/marble_0013_normal_opengl_05k.png";
import marble_roughness from "./assets/textures/marble_checker/marble_0013_roughness_05k.jpg";
// import marble_displacement from "./assets/textures/marble_checker/marble_0013_height_05k.png";
import corinthian_color from "./assets/textures/corinthian/concrete_0014_color_05k.jpg";
import corinthian_normal from "./assets/textures/corinthian/concrete_0014_normal_opengl_05k.png";
import corinthian_roughness from "./assets/textures/corinthian/concrete_0014_roughness_05k.jpg";
import corinthian_displacement from "./assets/textures/corinthian/concrete_0014_height_05k.png";
// 3D Models
//------------------------------------------------------------------------------------------
// https://free3d.com/3d-model/eyeball-3d-model-181166.html
import eyeball_glb from "./assets/eyeball.glb";
// https://free3d.com/3d-model/-doric-column--353773.html
import column_glb from "./assets/column2.glb";
// https://www.robscanlon.com/hexasphere/
import hexasphere_glb from "./assets/hexasphere.glb";
// https://sketchfab.com/tochechka
import fourSeasonsTree_glb from "./assets/fourSeasonsTree.glb";
// https://sketchfab.com/dangry
import ivy_glb from "./assets/ivy3.glb";
import tree_glb from "./assets/tree.glb";
import ornaments_glb from "./assets/ornaments.glb";
// https://optimesh.gumroad.com/l/SJpXC
// import statue1_glb from "./assets/Horse_Copper2.glb";
/*
import statue2_glb from "./assets/aspasia.glb";
import statue3_glb from "./assets/apollo.glb";
import statue4_glb from "./assets/engel.glb";
import statue5_glb from "./assets/johannes_benk.glb";
*/
// Shaders
//------------------------------------------------------------------------------------------
// https://www.clicktorelease.com/code/perlin/explosion.html
import fireballTexture from "./assets/textures/explosion.png";
import * as fireballFragmentShader from "./src/shaders/fireball.frag.js";
import * as fireballVertexShader from "./src/shaders/fireball.vert.js";
// Sounds
//------------------------------------------------------------------------------------------
import bounceSound from "./assets/sounds/bounce.wav";
import shootSound from "./assets/sounds/shot1.wav";
import shootFailSound from "./assets/sounds/ShootFail.wav";
import rechargedSound from "./assets/sounds/Recharge.wav";
import enterSound from "./assets/sounds/avatarEnter.wav";
import exitSound from "./assets/sounds/avatarLeave.wav";
import missileSound from "./assets/sounds/Warning.mp3";
import implosionSound from "./assets/sounds/Implosion.mp3";
//import cellSound from "./assets/sounds/Granted.wav";
import cellSound from "./assets/sounds/Heartbeat4.wav";
import shockSound from "./assets/sounds/Shock.wav";
import aweSound from "./assets/sounds/Awe.wav";
import winSound from "./assets/sounds/Win.wav";
import startGameSound from "./assets/sounds/StartGame.wav";
import clockSound from "./assets/sounds/Clock.wav";
export { clockSound };
// Global Variables
//------------------------------------------------------------------------------------------
const GAME_MINUTES = 15;
const PI_2 = Math.PI/2;
const PI_3 = Math.PI+PI_2;
const MISSILE_LIFE = 4000;
export const CELL_SIZE = 20;
const AVATAR_RADIUS = 3.7;
const AVATAR_HEIGHT = 7.5;
const AVATAR_SPEED = 1.5;
const MISSILE_RADIUS = 2;
const WALL_EPSILON = 0.01;
const MAZE_ROWS = 20;
const MAZE_COLUMNS = 20;
const MISSILE_SPEED = 1;
export let csm; // CSM is Cascaded Shadow Maps
let readyToLoad3D = false;
let readyToLoadTextures = false;
let readyToLoadSounds = false;
let readyToPlay = false;
export const seasons = {
Spring:{cell:{x:0,y:0}, emoji: "🌸", nextCell:{x:1,y:1}, angle:toRad(180+45), color:0xFFB6C1, color2:0xCC8A94, colorBlind:0xCC79A7, colorEye: 0xFFEEEE},
Summer: {cell: {x:0,y:CELL_SIZE-2}, emoji: "🌿", nextCell:{x:1,y:CELL_SIZE-3}, angle:toRad(270+45), color:0x90EE90, color2:0x65AA65, colorBlind:0x009E73, colorEye: 0xD0FFD0},
Autumn: {cell:{x:CELL_SIZE-2, y:CELL_SIZE-2}, emoji: "🍁", nextCell:{x:CELL_SIZE-3,y:CELL_SIZE-3}, angle:toRad(0+45), color:0xFFE5B4, color2:0xCCB38B, colorBlind:0xE69F00, colorEye: 0xFFE5B4},
Winter: {cell:{x:CELL_SIZE-2, y:0}, emoji: "❄️", nextCell:{x:CELL_SIZE-3,y:1}, angle:toRad(90+45), color:0xA5F2F3, color2:0x73BFBF, colorBlind:0x0072B2, colorEye: 0xE0E0FF},
none: {cell:{x:0,y:0}, emoji: "🤝", nextCell:{x:1,y:1}, angle:0, color:0xFFFFFF, color2:0xFFFFFF, colorBlind:0xFFFFFF, colorEye: 0xFFFFFF}
};
/*
getLocation().then(city => {
console.log("Nearest city:", city);
})*/
// display the rules window
showRules();
// the new game button
const gameButton = new GameButton();
// display the centered bottom text info display
function createTextDisplay() {
// Create container
const textDisplay = document.createElement('div');
textDisplay.className = 'text-display';
// Add to DOM
document.body.appendChild(textDisplay);
// Add CSS for fade effect with longer transition
const style = document.createElement('style');
style.textContent = `
.text-display {
position: fixed;
bottom: 100px; /* Fixed distance from bottom */
left: 50%;
transform: translateX(-50%);
font-family: Arial, sans-serif;
background: rgba(0, 0, 0, 0.5);
padding: 8px 16px;
border-radius: 4px;
z-index: 1000;
opacity: 1;
transition: opacity 2s ease-out;
pointer-events: none;
color: white;
}
.text-display.fade {
opacity: 0;
}
.text-display.hidden {
display: none;
}
`;
document.head.appendChild(style);
let currentTimeout;
let fadeTimeout;
// Return update function
return (text, duration = 0) => {
// Clear any existing timeouts
if (currentTimeout) clearTimeout(currentTimeout);
if (fadeTimeout) clearTimeout(fadeTimeout);
// Remove classes and show element
textDisplay.classList.remove('fade', 'hidden');
// Force a reflow
textDisplay.offsetHeight;
// Update text
textDisplay.textContent = text;
// Set up fade if duration provided
if (duration > 0) {
currentTimeout = setTimeout(() => {
textDisplay.classList.add('fade');
// Set up the hide after fade completes
fadeTimeout = setTimeout(() => {
textDisplay.classList.add('hidden');
}, 2000); // Match the transition duration
}, duration * 1000);
}
};
}
const setTextDisplay = createTextDisplay();
setTextDisplay("Device: "+ (device.isMobile? (device.isIOS?"iOS":"Android"):"desktop"),10);
// Initialize fullscreen button
new FullscreenButton();
const boxScore = new BoxScore();
// Minimap canvas
const minimapDiv = document.getElementById('minimap');
const minimapCanvas = document.createElement('canvas');
// Add handlers to both elements
minimapDiv.addEventListener('pointerdown', (event) => {
console.log("Minimap DIV clicked!"); // Debug log
event.stopPropagation();
event.preventDefault();
saveMinimap();
});
minimapCanvas.addEventListener('pointerdown', (event) => {
console.log("Minimap CANVAS clicked!"); // Debug log
event.stopPropagation();
event.preventDefault();
saveMinimap();
});
// Make both elements clickable
minimapDiv.style.cursor = 'pointer';
minimapCanvas.style.cursor = 'pointer';
const minimapCtx = minimapCanvas.getContext('2d');
minimapCtx.globalAlpha = 1;
minimapCanvas.width = 220;
minimapCanvas.height = 220;
function scaleMinimap() {
//const minimapDiv = document.getElementById('minimap');
const height = Math.min(window.innerHeight, window.innerWidth);
// Calculate size where diagonal is 1/3 of page height
// For a square, diagonal = side * √2
// So, side = diagonal / √2
const diagonal = height / 2;
const sideLength = diagonal / Math.sqrt(2);
// Set the size
minimapDiv.style.width = `${sideLength}px`;
minimapDiv.style.height = `${sideLength}px`;
minimapCanvas.style.width = `${sideLength}px`;
minimapCanvas.style.height = `${sideLength}px`;
// Scale compass length to match minimap size
const compass = document.getElementById('compass');
if (compass) {
compass.style.width = `${diagonal/3}px`; // Quarter of the minimap width (half of half)
}
}
scaleMinimap();
function saveMinimap() {
// Save the current canvas state
console.log("saveMinimap called"); // Debug log
const dataURL = minimapCanvas.toDataURL('image/png');
// Create and trigger download
const link = document.createElement('a');
link.download = 'labyrinth-minimap.png';
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Optional: Show feedback that save occurred
setTextDisplay("Minimap saved!", 2); // Uses your existing text display system
}
let overlaysHidden = false;
let hiddenElements = new Map(); // Store original display values
function toggleOverlays() {
overlaysHidden = !overlaysHidden;
// List of element IDs and classes to toggle
const elements = [
'#minimap',
'.box-score',
'#countdown',
'#version-number',
'.text-display',
'.help-button',
'#codelink',
'.victory-display',
'.victory-icon',
'.victory-text',
'#built-with'
];
// Handle standard elements
elements.forEach(selector => {
const element = document.querySelector(selector);
if (element) {
if (overlaysHidden) {
if (window.getComputedStyle(element).display !== 'none') {
hiddenElements.set(selector, element.style.display || 'block');
element.style.display = 'none';
}
} else {
if (hiddenElements.has(selector)) {
element.style.display = hiddenElements.get(selector);
hiddenElements.delete(selector);
}
}
}
});
// Handle fullscreen button separately to preserve flex display
const fullscreenButton = document.querySelector('.fullscreen-button');
if (fullscreenButton) {
if (overlaysHidden) {
if (window.getComputedStyle(fullscreenButton).display !== 'none') {
hiddenElements.set('fullscreen', 'flex'); // Always store as flex
fullscreenButton.style.display = 'none';
}
} else {
if (hiddenElements.has('fullscreen')) {
fullscreenButton.style.display = 'flex'; // Always restore as flex
hiddenElements.delete('fullscreen');
}
}
}
// Handle emoji element from EmojiDisplay
const emojiElement = document.querySelector('div[style*="position: fixed"][style*="z-index: 1000"]');
if (emojiElement) {
if (overlaysHidden) {
if (window.getComputedStyle(emojiElement).display !== 'none') {
hiddenElements.set('emoji', emojiElement.style.display || 'flex');
emojiElement.style.display = 'none';
}
} else {
if (hiddenElements.has('emoji')) {
emojiElement.style.display = hiddenElements.get('emoji');
hiddenElements.delete('emoji');
}
}
}
}
console.log("Telegram Web App API WebApp:", window.Telegram.WebApp);
console.log("Telegram Web App API initDataUnsafe:", window.Telegram.WebApp.initDataUnsafe);
console.log("Telegram Web App API user:", window.Telegram.WebApp.initDataUnsafe.user);
let telegramUser = window.Telegram.WebApp.initDataUnsafe.user; // this is undefined if not on telegram
// window.Telegram.WebApp.requestFullscreen();
// Sound Manager
//------------------------------------------------------------------------------------------
export let soundSwitch = false; // turn sound on and off
let soundOn = true; // toggle sound on and off
let volume = 1;
const maxSound = 16;
const listener = new THREE.AudioListener();
const soundList = {};
const soundLoops = [];
const loopSoundVolume = 0.25;
function warmupAudio() {
// Create and play a silent buffer
const audioContext = listener.context;
const silentBuffer = audioContext.createBuffer(1, 1, 22050);
const source = audioContext.createBufferSource();
source.buffer = silentBuffer;
source.connect(audioContext.destination);
source.start();
source.stop();
// Resume audio context if it's suspended
if (audioContext.state === 'suspended') {
audioContext.resume();
}
}
document.addEventListener('click', () => {
warmupAudio();
}, { once: true });
export const playSound = function() {
function play(soundURL, parent3D, force, loop = false) {
if (!soundSwitch) return;
// Check if we're on mobile and the audio context is suspended
const audioContext = THREE.AudioContext.getContext();
if (device.isMobile && audioContext.state === 'suspended') {
audioContext.resume().then(() => {
return playSoundOnce(soundList[soundURL], parent3D, force, loop);
});
} else if (soundList[soundURL]) {
return playSoundOnce(soundList[soundURL], parent3D, force, loop);
}
}
return play;
}();
function playSoundOnce(sound, parent3D, force, loop = false) {
// console.log("playSoundOnce", sound.count, maxSound, parent3D);
if (!force && sound.count>maxSound) return null;
sound.count++;
let mySound;
if (parent3D) {
mySound = new THREE.PositionalAudio( listener ); // listener is a global
//mySound = new MyAudio( listener ); // listener is a global
mySound.setRefDistance( 8 );
mySound.setVolume( volume );
}
else {
mySound = new THREE.Audio( listener );
mySound.setVolume( volume * loopSoundVolume );
soundLoops.push(mySound);
}
mySound.setBuffer( sound.buffer );
mySound.setLoop(loop);
if (parent3D) {
parent3D.add(mySound);
parent3D.mySound = mySound;
mySound.onEnded = ()=> { sound.count--; mySound.removeFromParent(); };
} else mySound.onEnded = ()=> { sound.count--; };
// don't play if sound is muted
if ( soundSwitch )mySound.play();
return mySound;
}
async function loadSounds() {
const audioLoader = new THREE.AudioLoader();
// Add mobile audio unlock
if (device.isMobile) {
const unlockAudio = () => {
// Create and play a silent audio context
const audioContext = THREE.AudioContext.getContext();
if (audioContext.state === 'suspended') {
audioContext.resume();
}
// Create and play a silent audio element
const silentSound = new Audio();
silentSound.play().catch(() => {});
// Remove the event listeners once unlocked
['touchstart', 'touchend', 'click'].forEach(event => {
document.removeEventListener(event, unlockAudio);
});
};
// Add event listeners for user interaction
['touchstart', 'touchend', 'click'].forEach(event => {
document.addEventListener(event, unlockAudio);
});
}
return Promise.all([
audioLoader.loadAsync(bounceSound),
audioLoader.loadAsync(shootSound),
audioLoader.loadAsync(shootFailSound),
audioLoader.loadAsync(rechargedSound),
audioLoader.loadAsync(enterSound),
audioLoader.loadAsync(exitSound),
audioLoader.loadAsync(missileSound),
audioLoader.loadAsync(implosionSound),
audioLoader.loadAsync(cellSound),
audioLoader.loadAsync(shockSound),
audioLoader.loadAsync(aweSound),
audioLoader.loadAsync(winSound),
audioLoader.loadAsync(startGameSound),
audioLoader.loadAsync(clockSound),
]);
}
loadSounds().then( sounds => {
readyToLoadSounds = true;
console.log("sounds loaded-------------------");
soundList[bounceSound] = {buffer:sounds[0], count:0};
soundList[shootSound] = {buffer:sounds[1], count:0};
soundList[shootFailSound] = {buffer:sounds[2], count:0};
soundList[rechargedSound] = {buffer:sounds[3], count:0};
soundList[enterSound] = {buffer:sounds[4], count:0};
soundList[exitSound] = {buffer:sounds[5], count:0};
soundList[missileSound] = {buffer:sounds[6], count:0};
soundList[implosionSound] = {buffer:sounds[7], count:0};
soundList[cellSound] = {buffer:sounds[8], count:0};
soundList[shockSound] = {buffer:sounds[9], count:0};
soundList[aweSound] = {buffer:sounds[10], count:0};
soundList[winSound] = {buffer:sounds[11], count:0};
soundList[startGameSound] = {buffer:sounds[12], count:0};
soundList[clockSound] = {buffer:sounds[13], count:0};
});
// Load 3D Models
//------------------------------------------------------------------------------------------
// 3D Models
let eyeball, column, hexasphere, trees, ivy, statue1, statue2; //, statue2, statue3, statue4, statue5;
const staticModels ={};
function deepClone(original) {
let clone;
if (original.isMesh)
clone = new THREE.Mesh(original.geometry.clone(),original.material.clone());
else clone = new THREE.Group();
original.children.forEach((child) => clone.add(deepClone(child)));
// Copy transform
clone.position.copy(original.position);
clone.rotation.copy(original.rotation);
clone.scale.copy(original.scale);
return clone;
}
async function modelConstruct() {
const gltfLoader = new ADDONS.GLTFLoader();
const dracoLoader = new ADDONS.DRACOLoader();
dracoLoader.setDecoderPath('draco/');
gltfLoader.setDRACOLoader(dracoLoader);
return [eyeball, column, ivy, hexasphere, trees, statue1, statue2 /*statue2, statue3, statue4, statue5*/] = await Promise.all( [
// add additional GLB files to load here
gltfLoader.loadAsync( eyeball_glb ),
gltfLoader.loadAsync( column_glb ),
gltfLoader.loadAsync( ivy_glb ),
gltfLoader.loadAsync( hexasphere_glb ),
gltfLoader.loadAsync( fourSeasonsTree_glb ),
gltfLoader.loadAsync( tree_glb ),
gltfLoader.loadAsync( ornaments_glb ),
//gltfLoader.loadAsync( statue2_glb ),
//gltfLoader.loadAsync( statue3_glb ),
//gltfLoader.loadAsync( statue4_glb ),
//gltfLoader.loadAsync( statue5_glb ),
]);
}
modelConstruct().then( () => {
readyToLoad3D = true;
console.log("models loaded-------------------");
instances.column = column.scene.children[0];
instances.column.geometry.scale(0.028,0.028,0.028);
instances.column.geometry.rotateX(-PI_2);
instances.ivy0 = ivy.scene.children[0];
instances.ivy1 = ivy.scene.children[1];
instances.ivy0.geometry.scale(8,5,4);
instances.ivy1.geometry.scale(8,5,4);
instances.ivy0.geometry.translate(0,3.5,0.22);
instances.hexasphere = hexasphere.scene.children[0].children[0];
instances.hexasphere.geometry.scale(0.05,0.05,0.05);
fixUV(instances.hexasphere.geometry);
const width = 20;
const height = 10;
const frontWall = new THREE.PlaneGeometry(width, height);
const backWall = new THREE.PlaneGeometry(width, height);
backWall.rotateY(Math.PI);
geometries.wall = ADDONS.BufferGeometryUtils.mergeGeometries([frontWall, backWall], false);
function prepare(mesh, depthTest) {
mesh = mesh.scene;
mesh.traverse( m => {if (m.geometry) {
m.castShadow=true;
m.receiveShadow=true;
m.position.set(0,0,0);
//m.renderOrder = renderOrder;
if(depthTest) {
m.material.depthTest = depthTest;
m.material.depthWrite = depthTest;
}
m.material.needsUpdate = true;
} });
return mesh;
}
function setupEye3d(season){
if(!staticModels[season+"Avatar"]){
let color = seasons[season].colorEye;
let eye = deepClone(staticModels.BaseAvatar);
const material = eye.children[0].children[0].material;
material.color = new THREE.Color(color);
// console.log("material color", color, color.toString(16),material.color);
if(season === "Spring") material.map = eyeball_spring_t;
else if(season === "Autumn") material.map = eyeball_autumn_t;
else if(season === "Winter") material.map = eyeball_winter_t;
material.needsUpdate = true;
eye.traverse( m => {
if (m.geometry) {
m.castShadow=true;
m.receiveShadow=true;
}
});
eye.scale.set(40,40,40);
eye.rotation.set(0,Math.PI,0);
console.log("setupEye3d", season, eye);
staticModels[season+"Avatar"] = eye;
}
}
staticModels.statue1 = prepare(statue1);
staticModels.statue2 = prepare(statue2, true);
//staticModels.statue2 = prepare(statue2);
//staticModels.statue3 = prepare(statue3);
//staticModels.statue4 = prepare(statue4);
//staticModels.statue5 = prepare(statue5);
staticModels.Spring = new THREE.Group();
staticModels.Summer = new THREE.Group();
staticModels.Autumn = new THREE.Group();
staticModels.Winter = new THREE.Group();
staticModels.BaseAvatar = eyeball.scene;
setupEye3d("Spring");
setupEye3d("Summer");
setupEye3d("Autumn");
setupEye3d("Winter");
trees.scene.children.forEach(node => {
if (node.name) {
if (node.name.includes("spring")) staticModels.Spring.add(node.clone());
else if (node.name.includes("summer")) staticModels.Summer.add(node.clone());
else if (node.name.includes("fall")) staticModels.Autumn.add(node.clone());
else if (node.name.includes("winter")) staticModels.Winter.add(node.clone());
}
});
staticModels.Spring.traverse( m => {if (m.geometry) { m.castShadow=true; m.receiveShadow=true; m.position.set(0,0,0); } });
staticModels.Summer.traverse( m => {if (m.geometry) { m.castShadow=true; m.receiveShadow=true; m.position.set(0,0,0);} });
staticModels.Autumn.traverse( m => {if (m.geometry) { m.castShadow=true; m.receiveShadow=true; m.position.set(0,0,0);} });
staticModels.Winter.traverse( m => {if (m.geometry) { m.castShadow=true; m.receiveShadow=true; m.position.set(0,0,0);} });
});
function fixUV(geometry) {
// Angle around the Y axis, counter-clockwise when looking from above.
function azimuth( vector ) { return Math.atan2( vector.z, -vector.x ); }
// Angle above the XZ plane.
function inclination( vector ) {return Math.atan2( -vector.y, Math.sqrt( ( vector.x * vector.x ) + ( vector.z * vector.z ) ) ); }
const uvBuffer = [];
const vertex = new THREE.Vector3();
const positions = geometry.getAttribute('position').array;
// console.log("fixUV", positions);
for ( let i = 0; i < positions.length; i += 3 ) {
vertex.x = positions[ i + 0 ];
vertex.y = positions[ i + 1 ];
vertex.z = positions[ i + 2 ];
const u = azimuth( vertex ) / 2 / Math.PI + 0.5;
const v = inclination( vertex ) / Math.PI + 0.5;
uvBuffer.push( u, 1 - v );
}
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvBuffer, 2));
}
// Create fireball material
//------------------------------------------------------------------------------------------
let fireMaterial;
new THREE.TextureLoader().load(fireballTexture, texture => {
fireMaterial = new THREE.ShaderMaterial( {
uniforms: {
tExplosion: { value: texture },
time: { value: 0.0 },
tOpacity: { value: 1.0 }
},
vertexShader: fireballVertexShader.vertexShader(),
fragmentShader: fireballFragmentShader.fragmentShader(),
side: THREE.DoubleSide
} );
});
let sky_t, missile_color_t, missile_normal_t, missile_roughness_t, //missile_displacement_t, missile_metalness_t,
// power_color_t, power_normal_t, power_roughness_t, power_displacement_t, power_metalness_t,
marble_color_t, marble_roughness_t, // marble_normal_t, marble_displacement_t,
corinthian_color_t, corinthian_normal_t, corinthian_roughness_t, corinthian_displacement_t, eyeball_spring_t, eyeball_autumn_t, eyeball_winter_t;
async function textureConstruct() {
["hexasphere", "wall", "floor"].forEach( name => {
const material = new THREE.MeshStandardMaterial();
materials[name] = material;
});
const textureLoader = new THREE.TextureLoader();
textureLoader.crossOrigin = 'anonymous';
return [sky_t, missile_color_t, missile_normal_t, //missile_roughness_t, // missile_displacement_t, missile_metalness_t,
// power_color_t, power_normal_t, power_roughness_t, power_displacement_t, power_metalness_t,
marble_color_t, marble_roughness_t, //marble_normal_t, marble_displacement_t,
corinthian_color_t, corinthian_normal_t, corinthian_roughness_t, corinthian_displacement_t,
eyeball_spring_t, eyeball_autumn_t, eyeball_winter_t
] = await Promise.all( [
textureLoader.loadAsync(sky),
textureLoader.loadAsync(missile_color),
textureLoader.loadAsync(missile_normal),
//textureLoader.loadAsync(missile_roughness),
//textureLoader.loadAsync(missile_displacement),
//textureLoader.loadAsync(missile_metalness),
// textureLoader.loadAsync(power_color),
// textureLoader.loadAsync(power_normal),
// textureLoader.loadAsync(power_roughness),
// textureLoader.loadAsync(power_displacement),
// textureLoader.loadAsync(power_metalness),
textureLoader.loadAsync(marble_color),
//textureLoader.loadAsync(marble_normal),
textureLoader.loadAsync(marble_roughness),
// textureLoader.loadAsync(marble_displacement),
textureLoader.loadAsync(corinthian_color),
textureLoader.loadAsync(corinthian_normal),
textureLoader.loadAsync(corinthian_roughness),
textureLoader.loadAsync(corinthian_displacement),
textureLoader.loadAsync(eyeball_spring),
textureLoader.loadAsync(eyeball_autumn),
textureLoader.loadAsync(eyeball_winter),
]);
}
textureConstruct().then( () => {
readyToLoadTextures = true;
console.log("textures loaded-------------------");
complexMaterial({
colorMap: missile_color_t,
normalMap: missile_normal_t,
//roughnessMap: missile_roughness_t,
//metalnessMap: missile_metalness_t,
//displacementMap: missile_displacement_t,
repeat: [1.5,1],
displacementScale: 0.1,
displacementBias: -0.05,
side: THREE.DoubleSide,
name: "hexasphere"
});
/*
complexMaterial({
colorMap: power_color_t,
normalMap: power_normal_t,
roughnessMap: power_roughness_t,
metalnessMap: power_metalness_t,
displacementMap: power_displacement_t,
repeat: [1.5,1],
displacementScale: 0.1,
displacementBias: -0.05,
name: "power"
});
*/
complexMaterial({
colorMap: corinthian_color_t,
normalMap: corinthian_normal_t,
roughnessMap: corinthian_roughness_t,
displacementMap: corinthian_displacement_t,
displacementScale: 1.5,
displacementBias: -0.4,
anisotropy: 4,
repeat: [2, 1],
name: "wall"
});
complexMaterial({
colorMap: marble_color_t,
//normalMap: marble_normal_t,
roughnessMap: marble_roughness_t,
// displacementMap: marble_displacement_t,
anisotropy: 4,
metalness: 0.1,
repeat: [1, 1],
transparent: true,
opacity: 0.8,
name: "floor"
});
});
// Create complex materials
//------------------------------------------------------------------------------------------
function complexMaterial(options) {
const material = materials[options.name];
const repeat = options.repeat || [1,1];
let map = options.colorMap;