File size: 49,956 Bytes
60c08e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bf40a38
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
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Blocky Brawl: Dungeon Dimensions</title>
    <style>
        /* Basic styling for the body to remove default margins and overflows */
        body {
            margin: 0;
            overflow: hidden; /* Hide scrollbars */
            font-family: 'Inter', sans-serif; /* Use Inter font */
            background-color: #1a1a1a; /* Dark background for the page */
        }

        /* Container for the game canvas and UI elements */
        #game-container {
            position: relative;
            width: 100vw; /* Full viewport width */
            height: 100vh; /* Full viewport height */
            display: flex; /* Use flexbox for centering */
            justify-content: center; /* Center horizontally */
            align-items: center; /* Center vertically */
            overflow: hidden; /* Ensure no overflow */
        }

        /* Main UI container at the top center */
        #top-ui-container {
            position: absolute;
            top: 20px; /* Distance from the top */
            left: 50%;
            transform: translateX(-50%);
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 15px; /* Space between UI sections */
            z-index: 10;
            pointer-events: none; /* Allow interaction with elements inside */
        }

        /* Container for scores and health/mana bars */
        #player-stats-container {
            display: flex;
            justify-content: space-between;
            width: 100%; /* Will be set dynamically by JS to fit content */
            max-width: 800px; /* Max width for the stats section */
            gap: 50px; /* Space between player stat blocks */
            pointer-events: auto; /* Re-enable pointer events for UI elements */
        }

        /* Styling for the score displays */
        .score-display {
            color: white; /* White text color */
            font-size: 2.2em; /* Larger font size */
            font-weight: bold; /* Bold text */
            text-shadow: 3px 3px 6px rgba(0,0,0,0.8); /* Stronger text shadow for readability */
            padding: 10px 15px; /* Padding around text */
            background-color: rgba(0, 0, 0, 0.4); /* Semi-transparent background */
            border-radius: 12px; /* Rounded corners */
            box-shadow: 0 4px 10px rgba(0,0,0,0.5); /* Subtle box shadow */
            text-align: center;
            min-width: 150px; /* Ensure minimum width */
        }

        /* Styling for the health and mana bars container */
        .player-bars {
            height: 70px; /* Height of the bar container */
            width: 250px; /* Max width for bars */
            border: 3px solid #ffffff; /* White border */
            border-radius: 10px; /* Rounded corners */
            box-shadow: 0 4px 10px rgba(0,0,0,0.5); /* Subtle box shadow */
            display: flex;
            flex-direction: column;
            justify-content: space-around;
            padding: 5px;
            box-sizing: border-box;
            background-color: rgba(0, 0, 0, 0.4); /* Background for the bars container */
        }

        .bar-container {
            width: 100%;
            height: 25px; /* Height for individual bars */
            background-color: #4a5568; /* Grey background for empty bar */
            border-radius: 8px;
            overflow: hidden;
        }

        .health-bar, .mana-bar {
            height: 100%;
            width: 100%; /* Initial width, will be updated by JS */
            transition: width 0.3s ease-out, background-color 0.3s ease-out; /* Smooth transitions */
            border-radius: 8px;
        }

        .health-bar { background-color: #28a745; } /* Green for health */
        .mana-bar { background-color: #3b82f6; } /* Blue for mana */


        /* Styling for the reset button */
        #reset-button {
            position: absolute;
            bottom: 40px; /* Distance from the bottom */
            left: 50%; /* Center horizontally */
            transform: translateX(-50%); /* Adjust for true centering */
            padding: 18px 35px; /* Generous padding */
            font-size: 1.8em; /* Larger font size */
            background: linear-gradient(145deg, #ff6b6b, #ee4444); /* Gradient background */
            color: white; /* White text */
            border: none; /* No border */
            border-radius: 15px; /* More rounded corners */
            cursor: pointer; /* Pointer cursor on hover */
            box-shadow: 0 8px 20px rgba(0,0,0,0.6); /* Stronger shadow */
            transition: background 0.3s ease, transform 0.1s ease, box-shadow 0.3s ease; /* Smooth transitions */
            z-index: 10; /* Ensure it's above the canvas */
            font-weight: bold; /* Bold text */
            letter-spacing: 1px; /* Slight letter spacing */
            text-transform: uppercase; /* Uppercase text */
            pointer-events: auto; /* Allow interaction */
        }

        #reset-button:hover {
            background: linear-gradient(145deg, #ff4d4d, #cc3333); /* Darker gradient on hover */
            transform: translateX(-50%) scale(1.05); /* Slightly enlarge on hover */
            box-shadow: 0 10px 25px rgba(0,0,0,0.8); /* Deeper shadow on hover */
        }

        #reset-button:active {
            transform: translateX(-50%) scale(0.98); /* Shrink slightly on click */
            box-shadow: 0 4px 10px rgba(0,0,0,0.4); /* Recessed shadow on click */
        }

        /* Styling for the Three.js canvas */
        canvas {
            display: block; /* Remove extra space below canvas */
            width: 100%; /* Make canvas fill its container */
            height: 100%; /* Make canvas fill its container */
            border-radius: 15px; /* Rounded corners for the canvas itself */
            box-shadow: 0 0 25px rgba(0,0,0,0.7); /* Shadow around the canvas */
        }

        /* Controls display */
        #controls {
            background-color: rgba(0, 0, 0, 0.6);
            color: white;
            padding: 15px 25px;
            border-radius: 12px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.7);
            font-size: 1.1em;
            text-align: center;
            pointer-events: auto; /* Allow interaction */
            display: flex;
            gap: 40px; /* Space between player control sections */
        }

        #controls h3 {
            margin-top: 0;
            color: #ffd700; /* Gold color for headings */
            font-size: 1.3em;
            margin-bottom: 10px;
        }

        #controls ul {
            list-style: none;
            padding: 0;
            margin: 0;
            text-align: left;
        }

        #controls li {
            margin-bottom: 5px;
        }

        #controls span {
            font-weight: bold;
            color: #aaffaa; /* Light green for keys */
            display: inline-block;
            width: 40px; /* Fixed width for key display */
        }
    </style>
