Repository navigation
Expand file tree
/
Copy pathjs.js
More file actions
2706 lines (2375 loc) · 99.9 KB
/
Copy pathjs.js
File metadata and controls
2706 lines (2375 loc) · 99.9 KB
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
"use strict";
window.onerror = alert;
let theCurrentLevel = null;
let theCurrentPlayer = null;
let theCurrentDisplay = null;
let isDropping = false;
let isPuttingOn = false;
let isRemoving = false;
let pendingAttackTarget = null; //when player is asked "attack this ignore/following monster? Y/N" the monster is stored here
const visRad = 8;//how many tiles the player can see in any direction
const dungeonDepth = 5;//how many levels deep the dungeon goes (the crown is on the deepest level)
const traits = {
pyromancer:
{
name: "Pyromancer",
element: "pyro",
maxHpBonus: 0,
atkBonus: 2,
defBonus: 0,
cantWeild: ["Armour", "Claymore"],
desc: "uses pyro (fire) to deal 2 extra damage, but can't wear armour or use a claymore"
},
cryomancer:
{
name: "Cryomancer",
element: "cryo",
maxHpBonus: 10,
atkBonus: 1,
defBonus: 1,
cantWeild: ["Claymore"],
desc: "uses cryo (ice) to gain 10 HP, and an attack and defence bonus of 1, but can't use a claymore"
},
hydromancer:
{
name: "Hydromancer",
element: "hydro",
maxHpBonus: 30,
atkBonus: -1,
defBonus: 1,
cantWeild: ["Armour"],
desc: "uses hydro (water) to gain 30 HP and 1 defence, but loses 1 attack and can't wear armour"
},
electromancer:
{
name: "Electromancer",
element: "electro",
maxHpBonus: -20,
atkBonus: 4,
defBonus: 0,
cantWeild: ["Armour", "Shield"],
desc: "uses electro (electricity) to gain 4 attack, but loses 20 HP and can't wear armour and weild a shield"
},
aeromancer:
{
name: "Aeromancer",
element: "anemo",
maxHpBonus: 5,
atkBonus: 2,
defBonus: 0,
cantWeild: ["Armour", "Claymore"],
desc: "uses anemo (wind) to gain 5 Hp and 2 attack but can't wear armour or use a claymore"
},
geomancer:
{
name: "Geomancer",
element: "geo",
maxHpBonus: 15,
atkBonus: 0,
defBonus: 2,
cantWeild: [],
desc: "uses geo (earth) to gain 15 HP and 2 defence."
},
dendromancer:
{
name: "Dendromancer",
element: "dendro",
maxHpBonus: 20,
atkBonus: 1,
defBonus: 1,
cantWeild: ["Armour"],
desc: "uses dendro (nature) to gain 20 HP, 1 attack, and 1 defence, but can't wear armour"
}
};
const effectiveness =//lookup table for what elements are effective against what elements
{
pyro: { pyro: 1, hydro: 0.5, electro: 1, cryo: 1, anemo: 2, geo: 1, dendro: 1 },
hydro: { pyro: 2, hydro: 1, electro: 1, cryo: 1, anemo: 1, geo: 1, dendro: 0.5 },
electro: { pyro: 1, hydro: 1, electro: 1, cryo: 0.5, anemo: 1, geo: 1, dendro: 2 },
cryo: { pyro: 1, hydro: 1, electro: 2, cryo: 1, anemo: 1, geo: 0.5, dendro: 1 },
anemo: { pyro: 0.5, hydro: 1, electro: 1, cryo: 1, anemo: 1, geo: 2, dendro: 1 },
geo: { pyro: 1, hydro: 1, electro: 1, cryo: 2, anemo: 0.5, geo: 1, dendro: 1 },
dendro: { pyro: 1, hydro: 2, electro: 0.5, cryo: 1, anemo: 1, geo: 1, dendro: 1 }
};//every element is strong against exactly one and weak against exactly one
const messageLog = [];//the output box should log messages (20 i've decided) instead of just showing one
function logMessage(message) {
messageLog.push(message);//adds the message to the end of the array
if (messageLog.length > 20) {
messageLog.shift(); // deletes the 21st oldest message, keeping 20
}
const output = document.getElementById('outputtext');
if (output)//failsafe
{
output.innerText = messageLog.join("\n");//new line character beetween each line
output.scrollTop = output.scrollHeight; // Auto-scroll to the bottom
}
}
function updateHealthDisplayer() {//updates the player's current health on the display
const healthBox = document.getElementById("hb");
if (!healthBox) return;//failsafe
if (theCurrentPlayer)//if there is a player
{
const hpct = Math.round((theCurrentPlayer.hp / theCurrentPlayer.maxHp) * 100);
healthBox.innerText = "Health: " + hpct + "%";
} else {
healthBox.innerText = "Health: -";//display nothing for health if tehre is no player
}
}
function updateFloorIndicator()//show which floor the player is on
{
const floorBox = document.getElementById("floor-indicator");
if (!floorBox) return;//failsafe
if (theCurrentLevel)//if there's a level
{
floorBox.innerText = "Floor: " + theCurrentLevel.depth;
}
else
{
floorBox.innerText = "Floor: -";//display nothing if no level
}
}
function updateEquipmentDisplayer()//updates the box that shows what the player has equipped
{
const equipBox = document.getElementById("equipment-list");
if (!equipBox) return;//failsafe
if (!theCurrentPlayer)//if there is no player (like after dying)
{
equipBox.innerText = "Equipment: -";
return;
}
let text = "Equipment:\n";//start the text with a heading line
//these slots have one item each unlike hands
const oneItemSlots = ["head", "body", "finger", "feet"];//all the single-item slots (hands are handled separately)
for (const slot of oneItemSlots)//loop through each slot
{
const item = theCurrentPlayer.equipment.get(slot);//what is in this slot (undefined if empty)
if (item)//if there is an item in the slot
{
text += slot + ": " + item.constructor.name.toLowerCase() + "\n";//show its name
}
else//nothing
{
text += slot + ": none\n";
}
}
//two slots for hands
if (theCurrentPlayer.handItems.length === 0)//if nothing is being held
{
text += "hand: none";//none with no newline because it's the last line
}
else//if something is being held
{
text += "hand: ";
for (let i = 0; i < theCurrentPlayer.handItems.length; i++)//loop through each held item
{
const item = theCurrentPlayer.handItems[i];//get this held item
const itemName = item.constructor.name.toLowerCase();//the item's name
//"1 hand" vs "2 hands"
let handWord = "hand";//singular by default
if (item.hands !== 1)
{
handWord = "hands";//plural if it takes more than one hand
}
text += itemName + " (" + item.hands + " " + handWord + ")";//like "sword (1 hand)"
if (i < theCurrentPlayer.handItems.length - 1)//if this isn't the last held item
{
text += ", ";//separate the items with a comma
}
}
//also show how many of the hand slots are being used total
let totalHands = 0;//start at zero
for (const item of theCurrentPlayer.handItems)//loop through each held item
{
totalHands += item.hands;//add up how many hands each one takes
}
text += "\nhands used: " + totalHands + "/2";
}
equipBox.innerText = text;//draw it to the screen
}
function updateTraitDisplayer()//updates the box that shows the player's chosen trait(s)
{
const traitBox = document.getElementById("traitdisplay");
if (!traitBox)//failsafe
{
return;
}
if (!theCurrentPlayer)//no player
{
traitBox.innerText = "traits: -";
return;
}
if (theCurrentPlayer.traits.length === 0)//no traits chosen yet
{
traitBox.innerText = "none";
return;
}
let text = "Traits:\n";
for (let i = 0; i < theCurrentPlayer.traits.length; i++)//go through all traits IF it ends up being possible to have multiple traits at some point. futureproofing
{
const traitKey = theCurrentPlayer.traits[i];
const trait = traits[traitKey];//look up the full trait definition
if (!trait)//failsafe
{
continue;
}
text += trait.name + " (" + trait.element + ")";//"Pyromancer (pyro)"
if (i < theCurrentPlayer.traits.length - 1)//separate multiple traits with a newline (not last one)
{
text += "\n";
}
}
traitBox.innerText = text;//draw it to the screen
}
const helpbutton = document.getElementById('helpbutton');
if (helpbutton)
{
helpbutton.addEventListener('click', () => {
logMessage(
"-------------------------\n" +
"Controls:\n" +
"WASD to move (or attack if moving into a monster, which prompts you to press Y to confirm)\n" +
", to pick up items\n" +
"< (shift + ,) to climb the stairs\n" +
"> (shift + .) to descend the stairs\n" +
"I to manually show inventory (you shouldn't need to)\n" +
"Q to drop an item\n" +
"P to start weilding an item\n" +
"R to remove an item you're wearing / wielding\n" +
"The goal is to find the crown (♕) at the bottom and climb back up to the top level\n"+
"-------------------------"
);
});
}
const submitbutton = document.getElementById('submitbutton');
if (submitbutton) {
const inputbox = document.getElementById('inputbox');
const outputtext = document.getElementById('outputtext');
const enteryourname = document.getElementById('enteryourname');
const gamegridtext = document.getElementById('gamegridtext');
const traittext = document.getElementById('traittext');
const traitlist = document.getElementById('traitlist');
let storedData = "";
let selectedTraitKey = null;//chosen by the player
//create a button for each trait
for (const traitKey in traits)
{
const trait = traits[traitKey];//the trait definition object
const btn = document.createElement("button");//make the element (with the css style)
btn.textContent = trait.name + ": " + trait.desc;//put the trait name and description in the button
btn.dataset.traitKey = traitKey;//remember which trait this button is for (so the random button can find it)
btn.addEventListener('click', () => {
for (const other of traitlist.querySelectorAll('button'))
{
other.classList.remove('selected');//deselect
}
btn.classList.add('selected');//visually select
selectedTraitKey = traitKey;//select in code
if (traittext)//failsafe
{
traittext.innerText = "pick your trait (picked: " + trait.name + ")";//update the heading text
}
});
traitlist.appendChild(btn);//place the button into the trait list area
}
//random button code
const randomBtn = document.createElement("button");
randomBtn.textContent = "random trait";
randomBtn.addEventListener('click', () =>
{
const traitKeys = Object.keys(traits);//all avaliable traits
const randomKey = traitKeys[uniformrandom(traitKeys.length - 1)];//random one
//find that trait's button and click it (runs the normal selection code)
for (const btn of traitlist.querySelectorAll('button'))//get the right button
{
if (btn.dataset.traitKey === randomKey)
{
btn.click();//artificial click
break;
}
}
});
traitlist.appendChild(randomBtn);//place it in the list below the trait buttons
//making the enter key also work to submit, mirroring the click behaviour
inputbox.addEventListener('keydown', (event) => {
if (event.key === "Enter") {
event.preventDefault();
submitbutton.click();//click the submit button
}
});
submitbutton.addEventListener('click', () => {
storedData = inputbox.value;
if (!selectedTraitKey)//trait is required before starting: refuse and tell the player
{
if (traittext)//failsafe
{
traittext.innerText = "pick a trait first!";//can't start without a trait
}
return;
}
inputbox.style.display = 'none';
enteryourname.style.display = 'none';
if (traittext)
{
traittext.style.display = 'none';//hide the prompt text
}
traitlist.style.display = 'none';//hide the trait buttons
submitbutton.style.display = 'none';
const gameContainer = document.getElementById('game-container');
if (gameContainer) {
gameContainer.style.display = 'grid';//bunch of added space using this and other css changes to make sure long lines in the log don't push content to the right
}
console.log(storedData);
logMessage("hello " + storedData + ", welcome to the dungeon.");//using new log function instead
const roomdata = createtherooms();//instead of just running the function that creates the room, sitll dothat, but save the data (grid and rooms)
const dungeon = new Dungeon();
theCurrentLevel = new Level(roomdata.grid);
theCurrentLevel.parent = dungeon;
theCurrentPlayer = new Player();
theCurrentPlayer.name = storedData;
theCurrentPlayer.applyTrait(selectedTraitKey);//apply the trait
const chosenTrait = traits[selectedTraitKey];
logMessage("you are a " + chosenTrait.name + "!");//tell the player what they became
theCurrentPlayer.parent = theCurrentLevel;
updateHealthDisplayer();//initially to show the health
updateFloorIndicator();//initially to show which floor the player is on
updateEquipmentDisplayer();//show what slots can be equipped into
updateTraitDisplayer();//show trait in ui
//pick a random room and stand in its middle but try again if there's a pillar (it was possible for overlap before)
for (let attempt = 0; attempt < 20; attempt++)//20 attempts as per usual
{
const startroom = roomdata.rooms[uniformrandom(roomdata.rooms.length - 1)];//random start room
const px = Math.floor(startroom.left + startroom.width / 2);//middle of the room on the x and y axis
const py = Math.floor(startroom.top + startroom.height / 2);//
if (theCurrentLevel.map.get(px, py) === " ")//only spawn on bare floor (pillars "O" and walls "█" block)
{
theCurrentPlayer.x = px;//set the player's position
theCurrentPlayer.y = py;
break;//found a good spot
}
}
const upStairway = new UpStairway();
upStairway.x = theCurrentPlayer.x;
upStairway.y = theCurrentPlayer.y;
upStairway.parent = theCurrentLevel;
//first down stairs to go to the new levels
const downStairway = new DownStairway();
for (let attempt = 0; attempt < 20; attempt++)
{
const room = roomdata.rooms[uniformrandom(roomdata.rooms.length - 1)];//random room
const sx = boundedrandom(room.left + 1, room.right - 2);//random x
const sy = boundedrandom(room.top + 1, room.bottom - 2);//random y
const occupied = (theCurrentPlayer.x === sx && theCurrentPlayer.y === sy) ||//not on the player
theCurrentLevel.children.some(child => child.x === sx && child.y === sy) ||//check through all children of the level
theCurrentLevel.map.get(sx, sy) !== " ";//clear space
if (!occupied)//if the spot is free
{
downStairway.x = sx;//place it there
downStairway.y = sy;//
downStairway.parent = theCurrentLevel;
break;
}
}
//spawning random items on the level
const itemClasses = [Coffee, Sword, Lyre, Potion, Sword, Helmet, Armour, Boots, Shield, Claymore, Ring, Cloak];//list of all the items
const numItems = boundedrandom(5, 10);
for (let i = 0; i < numItems; i++) {//for the randomly chosen amount of items
for (let attempt = 0; attempt < 20; attempt++) {//20 attempts
const itemRoom = roomdata.rooms[uniformrandom(roomdata.rooms.length - 1)];//random room
const ix = boundedrandom(itemRoom.left + 1, itemRoom.right - 2);//random x in room
const iy = boundedrandom(itemRoom.top + 1, itemRoom.bottom - 2);//random y in room
const occupied = (theCurrentPlayer.x === ix && theCurrentPlayer.y === iy) || theCurrentLevel.children.some(child => child.x === ix && child.y === iy) || theCurrentLevel.map.get(ix, iy) !== " ";//check if there's an entity there already or if the tile is a wall/pillar
if (!occupied) {//if not occupied
const itemClass = itemClasses[uniformrandom(itemClasses.length - 1)];//random item with the name itemClass
const item = new itemClass();//create the item
item.x = ix;//set location
item.y = iy;
item.parent = theCurrentLevel;
break;
}
}
}
// adding 3-7 monsters (now just monsters in general instead of just hilichurls) of various goals to random rooms
const monsterClasses = [hilichurl, hilichurl, hilichurl, slime, treasureHoarder, dog];// added more genshin impact enemies (other than dog). hilichurls are most common
const numMonsters = boundedrandom(3, 7);
for (let i = 0; i < numMonsters; i++) {//loop through number of monsters to add
for (let attempt = 0; attempt < 20; attempt++) {//20 attempts before giving up for each monster
const monsterRoom = roomdata.rooms[uniformrandom(roomdata.rooms.length - 1)];//getting a random room to place the monster in
const hx = boundedrandom(monsterRoom.left + 1, monsterRoom.right - 2);//random x in room
const hy = boundedrandom(monsterRoom.top + 1, monsterRoom.bottom - 2);//random y in room
const occupied = (theCurrentPlayer.x === hx && theCurrentPlayer.y === hy) ||
theCurrentLevel.children.some(child => child.x === hx && child.y === hy) ||
theCurrentLevel.map.get(hx, hy) !== " ";//arrow callback function checking if player is there or any other entities are there, and also now rejecting non-floor in general
if (!occupied) {//if the position is not occupied
const monsterClass = monsterClasses[uniformrandom(monsterClasses.length - 1)];//random monster type but hilichurl is still most common
const theMonster = new monsterClass();//create the random monster
theMonster.x = hx;
theMonster.y = hy;
theMonster.parent = theCurrentLevel;
// 30% chance for this hilichurl or other monster to get a random item
if (uniformrandom(9) < 3) {
const itemClass = itemClasses[uniformrandom(itemClasses.length - 1)];
const carriedItem = new itemClass();
carriedItem.parent = theMonster;
//automatically equip if possible
if (theMonster.canWear(carriedItem))
{
new putOnAction(theMonster, carriedItem).execute();
}
}
break;
}
}
}
console.log("Player spawned at:", theCurrentPlayer.x, theCurrentPlayer.y);
console.log("made ", dungeon);//added earlier because the code wasn't working before
theCurrentDisplay = new Grid(theCurrentLevel.map.width, theCurrentLevel.map.height, " ");//the grid that will be displayed. blank for now
drawLevel(theCurrentLevel, theCurrentDisplay);//we have to pass in level instead of roomdata.grid becuase roomdata.grid doesn't know the location of the entities
outputtext.classList.add("dungeon");//styling welcome text
window.focus(); // Release focus from inputs so keyboard controls work instantly
});
}
function whereVisible(map, playerX, playerY, radius)//the function that calculates where the player can see works by creating a new temprary grid with the same shape and size as the actual grid, but only with true and false for where the player should be able to see
{
const visibility = new Grid(map.width, map.height, false);//creating new grid with all squares invisible (false) by default
for (let y = 0; y < map.height; y++)
{
for (let x = 0; x < map.width; x++)
{
const dx = x - playerX;
const dy = y - playerY;
if (dx * dx + dy * dy < radius * radius)//pythagorean distance formula. only check tiles in range
{
//walls and pillars block vision behind them, so only mark it visible if the line to it is clear
visibility.set(x, y, hasLineOfSight(map, playerX, playerY, x, y));
}
}
}
//there was a bug where the corner tiles of the map/filled in walls ("█") were being made invisible by the code that doens't allow you to see past you current room's wall. the code until the end of this function fixes that
let changed = true;
while (changed)
{
changed = false;
for (let y = 0; y < map.height; y++)
{
for (let x = 0; x < map.width; x++)
{
const dx = x - playerX;
const dy = y - playerY;
if (dx * dx + dy * dy >= radius * radius) continue;//if this tile is in the circular range of the player's vision
if (visibility.get(x, y)) continue;//already visible then continue to next tile
if (!blocksVision(map.get(x, y))) continue;//only apply this to walls and what's behind and next to walls
let neighborVisible = false;
for (let ny = y - 1; ny <= y + 1 && !neighborVisible; ny++)//
{ // go through the 3x3 grid around each square (that of course is in the player's visibility circle)
for (let nx = x - 1; nx <= x + 1; nx++) //
{
if (nx === x && ny === y) continue;//this is what skips the tile itself
if (nx < 0 || nx >= map.width || ny < 0 || ny >= map.height) continue;//off the map
if (visibility.get(nx, ny))
{
neighborVisible = true;
break;
}
}
}
if (neighborVisible)
{
visibility.set(x, y, true);//set these walls to visible
changed = true;
}
}
}
}
return visibility;
}
//anything that is not bare floor blocks vision (walls, room borders, pillars, etc.)
function blocksVision(cell) {
return cell !== " ";
}
//walk along the line from the player to the target tile; if we hit a wall or pillar first, the target is hidden
function hasLineOfSight(map, x0, y0, x1, y1) {
const lineCells = cellsInLine(x0, y0, x1, y1);
for (const c of lineCells) {
if (c.x === x0 && c.y === y0) continue;// player's own tile so don't count it
if (c.x === x1 && c.y === y1) return true;// only runs if it has gotten to the final tile which is not checked itself
if (blocksVision(map.get(c.x, c.y))) return false;// returns false if the blocksvision function returns true
}
return true;//failsafe that runs in a scenario where the player's own cell is checked alone
}
//a "supercover" line: when the line passes exactly through a corner, both side tiles are included
//so you can't peek diagonally through the crack between two walls or pillars
function cellsInLine(x0, y0, x1, y1) {
const cells = [];
const dx = Math.abs(x1 - x0);//hroizontal distance (always positive) between the first and last x
const dy = Math.abs(y1 - y0);//vertical distance (always positive) between the first and last y
let stepX;
if (x0 < x1)
{
stepX = 1;//moving right
}
else
{
stepX = -1;//moving left
}
let stepY;
if (y0 < y1)
{
stepY = 1;//moving down
}
else
{
stepY = -1;//moving up
}
let x = x0;
let y = y0;
let ix = 0;
let iy = 0;
cells.push({ x, y });//the player's initial tile
while (ix < dx || iy < dy) {
// This decision formula compares how "far along" each axis we are.
const decision = (1 + 2 * ix) * dy - (1 + 2 * iy) * dx;//pos if the invisible line passes through a horizontal line before vertical (and vice versa), 0 if it passes through an intersection of 4 perfectly (draws from middle of current square that the line is on)
if (decision === 0)//passing through intersection of 4 tiles
{
cells.push({ x: x + stepX, y });
cells.push({ x, y: y + stepY });
x = x + stepX;//
y = y + stepY;//
ix = ix + 1; //since we moved on the x and y axis for the visibility line
iy = iy + 1; //
cells.push({ x, y });
}
else if (decision < 0)//going to the horuizontal tile
{
x = x + stepX;
ix = ix + 1;
cells.push({ x, y });
}
else//going to the vertical tile
{
y = y + stepY;
iy = iy + 1;
cells.push({ x, y });
}
}
return cells;//array of all cells between any two cells in a line
}
//new randomness functions that make the code more readable
function uniformrandom(max) {
return Math.floor(Math.random() * (max + 1));
}
function boundedrandom(min, max) {
return uniformrandom(max - min) + min;
}
function drawLevel(level, display) {
if (!theCurrentPlayer) return;//failsafe
const visibilityGrid = whereVisible(level.map, theCurrentPlayer.x, theCurrentPlayer.y, visRad);//runs the function that gets the grid of where the player should be able to see
//get player memory
let memoryGrid = theCurrentPlayer.memory.get(level);
if (!memoryGrid)//if no memory yet
{
memoryGrid = new Grid(level.map.width, level.map.height, false);//create a new grid filled with false
theCurrentPlayer.memory.set(level, memoryGrid);//show already explored tiles
}
for (let y = 0; y < level.map.height; y++) {
for (let x = 0; x < level.map.width; x++) {
if (visibilityGrid.get(x, y))//only paint cells that are visible according to the equivelant vision grid (the "disk")
{
memoryGrid.set(x, y, true);//save newly explored tiles
display.set(x, y, level.map.get(x, y));//visible tiles get shown (still without entities. now if they're supposed to be visible they get shows later in the for loop beginning with for (const child of sortedChildren))
}
else if (memoryGrid.get(x, y))//if in memory show too
{
const cell = level.map.get(x, y);//remembered room/hall interior (blank floor) shows as shaded, but walls and pillars stay visible
if (cell === " ")
{
display.set(x, y, "▒");//now if you cant see a floor tile even if you've been there, it shows up as ▒ but the walls of remembered areas are still visible. this is so you know the difference between where there's no entities and where you simply can't see
}
else
{
display.set(x, y, cell);//remembered walls and pillars (once i add that) stay visible
}
}
else
{
display.set(x, y, "▒");//clear cells outside (shaded so you can see the circle boundary)
}
}
}
const sortedChildren = level.children.sort((a, b) => {//switched to new priority system which determines what should be displayed if multiple characters overlap
const priorityA = a.renderPriority || 0;//0 if not defined for whatever reason. here we're telling the sorting algorithm what to sort all the entities by
const priorityB = b.renderPriority || 0;
return priorityA - priorityB;//return lowest first in entities list
});
for (const child of sortedChildren) {//loop through all the children and display them in order (highest priority is last as they get written last to display, meaning they'll nbe actually displayed)
if (visibilityGrid.get(child.x, child.y)) {//only draw entities that are visible (if this returns true)
const symbol = child.symbol || "?";//gets symbol from child class (set to @ for player below) or '?' if there is no symbol
display.set(child.x, child.y, symbol);//sets it onto the grid that is to be displayed
}
}
const gamegridtext = document.getElementById('gamegridtext');
if (gamegridtext) {
gamegridtext.textContent = display.toString();//draw to screen
}
}
class Grid {
constructor(width, height, val) {
// if there's negative values in width or height
if (width < 0 || height < 0) {
throw new RangeError("Width and height must be greater than or equal to zero.");
}
this.width = width;
this.height = height;
this.grid = [];
// make a row for each column
for (let y = 0; y < height; y++) {
let row = [];
for (let x = 0; x < width; x++) {
row.push(val);
}
this.grid.push(row);
}
}
// get an x and y coordinae's value
get(x, y) {
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
throw new RangeError("x or y out of bounds");
}
return this.grid[y][x];
}
// sets an x and y coordinate's value
set(x, y, newval) {
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
throw new RangeError("x or y out of bounds");
}
this.grid[y][x] = newval;
}
// turns the grid into a string with a newline \n between each row
toString() {
let lines = [];
for (let y = 0; y < this.height; y++) {
lines.push(this.grid[y].join(""));
}
return lines.join("\n");
}
addroom(room) {
// corners
this.set(room.left, room.top, "┌");
this.set(room.right - 1, room.top, "┐");
this.set(room.left, room.bottom - 1, "└");
this.set(room.right - 1, room.bottom - 1, "┘");
// top and bottom walls
for (let x = room.left + 1; x < room.right - 1; x++) {
this.set(x, room.top, "─");
this.set(x, room.bottom - 1, "─");
}
// left and right walls
for (let y = room.top + 1; y < room.bottom - 1; y++) {
this.set(room.left, y, "│");
this.set(room.right - 1, y, "│");
}
// interior empty space
for (let y = room.top + 1; y < room.bottom - 1; y++) {
for (let x = room.left + 1; x < room.right - 1; x++) {
this.set(x, y, " ");
}
}
}
}
class Entity {
constructor() {
if (new.target === Entity) {
throw new Error("can't do that");//because you're supposed to make a specific kind of entity ("abstract class")
}
this._parent = null; //defaukt parents/childten (nothing)
this._children = [];
}
get renderPriority() {
return 0;//lowest priority by default so if anything has a (higher) priority set, it'll be displayed over this (such as player)
}
get isPortable() {
return false;//can't take it by defualt
}
get parent() {
return this._parent; //using a private variable so that there's no infinite loop
}
set parent(newparent) {
if (this._parent === newparent) return; //stop if the parent is already what it was trying t oeb set to
const oldparent = this._parent;
this._parent = newparent;
if (oldparent) { //if there was an old parent, remove it
oldparent.removechild(this);
}
if (newparent) { //if there's a new parent, add it
newparent.addchild(this);
}
}
get children() {
return [...this._children];//... means to makea copy. we need that so that if someone tries to mess with the child list directly, it doesn't mess with our internal list.
}
addchild(child) {
if (!this._children.includes(child)) {//only run if this isn't already the case
this._children.push(child); //adds the child to the children list
child.parent = this; // 'this' is the entity that is the paprent (th8is sets the child's parent)
}
}
removechild(child) {
const index = this._children.indexOf(child);
if (index !== -1) {
this._children.splice(index, 1);
child.parent = null;
}
}
}
class Dungeon extends Entity { //extends means it's a modified version of the entity class. super(); means to just do the original thing from the normal entity class.
constructor() {
super();
}
get parent() {
return super.parent; //the crown picking up/leaving doesn't work without this (including in the other entity classes)
}
set parent(newparent) {
if (newparent !== null) {
throw new Error("the parent of a dungeon entity must always be null.");
}
super.parent = newparent;
}
}
class Level extends Entity {
constructor(mapgrid) {
super();
this.map = mapgrid;
this.depth = 1;//first level is at the top
}
get parent() {
return super.parent;
}
set parent(newparent) {
if (newparent !== null && !(newparent instanceof Dungeon)) {
throw new Error("the parent of a level must be a dungeon.");
}
super.parent = newparent;
}
}
class Player extends Entity {
constructor() {
super();
this.name = "";
this.x = 0;
this.y = 0;
this.maxHp = 100;
this.hp = 100;// max by default
this.atk = 15;//default attack value
this.def = 5;//defense shield
this.memory = new Map();//bool grid of the player's memory
this.equipment = new Map();//mapping items to their slots for the player
this.handItems = [];//items held in hands since there can be two one-handed items
this.traits = [];//chosen traits from element
this.element = null;//element
}
applyTrait(traitKey)//applying trait to the player
{
const trait = traits[traitKey];//look up the trait info
if (!trait) return;//failsafe
this.traits.push(traitKey);//remember which trait was chosen
this.element = trait.element;//element that comes from the trait
//apply the stat bonuses
this.maxHp = this.maxHp + trait.maxHpBonus;
this.hp = this.hp + trait.maxHpBonus;
this.atk = this.atk + trait.atkBonus;
this.def = this.def + trait.defBonus;
}
cantWeildItem(item)//some traits forbid items
{
const itemClassName = item.constructor.name;//takes the name of the passed in item
for (const traitKey of this.traits)//check every trait the player has
{
const trait = traits[traitKey];
if (trait && trait.cantWeild.includes(itemClassName))
{
return true;//forbidden
}
}
return false;//no trait forbids it
}
damageMultiplierAgainst(target) {
const targetElement = target.element;
if (this.element === null || this.element === undefined || targetElement === null || targetElement === undefined)
{
return 1;
}
return effectiveness[this.element][targetElement] || 1;//default 1 if something's missing
}
get symbol() {
return "@";
}
get renderPriority() {
return 999999;//basically always displayed
}
get parent() {
return super.parent;
}
set parent(newparent) {
if (newparent !== null && !(newparent instanceof Level)) {
throw new Error("the parent of a player must be a level.");
}
super.parent = newparent;
}
}
class UpStairway extends Entity {
constructor()
{
super();
this.correspondingStaircase = null;//starts unlinked but will be used to create the relation between staircases
}
get symbol() {
return "<";//stair
}
get renderPriority() {
return 10;//less than player
}
get parent() {
return super.parent;
}
set parent(newparent) {
if (newparent !== null && !(newparent instanceof Level))
{
throw new Error("must be a level");
}
super.parent = newparent;
}
}
class DownStairway extends Entity//to go to the new levels
{
constructor()
{
super();
this.correspondingStaircase = null;
}
get symbol() {
return ">"; // the down staircase symbol
}
get renderPriority() {
return 10; // same priority as up stairway, below player (999999)
}
get parent() {
return super.parent;
}
set parent(newparent) {
if (newparent !== null && !(newparent instanceof Level)) {
throw new Error("must be a level");
}
super.parent = newparent;
}
}
class Item extends Entity{
get isPortable() {
return true;
}
//what slot the thing takes
get slot() {
return null;
}
//for weapons only (1 by default)
get hands() {
return 1;
}
//how much attack this item gives while equipped (0 by default)
get atkBonus() {
return 0;
}
//how much defense this item gives while equipped (0 by default)
get defBonus() {
return 0;
}