</head>
<body>
    <div id="game-container">
        <div id="top-ui-container">
            <div id="player-stats-container">
                <div>
                    <div id="score-left" class="score-display">P1 Score: 0</div>
                    <div id="player-left-bars" class="player-bars">
                        <div class="bar-container"><div id="health-bar-left" class="health-bar"></div></div>
                        <div class="bar-container"><div id="mana-bar-left" class="mana-bar"></div></div>
                    </div>
                </div>
                <div>
                    <div id="score-right" class="score-display">P2 Score: 0</div>
                    <div id="player-right-bars" class="player-bars">
                        <div class="bar-container"><div id="health-bar-right" class="health-bar"></div></div>
                        <div class="bar-container"><div id="mana-bar-right" class="mana-bar"></div></div>
                    </div>
                </div>
            </div>

            <div id="controls">
                <div>
                    <h3>Player 1 Controls (Red Wizard)</h3>
                    <ul>
                        <li><span>WASD:</span> Move</li>
                        <li><span>E:</span> Cast Spell</li>
                    </ul>
                </div>
                <div>
                    <h3>Player 2 Controls (Blue Wizard)</h3>
                    <ul>
                        <li><span>IJKL:</span> Move</li>
                        <li><span>U:</span> Cast Spell</li>
                    </ul>
                </div>
            </div>
        </div>

        <button id="reset-button">Primitive Reset!</button>
    </div>

    <script type="module">
        // Import Three.js library from a reliable CDN
        import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';

        // Declare global variables for Three.js scene, camera, renderer, and game state
        let scene, camera, renderer;
        let player1, player2; // Our primitive-assembled characters (Wizards)
        let monsters = []; // Array to hold monster objects
        let activeProjectiles = []; // Array to hold active spell projectiles

        // Game grid dimensions and unit size
        const gridSize = 10; // Size of one grid square in Three.js units
        const mapWidth = 20; // Number of grid squares wide
        const mapHeight = 20; // Number of grid squares high
        const dungeonMap = []; // 2D array to represent the dungeon layout (0: floor, 1: wall)

        // Player statistics
        const playerStats = {
            left: {
                health: 100,
                maxHealth: 100,
                mana: 50,
                maxMana: 50,
                score: 0,
                position: new THREE.Vector3(-5 * gridSize, 0, -5 * gridSize) // Initial world position
            },
            right: {
                health: 100,
                maxHealth: 100,
                mana: 50,
                maxMana: 50,
                score: 0,
                position: new THREE.Vector3(5 * gridSize, 0, 5 * gridSize) // Initial world position
            }
        };

        // Movement speed for players and monsters
        const playerMoveSpeed = 0.15 * gridSize; // Adjust for desired player speed relative to grid
        const monsterMoveSpeed = 0.05 * gridSize; // Adjust for desired monster speed relative to grid

        // References to UI HTML elements
        const uiElements = {
            scoreLeft: document.getElementById('score-left'),
            scoreRight: document.getElementById('score-right'),
            healthBarLeft: document.getElementById('health-bar-left'),
            manaBarLeft: document.getElementById('mana-bar-left'),
            healthBarRight: document.getElementById('health-bar-right'),
            manaBarRight: document.getElementById('mana-bar-right'),
            resetButton: document.getElementById('reset-button')
        };

        // Object to keep track of pressed keys for smooth movement
        const keys = {
            // Player 1 movement
            w: false, a: false, s: false, d: false,
            // Player 1 action
            p1_action: false,

            // Player 2 movement
            i: false, j: false, k: false, l: false,
            // Player 2 action
            p2_action: false
        };

        // Spell projectile parameters
        const spellProjectileSpeed = 0.8 * gridSize;
        const spellProjectileLife = 120; // frames (2 seconds at 60fps)
        const spellProjectileRadius = 0.03 * gridSize; // Adjusted for grid size
        const spellDamage = 20; // Damage dealt by a spell

        // Player-to-player healing parameters
        const playerHealingThreshold = gridSize * 2.5; // Players heal each other if within this distance
        const healingAmountPerFrame = 0.1; // Small continuous heal

        /**
         * Handles window resize events to keep the camera and renderer aspect ratio correct.
         */
        function onWindowResize() {
            const aspectRatio = window.innerWidth / window.innerHeight;
            const frustumSize = Math.max(mapWidth, mapHeight) * gridSize * 0.7;
            camera.left = frustumSize * aspectRatio / - 2;
            camera.right = frustumSize * aspectRatio / 2;
            camera.top = frustumSize / 2;
            camera.bottom = frustumSize / - 2;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }

        /**
         * Handles keydown events, setting the corresponding key in the `keys` object to true.
         * @param {KeyboardEvent} event - The keyboard event.
         */
        function onKeyDown(event) {
            switch (event.code) {
                // Player 1 Movement
                case 'KeyW': keys.w = true; break;
                case 'KeyA': keys.a = true; break;
                case 'KeyS': keys.s = true; break;
                case 'KeyD': keys.d = true; break;
                // Player 1 Action
                case 'KeyE': keys.p1_action = true; break;

                // Player 2 Movement
                case 'KeyI': keys.i = true; break;
                case 'KeyJ': keys.j = true; break;
                case 'KeyK': keys.k = true; break;
                case 'KeyL': keys.l = true; break;
                // Player 2 Action
                case 'KeyU': keys.p2_action = true; break;
            }
        }

        /**
         * Handles keyup events, setting the corresponding key in the `keys` object to false.
         * @param {KeyboardEvent} event - The keyboard event.
         */
        function onKeyUp(event) {
            switch (event.code) {
                case 'KeyW': keys.w = false; break;
                case 'KeyA': keys.a = false; break;
                case 'KeyS': keys.s = false; break;
                case 'KeyD': keys.d = false; break;

                case 'KeyE': keys.p1_action = false; break;

                case 'KeyI': keys.i = false; break;
                case 'KeyJ': keys.j = false; break;
                case 'KeyK': keys.k = false; break;
                case 'KeyL': keys.l = false; break;

                case 'KeyU': keys.p2_action = false; break;
            }
        }

        /**
         * Generates a simple dungeon layout with floor and wall tiles.
         * The dungeon is a large open room with a border of walls and some random inner walls.
         */
        function generateDungeon() {
            for (let y = 0; y < mapHeight; y++) {
                dungeonMap[y] = []; // Initialize inner array for each row
                for (let x = 0; x < mapWidth; x++) {
                    // Create a border of walls around the map
                    if (x === 0 || x === mapWidth - 1 || y === 0 || y === mapHeight - 1) {
                        dungeonMap[y][x] = 1; // Mark as wall
                        createWall(x, y); // Create the 3D wall object
                    } else if (Math.random() < 0.05) { // 5% chance of a random inner wall
                        dungeonMap[y][x] = 1; // Mark as wall
                        createWall(x, y); // Create the 3D wall object
                    } else {
                        dungeonMap[y][x] = 0; // Mark as floor
                        createFloor(x, y); // Create the 3D floor object
                    }
                }
            }
        }

        /**
         * Creates a 3D floor tile at the given grid coordinates.
         * Uses MeshPhongMaterial for better lighting.
         * @param {number} x - The x-coordinate on the dungeon grid.
         * @param {number} y - The y-coordinate on the dungeon grid.
         */
        function createFloor(x, y) {
            const geometry = new THREE.BoxGeometry(gridSize, 1, gridSize); // Flat box for floor
            const material = new THREE.MeshPhongMaterial({
                color: 0x3d4a5c, // Darker grey floor color
                specular: 0x111111, // Slight specular highlight
                shininess: 30 // Moderate shininess
            });
            const floor = new THREE.Mesh(geometry, material);
            // Position the floor tile correctly in the 3D world
            floor.position.set(
                (x - mapWidth / 2 + 0.5) * gridSize, // Center X of the grid square
                -0.5, // Half height of the floor to be below the ground plane (y=0)
                (y - mapHeight / 2 + 0.5) * gridSize  // Center Z of the grid square
            );
            scene.add(floor); // Add to the scene
        }

        /**
         * Creates a 3D wall tile at the given grid coordinates.
         * Uses MeshPhongMaterial for better lighting.
         * @param {number} x - The x-coordinate on the dungeon grid.
         * @param {number} y - The y-coordinate on the dungeon grid.
         */
        function createWall(x, y) {
            const geometry = new THREE.BoxGeometry(gridSize, gridSize * 2, gridSize); // Taller box for walls
            const material = new THREE.MeshPhongMaterial({
                color: 0x90a4ae, // Lighter grey wall color
                specular: 0x333333, // More pronounced specular highlight
                shininess: 60 // Higher shininess for walls
            });
            const wall = new THREE.Mesh(geometry, material);
            // Position the wall tile correctly in the 3D world
            wall.position.set(
                (x - mapWidth / 2 + 0.5) * gridSize,
                gridSize, // Position walls so they sit on the floor (half of wall height from y=0)
                (y - mapHeight / 2 + 0.5) * gridSize
            );
            scene.add(wall); // Add to the scene
        }

        /**
         * Creates a 3D wizard character assembled from primitive shapes, made less blocky.
         * @param {number} color - The base color for the wizard's body.
         * @returns {THREE.Group} A Three.js Group containing all wizard parts.
         */
        function createWizard(color) {
            const wizardGroup = new THREE.Group();
            const scaleFactor = 0.8; // Overall scale for the wizard
            const bodyHeight = 1.0 * scaleFactor;
            const bodyRadiusTop = 0.3 * scaleFactor;
            const bodyRadiusBottom = 0.4 * scaleFactor;
            const headRadius = 0.25 * scaleFactor;
            const hatHeight = 0.6 * scaleFactor;
            const hatRadius = 0.35 * scaleFactor;
            const armRadius = 0.1 * scaleFactor;
            const armLength = 0.8 * scaleFactor;
            const staffHandleLength = 1.2 * scaleFactor;
            const staffOrbRadius = 0.15 * scaleFactor;

            // Body (Capsule-like: Cylinder + two Hemispheres)
            const bodyCylinder = new THREE.Mesh(
                new THREE.CylinderGeometry(bodyRadiusTop, bodyRadiusBottom, bodyHeight, 32), // High segments for smooth body
                new THREE.MeshPhongMaterial({ color: color, specular: 0x555555, shininess: 80 })
            );
            bodyCylinder.position.y = bodyHeight / 2;
            wizardGroup.add(bodyCylinder);

            const bodyTopSphere = new THREE.Mesh(
                new THREE.SphereGeometry(bodyRadiusTop, 32, 16, 0, Math.PI * 2, 0, Math.PI / 2), // Top hemisphere
                new THREE.MeshPhongMaterial({ color: color, specular: 0x555555, shininess: 80 })
            );
            bodyTopSphere.position.y = bodyHeight;
            wizardGroup.add(bodyTopSphere);

            const bodyBottomSphere = new THREE.Mesh(
                new THREE.SphereGeometry(bodyRadiusBottom, 32, 16, 0, Math.PI * 2, Math.PI / 2, Math.PI / 2), // Bottom hemisphere
                new THREE.MeshPhongMaterial({ color: color, specular: 0x555555, shininess: 80 })
            );
            bodyBottomSphere.position.y = 0;
            wizardGroup.add(bodyBottomSphere);


            // Head (Sphere)
            const headGeometry = new THREE.SphereGeometry(headRadius, 64, 32); // Very high segments for smooth head
            const headMaterial = new THREE.MeshPhongMaterial({ color: 0xffe0bd, specular: 0x222222, shininess: 50 }); // Skin tone
            const head = new THREE.Mesh(headGeometry, headMaterial);
            head.position.y = bodyHeight + headRadius; // On top of body
            wizardGroup.add(head);

            // Hat (Cone)
            const hatGeometry = new THREE.ConeGeometry(hatRadius, hatHeight, 32); // High segments for smooth hat
            const hatMaterial = new THREE.MeshPhongMaterial({ color: 0x1a1a1a, specular: 0x111111, shininess: 70 }); // Dark hat
            const hat = new THREE.Mesh(hatGeometry, hatMaterial);
            hat.position.y = head.position.y + headRadius + hatHeight / 2; // On top of head
            wizardGroup.add(hat);

            // Arms (Cylinders)
            const armGeometry = new THREE.CylinderGeometry(armRadius, armRadius, armLength, 16);
            const armMaterial = new THREE.MeshPhongMaterial({ color: 0xaaaaaa, specular: 0x333333, shininess: 40 });

            const leftArm = new THREE.Mesh(armGeometry, armMaterial);
            leftArm.position.set(-bodyRadiusTop - armRadius, bodyHeight * 0.7, 0);
            leftArm.rotation.z = Math.PI / 2; // Point outwards
            wizardGroup.add(leftArm);

            const rightArm = leftArm.clone();
            rightArm.position.x = bodyRadiusTop + armRadius;
            rightArm.rotation.z = -Math.PI / 2; // Point outwards
            wizardGroup.add(rightArm);

            // Staff (Cylinder and Sphere)
            const staffHandleGeometry = new THREE.CylinderGeometry(0.005 * gridSize, 0.005 * gridSize, staffHandleLength, 16);
            const staffHandleMaterial = new THREE.MeshPhongMaterial({ color: 0x8b4513, specular: 0x333333, shininess: 40 }); // Brown
            const staffHandle = new THREE.Mesh(staffHandleGeometry, staffHandleMaterial);
            staffHandle.position.set(0.5 * scaleFactor, bodyHeight * 0.5, 0.5 * scaleFactor); // Position relative to wizard
            staffHandle.rotation.x = Math.PI / 4; // Angle the staff
            wizardGroup.add(staffHandle);

            const staffOrbGeometry = new THREE.SphereGeometry(staffOrbRadius, 32, 16); // High segments for smooth orb
            const staffOrbMaterial = new THREE.MeshPhongMaterial({ color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 0.5, specular: 0xffffff, shininess: 100 }); // Glowing Cyan orb
            const staffOrb = new THREE.Mesh(staffOrbGeometry, staffOrbMaterial);
            staffOrb.position.copy(staffHandle.position);
            staffOrb.position.y += staffHandleLength / 2; // At the top of the staff
            wizardGroup.add(staffOrb);

            wizardGroup.scale.set(gridSize, gridSize, gridSize); // Scale the entire wizard to fit the grid unit

            return wizardGroup; // Return the group containing all parts
        }

        /**
         * Creates a 3D monster assembled from primitive shapes, made less blocky.
         * @param {number} x - The x-coordinate on the dungeon grid.
         * @param {number} y - The y-coordinate on the dungeon grid.
         * @param {number} color - The color of the monster.
         */
        function createMonster(x, y, color = 0x800080) { // Default purple monster
            const monsterGroup = new THREE.Group();
            const scaleFactor = 0.7; // Overall scale for the monster

            // Main Body (Sphere)
            const bodyGeometry = new THREE.SphereGeometry(0.5 * scaleFactor, 32, 16); // Smoother sphere
            const bodyMaterial = new THREE.MeshPhongMaterial({ color: color, specular: 0x444444, shininess: 50 });
            const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
            body.position.y = 0.5 * scaleFactor; // Center on the ground
            monsterGroup.add(body);

            // Eyes (small spheres)
            const eyeGeometry = new THREE.SphereGeometry(0.1 * scaleFactor, 16, 8);
            const eyeMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff, emissive: 0xffffff, emissiveIntensity: 0.8 });
            const pupilMaterial = new THREE.MeshPhongMaterial({ color: 0x000000 });

            const eye1 = new THREE.Mesh(eyeGeometry, eyeMaterial);
            eye1.position.set(-0.2 * scaleFactor, 0.6 * scaleFactor, 0.4 * scaleFactor);
            monsterGroup.add(eye1);
            const pupil1 = new THREE.Mesh(eyeGeometry.clone(), pupilMaterial);
            pupil1.scale.set(0.5, 0.5, 0.5);
            pupil1.position.set(-0.2 * scaleFactor, 0.6 * scaleFactor, 0.45 * scaleFactor);
            monsterGroup.add(pupil1);

            const eye2 = eye1.clone();
            eye2.position.x = 0.2 * scaleFactor;
            monsterGroup.add(eye2);
            const pupil2 = pupil1.clone();
            pupil2.position.x = 0.2 * scaleFactor;
            monsterGroup.add(pupil2);

            // Spikes (Cones)
            const spikeGeometry = new THREE.ConeGeometry(0.15 * scaleFactor, 0.4 * scaleFactor, 8);
            const spikeMaterial = new THREE.MeshPhongMaterial({ color: 0x555555, specular: 0x222222, shininess: 30 });

            const spike1 = new THREE.Mesh(spikeGeometry, spikeMaterial);
            spike1.position.set(0, 0.9 * scaleFactor, 0);
            monsterGroup.add(spike1);

            const spike2 = spike1.clone();
            spike2.rotation.y = Math.PI / 2;
            spike2.position.set(0.5 * scaleFactor, 0.5 * scaleFactor, 0);
            monsterGroup.add(spike2);

            const spike3 = spike1.clone();
            spike3.rotation.y = -Math.PI / 2;
            spike3.position.set(-0.5 * scaleFactor, 0.5 * scaleFactor, 0);
            monsterGroup.add(spike3);

            monsterGroup.scale.set(gridSize, gridSize, gridSize); // Scale the entire monster to fit the grid unit

            // Position the monster mesh in the 3D world
            monsterGroup.position.set(
                (x - mapWidth / 2 + 0.5) * gridSize,
                0, // Base of monster at y=0
                (y - mapHeight / 2 + 0.5) * gridSize
            );
            scene.add(monsterGroup); // Add to the scene

            // Create a monster object to hold its mesh, stats
            const monster = {
                mesh: monsterGroup,
                health: 50,
                maxHealth: 50,
            };
            monsters.push(monster); // Add to the monsters array
        }

        /**
         * Updates the 3D position of a player/monster mesh based on its game world coordinates.
         * @param {THREE.Group} objectMesh - The Three.js Group representing the object.
         * @param {number} worldX - The x-coordinate in the 3D world.
         * @param {number} worldZ - The z-coordinate in the 3D world.
         */
        function updateObjectPosition(objectMesh, worldX, worldZ) {
            objectMesh.position.x = worldX;
            objectMesh.position.z = worldZ;
        }

        /**
         * Checks for collision with walls for a given position.
         * Returns true if the position is inside a wall, false otherwise.
         * @param {number} x - World X coordinate.
         * @param {number} z - World Z coordinate.
         * @returns {boolean}
         */
        function isCollidingWithWall(x, z) {
            // Convert world coordinates to grid coordinates
            const gridX = Math.floor(x / gridSize + mapWidth / 2);
            const gridY = Math.floor(z / gridSize + mapHeight / 2); // Z maps to Y in our 2D map

            // Check boundaries
            if (gridX < 0 || gridX >= mapWidth || gridY < 0 || gridY >= mapHeight) {
                return true; // Out of bounds is a collision
            }
            // Check if it's a wall tile
            return dungeonMap[gridY][gridX] === 1;
        }

        /**
         * Updates player position with collision detection and sliding.
         * @param {object} playerStat - The player's statistics object (playerStats.left or playerStats.right).
         * @param {THREE.Group} playerMesh - The player's Three.js mesh.
         * @param {number} moveX - Desired movement in X direction.
         * @param {number} moveZ - Desired movement in Z direction.
         */
        function updatePlayerMovement(playerStat, playerMesh, moveX, moveZ) {
            let newX = playerStat.position.x + moveX;
            let newZ = playerStat.position.z + moveZ; // Use .z for world Z

            let collidedX = isCollidingWithWall(newX, playerStat.position.z);
            let collidedZ = isCollidingWithWall(playerStat.position.x, newZ);

            if (!collidedX) {
                playerStat.position.x = newX;
            }
            if (!collidedZ) {
                playerStat.position.z = newZ;
            }

            updateObjectPosition(playerMesh, playerStat.position.x, playerStat.position.z);
        }

        /**
         * Creates a temporary visual spell effect at a given position.
         * The effect expands and fades out over time.
         * @param {THREE.Vector3} position - The world position where the spell effect should appear.
         * @param {number} color - The color of the spell effect.
         */
        function createSpellEffect(position, color) {
            const geometry = new THREE.SphereGeometry(spellProjectileRadius * 2, 16, 16); // Sphere for the effect
            const material = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.8 });
            const spellEffect = new THREE.Mesh(geometry, material);
            spellEffect.position.copy(position); // Copy player's position
            spellEffect.position.y = gridSize * 0.5; // Slightly above ground
            scene.add(spellEffect);

            // Animation variables for scaling and fading
            let scale = 0.1;
            const fadeSpeed = 0.05;

            /**
             * Animates the spell effect (scaling up and fading out).
             */
            function animateEffect() {
                if (spellEffect.material.opacity > 0) {
                    spellEffect.material.opacity -= fadeSpeed; // Reduce opacity
                    scale += 0.05; // Increase scale
                    spellEffect.scale.set(scale, scale, scale); // Apply new scale
                    requestAnimationFrame(animateEffect); // Continue animation
                } else {
                    scene.remove(spellEffect); // Remove from scene when fully faded
                    spellEffect.geometry.dispose(); // Clean up geometry
                    spellEffect.material.dispose(); // Clean up material
                }
            }
            animateEffect(); // Start the effect animation
        }

        /**
         * Class to represent a spell projectile.
         */
        class Projectile extends THREE.Mesh {
            constructor(originPlayer, targetPosition, damage) {
                const geometry = new THREE.SphereGeometry(spellProjectileRadius, 16, 16);
                const material = new THREE.MeshPhongMaterial({ color: 0xffd700, emissive: 0xffd700, emissiveIntensity: 0.5 });
                super(geometry, material);

                this.originPlayer = originPlayer;
                this.damage = damage;
                this.life = spellProjectileLife;

                // Set initial position near the origin player, slightly above ground
                this.position.copy(originPlayer.position);
                this.position.y = gridSize * 0.5;

                // Calculate velocity towards the target position
                this.velocity = new THREE.Vector3();
                this.velocity.subVectors(targetPosition, this.position).normalize().multiplyScalar(spellProjectileSpeed);

                scene.add(this); // Add projectile to the scene
            }

            update() {
                this.position.add(this.velocity);
                this.life--;
            }
        }

        /**
         * Updates the health, mana, and score displayed in the UI.
         */
        function updateUI() {
            // Update Left Player UI
            uiElements.scoreLeft.textContent = `P1 Score: ${playerStats.left.score}`;
            uiElements.healthBarLeft.style.width = `${(playerStats.left.health / playerStats.left.maxHealth) * 100}%`;
            uiElements.healthBarLeft.style.backgroundColor = playerStats.left.health > playerStats.left.maxHealth * 0.6 ? '#28a745' : (playerStats.left.health > playerStats.left.maxHealth * 0.3 ? '#ffc107' : '#dc3545');
            uiElements.manaBarLeft.style.width = `${(playerStats.left.mana / playerStats.left.maxMana) * 100}%`;

            // Update Right Player UI
            uiElements.scoreRight.textContent = `P2 Score: ${playerStats.right.score}`;
            uiElements.healthBarRight.style.width = `${(playerStats.right.health / playerStats.right.maxHealth) * 100}%`;
            uiElements.healthBarRight.style.backgroundColor = playerStats.right.health > playerStats.right.maxHealth * 0.6 ? '#28a745' : (playerStats.right.health > playerStats.right.maxHealth * 0.3 ? '#ffc107' : '#dc3545');
            uiElements.manaBarRight.style.width = `${(playerStats.right.mana / playerStats.right.maxMana) * 100}%`;
        }

        /**
         * Resets the game state: restores player health/mana, resets scores,
         * moves players to starting positions, and respawns monsters.
         */
        function resetGame() {
            // Reset Left Player stats and position
            playerStats.left.health = playerStats.left.maxHealth;
            playerStats.left.mana = playerStats.left.maxMana;
            playerStats.left.score = 0;
            playerStats.left.position.set(-5 * gridSize, 0, -5 * gridSize);
            updateObjectPosition(player1, playerStats.left.position.x, playerStats.left.position.z);

            // Reset Right Player stats and position
            playerStats.right.health = playerStats.right.maxHealth;
            playerStats.right.mana = playerStats.right.maxMana;
            playerStats.right.score = 0;
            playerStats.right.position.set(5 * gridSize, 0, 5 * gridSize);
            updateObjectPosition(player2, playerStats.right.position.x, playerStats.right.position.z);

            // Remove all existing monsters from the scene and array
            monsters.forEach(monster => scene.remove(monster.mesh));
            monsters = [];
            spawnMonsters(3); // Spawn a new set of monsters

            // Remove all active projectiles
            while (activeProjectiles.length > 0) {
                const projectile = activeProjectiles.pop();
                scene.remove(projectile);
                projectile.geometry.dispose();
                projectile.material.dispose();
            }

            updateUI(); // Update UI to reflect reset stats
            console.log("Game reset!");
        }

        /**
         * Spawns a specified number of monsters at random valid locations in the dungeon.
         * @param {number} count - The number of monsters to spawn.
         */
        function spawnMonsters(count) {
            for (let i = 0; i < count; i++) {
                let spawned = false;
                while (!spawned) {
                    // Generate random grid coordinates, avoiding the border walls
                    const randomGridX = Math.floor(Math.random() * (mapWidth - 2)) + 1;
                    const randomGridY = Math.floor(Math.random() * (mapHeight - 2)) + 1;

                    // Convert to world coordinates for spawning check
                    const worldX = (randomGridX - mapWidth / 2 + 0.5) * gridSize;
                    const worldZ = (randomGridY - mapHeight / 2 + 0.5) * gridSize;

                    // Ensure the spawn location is a floor tile and not too close to players
                    if (dungeonMap[randomGridY][randomGridX] === 0 &&
                        new THREE.Vector3(worldX, 0, worldZ).distanceTo(player1.position) > gridSize * 3 &&
                        new THREE.Vector3(worldX, 0, worldZ).distanceTo(player2.position) > gridSize * 3) {
                        createMonster(randomGridX, randomGridY); // Create the monster
                        spawned = true; // Monster successfully spawned
                    }
                }
            }
        }

        /**
         * Initializes the Three.js scene, camera, renderer, and game elements.
         */
        function init() {
            // Scene Setup: The container for all 3D objects, lights, and cameras
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x333333); // Dark grey background for the 3D scene

            // Camera Setup: Orthographic camera for a top-down semi-isometric view
            const aspectRatio = window.innerWidth / window.innerHeight;
            const frustumSize = Math.max(mapWidth, mapHeight) * gridSize * 0.7; // Adjusted frustum size for dungeon
            camera = new THREE.OrthographicCamera(
                frustumSize * aspectRatio / - 2,
                frustumSize * aspectRatio / 2,
                frustumSize / 2,
                frustumSize / - 2,
                1, 1000
            );
            // Position camera for a top-down semi-isometric view
            camera.position.set(0, 40, 40); // Higher Y, and equal X/Z for a balanced angle
            camera.lookAt(0, 0, 0); // Make the camera look directly at the origin

            // Renderer Setup: Renders the 3D scene onto a 2D canvas
            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(window.devicePixelRatio);
            document.getElementById('game-container').appendChild(renderer.domElement);

            // Lighting: Essential for seeing 3D objects
            const ambientLight = new THREE.AmbientLight(0x404040, 2); // Soft ambient light, affects all objects evenly
            scene.add(ambientLight);
            const directionalLight = new THREE.DirectionalLight(0xffffff, 1); // Directional light, like sunlight
            directionalLight.position.set(0, 100, 0); // Positioned above the scene
            scene.add(directionalLight);

            // Generate the dungeon environment
            generateDungeon();

            // Player 1 Character: Wizard
            player1 = createWizard(0xff4500); // Red-orange color
            player1.position.copy(playerStats.left.position);
            scene.add(player1);

            // Player 2 Character: Wizard
            player2 = createWizard(0x00aaff); // Blue color
            player2.position.copy(playerStats.right.position);
            scene.add(player2);

            // UI Elements Initialization and Event Listeners
            uiElements.resetButton.addEventListener('click', resetGame);

            // Event Listeners for Keyboard Input
            window.addEventListener('keydown', onKeyDown);
            window.addEventListener('keyup', onKeyUp);
            window.addEventListener('resize', onWindowResize);

            // Spawn initial monsters
            spawnMonsters(3);

            // Update UI for the first time
            updateUI();

            // Start the animation loop
            animate();
        }

        /**
         * The main animation loop for Three.js.
         * It continuously renders the scene and calls the game logic.
         */
        function animate() {
            requestAnimationFrame(animate);

            // Player 1 Movement
            let p1MoveX = 0, p1MoveZ = 0;
            if (keys.w) p1MoveZ -= playerMoveSpeed; // Up
            if (keys.s) p1MoveZ += playerMoveSpeed; // Down
            if (keys.a) p1MoveX -= playerMoveSpeed; // Left
            if (keys.d) p1MoveX += playerMoveSpeed; // Right

            // Normalize diagonal movement speed
            if (p1MoveX !== 0 && p1MoveZ !== 0) {
                const length = Math.sqrt(p1MoveX * p1MoveX + p1MoveZ * p1MoveZ);
                p1MoveX /= length;
                p1MoveZ /= length;
                p1MoveX *= playerMoveSpeed;
                p1MoveZ *= playerMoveSpeed;
            }
            updatePlayerMovement(playerStats.left, player1, p1MoveX, p1MoveZ);


            // Player 2 Movement
            let p2MoveX = 0, p2MoveZ = 0;
            if (keys.i) p2MoveZ -= playerMoveSpeed; // Up
            if (keys.k) p2MoveZ += playerMoveSpeed; // Down
            if (keys.j) p2MoveX -= playerMoveSpeed; // Left
            if (keys.l) p2MoveX += playerMoveSpeed; // Right

            // Normalize diagonal movement speed
            if (p2MoveX !== 0 && p2MoveZ !== 0) {
                const length = Math.sqrt(p2MoveX * p2MoveX + p2MoveZ * p2MoveZ);
                p2MoveX /= length;
                p2MoveZ /= length;
                p2MoveX *= playerMoveSpeed;
                p2MoveZ *= playerMoveSpeed;
            }
            updatePlayerMovement(playerStats.right, player2, p2MoveX, p2MoveZ);

            // Auto-rotate players to face each other
            player1.lookAt(player2.position.x, player1.position.y, player2.position.z);
            player2.lookAt(player1.position.x, player2.position.y, player1.position.z);

            // Player Actions
            if (keys.p1_action) {
                if (playerStats.left.mana >= 10) {
                    playerStats.left.mana -= 10;
                    console.log("Left player cast a spell!");
                    createSpellEffect(player1.position, 0xffa500); // Visual effect at cast
                    
                    // Find closest monster for projectile target
                    let closestMonster = null;
                    let minDistance = Infinity;
                    monsters.forEach(monster => {
                        const dx = monster.mesh.position.x - player1.position.x;
                        const dz = monster.mesh.position.z - player1.position.z;
                        const distance = Math.sqrt(dx * dx + dz * dz);
                        if (distance < minDistance) {
                            minDistance = distance;
                            closestMonster = monster;
                        }
                    });

                    if (closestMonster) {
                        activeProjectiles.push(new Projectile(player1, closestMonster.mesh.position, spellDamage));
                    } else {
                        console.log("No monster to target for Left player's spell.");
                    }
                } else {
                    console.log("Left player out of mana!");
                }
                keys.p1_action = false; // Prevent continuous casting on key hold
            }

            if (keys.p2_action) {
                if (playerStats.right.mana >= 10) {
                    playerStats.right.mana -= 10;
                    console.log("Right player cast a spell!");
                    createSpellEffect(player2.position, 0x00ff00); // Visual effect at cast

                    // Find closest monster for projectile target
                    let closestMonster = null;
                    let minDistance = Infinity;
                    monsters.forEach(monster => {
                        const dx = monster.mesh.position.x - player2.position.x;
                        const dz = monster.mesh.position.z - player2.position.z;
                        const distance = Math.sqrt(dx * dx + dz * dz);
                        if (distance < minDistance) {
                            minDistance = distance;
                            closestMonster = monster;
                        }
                    });

                    if (closestMonster) {
                        activeProjectiles.push(new Projectile(player2, closestMonster.mesh.position, spellDamage));
                    } else {
                        console.log("No monster to target for Right player's spell.");
                    }
                } else {
                    console.log("Right player out of mana!");
                }
                keys.p2_action = false; // Prevent continuous casting on key hold
            }

            // Update and check collisions for projectiles
            for (let i = activeProjectiles.length - 1; i >= 0; i--) {
                const projectile = activeProjectiles[i];
                projectile.update();

                let hit = false;
                // Check collision with monsters
                for (let j = monsters.length - 1; j >= 0; j--) {
                    const monster = monsters[j];
                    if (projectile.position.distanceTo(monster.mesh.position) < (spellProjectileRadius + monster.mesh.scale.x * 0.5 * gridSize)) {
                        monster.health -= projectile.damage;
                        createSpellEffect(monster.mesh.position, 0xff0000); // Red impact for damage
                        if (monster.health <= 0) {
                            console.log("Monster defeated!");
                            scene.remove(monster.mesh);
                            monsters.splice(j, 1);
                            // Award score to the player who cast the spell
                            if (projectile.originPlayer === player1) playerStats.left.score += 100;
                            else if (projectile.originPlayer === player2) playerStats.right.score += 100;
                        }
                        hit = true;
                        break; // Only hit one monster per projectile
                    }
                }

                if (hit || projectile.life <= 0) {
                    scene.remove(projectile);
                    projectile.geometry.dispose();
                    projectile.material.dispose();
                    activeProjectiles.splice(i, 1);
                }
            }

            // Monster AI: Move towards and attack closest player
            monsters.forEach(monster => {
                const distToP1 = monster.mesh.position.distanceTo(player1.position);
                const distToP2 = monster.mesh.position.distanceTo(player2.position);

                let targetPlayerMesh = null;
                let targetPlayerStat = null;

                if (distToP1 <= distToP2) {
                    targetPlayerMesh = player1;
                    targetPlayerStat = playerStats.left;
                } else {
                    targetPlayerMesh = player2;
                    targetPlayerStat = playerStats.right;
                }

                // Monster movement direction
                let monsterMoveX = 0;
                let monsterMoveZ = 0;

                if (monster.mesh.position.x < targetPlayerMesh.position.x) {
                    monsterMoveX = monsterMoveSpeed;
                } else if (monster.mesh.position.x > targetPlayerMesh.position.x) {
                    monsterMoveX = -monsterMoveSpeed;
                }

                if (monster.mesh.position.z < targetPlayerMesh.position.z) {
                    monsterMoveZ = monsterMoveSpeed;
                } else if (monster.mesh.position.z > targetPlayerMesh.position.z) {
                    monsterMoveZ = -monsterMoveSpeed;
                }

                // Normalize diagonal movement for monsters
                if (monsterMoveX !== 0 && monsterMoveZ !== 0) {
                    const length = Math.sqrt(monsterMoveX * monsterMoveX + monsterMoveZ * monsterMoveZ);
                    monsterMoveX /= length;
                    monsterMoveZ /= length;
                }

                // Apply monster movement with collision detection
                let newMonsterX = monster.mesh.position.x + monsterMoveX;
                let newMonsterZ = monster.mesh.position.z + monsterMoveZ;

                let collidedMonsterX = isCollidingWithWall(newMonsterX, monster.mesh.position.z);
                let collidedMonsterZ = isCollidingWithWall(monster.mesh.position.x, newMonsterZ);

                if (!collidedMonsterX) {
                    monster.mesh.position.x = newMonsterX;
                }
                if (!collidedMonsterZ) {
                    monster.mesh.position.z = newMonsterZ;
                }

                // Monster attack logic
                if (monster.mesh.position.distanceTo(targetPlayerMesh.position) < gridSize * 1.2) { // Attack if close to player
                    targetPlayerStat.health = Math.max(0, targetPlayerStat.health - 0.5); // Deal small continuous damage
                    console.log(`Monster attacked ${targetPlayerStat === playerStats.left ? 'Left' : 'Right'} Player!`);
                }
            });

            // Player-to-player healing when close
            const distanceBetweenPlayers = player1.position.distanceTo(player2.position);
            if (distanceBetweenPlayers < playerHealingThreshold) {
                // Player 1 heals Player 2
                playerStats.right.health = Math.min(playerStats.right.maxHealth, playerStats.right.health + healingAmountPerFrame);
                // Player 2 heals Player 1
                playerStats.left.health = Math.min(playerStats.left.maxHealth, playerStats.left.health + healingAmountPerFrame);
            }

            // Check for game over condition
            if (playerStats.left.health <= 0 && playerStats.right.health <= 0) {
                console.log("Game Over! Both players incapacitated.");
                // In a full game, you'd show a game over screen here.
            }

            updateUI(); // Update UI every frame
            renderer.render(scene, camera);
        }

        // Initialize the game when the window loads
        window.onload = init;
    </script>
</body>
</html>