-
Notifications
You must be signed in to change notification settings - Fork 1
/
impossible-game.js
2257 lines (2027 loc) · 78.6 KB
/
impossible-game.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
//Inspired By games like Mario and Geometry Dash.
function positivMod(n, mod) {
return ((n % mod) + mod) % mod;
}
function keepNDec(x, nDec) {
/* "round" a number to its nDec decimal */
return Math.round(x * 10 ** nDec) / 10 ** nDec;
}
class point {
constructor(x, y) {
/* Point are coded ny their two coordinates */
this.x = x;
this.y = y;
}
copy() {
return new point(this.x, this.y);
}
sameAbsciss(other) {
/* return true if this and other have the same abscissa */
return this.x === other.x;
}
sameOrdinate(other) {
/* return true if this and other have the same ordinate */
return this.y === other.y;
}
equal(other) {
/* return trus if this and other are equal (same abscissa and odinate) */
return this.sameAbsciss(other) && this.sameOrdinate(other);
}
distance(other) {
/* return the distance between this and other */
return Math.sqrt((this.x - other.y) ** 2 + (this.y - other.y) ** 2);
}
addVector(v) {
/* compute a new point which is the sum of this and a vector v. v is an elemnt of class vector */
let res = new point(this.x + v.x, this.y + v.y);
return res;
}
translate(v) {
/* translate point this of vector v. contrary to addVector : the method change directly
the attribute of the point and return nothing */
this.x += v.x;
this.x = keepNDec(this.x, 10);
this.y += v.y;
this.y = keepNDec(this.y, 10);
}
}
class vector {
constructor(M, N) {
/* M and N can be :
- two points, in that case, vector coordinate are Difference of N and M coordinates
- two coordinates
*/
if (M instanceof point) {
this.x = N.x - M.x;
this.y = N.y - M.y;
} else {
/* vectors are coded ny their two coordinates */
this.x = M;
this.y = N;
}
}
is0() {
/* return true if the vector is null vector */
return this.x === 0 && this.y === 0;
}
sum(other) {
/* return the sum of this and another vector v */
let res = new vector(this.x + other.x, this.y + other.y);
return res;
}
product(lambda) {
/* return the external product of this per lambda*/
let res = new vector(lambda * this.x, lambda * this.y);
return res;
}
scalarProduct(other) {
/* compute the scalar product of this and other */
return this.x * other.x + this.y * other.y;
}
norm() {
/* compute the norm of this */
return Math.sqrt(this.scalarProduct(this));
}
orthogonalVector() {
/* Give the unique direct orthongonal vector of this with the same norm */
let res = new vector(-this.y, this.x);
return res;
}
polarCoordinate() {
/* Compute the polar coordintes of this */
return [this.norm(), Math.atan(this.y / this.x)];
}
}
class straightLine {
constructor(point1, element) {
if (element instanceof point) {
if (!point1.equal(element)) {
this.point1 = point1;
this.point2 = element;
} else {
throw "A straight line can't be definied by two identical points";
}
} else {
if (!element.is0()) {
this.point1 = point1;
this.point2 = point1.addVector(element);
} else {
throw "A straight line can't be definied by a null vector";
}
}
}
equation() {
/* return the equation of the straight line on the form of and array od three members. Equation ax+by+c = 0
is coded by [a,b,c] */
if (this.point1.sameAbsciss(this.point2)) {
return [1, 0, -this.point1.x];
} else {
let direction =
(this.point1.y - this.point2.y) / (this.point1.x - this.point2.x);
let ordinateOrigin = this.point1.y - direction * this.point1.x;
return [-direction, 1, -ordinateOrigin];
}
}
containPoint(point) {
let equationLine = this.equation();
return (
keepNDec(
equationLine[0] * point.x + equationLine[1] * point.y + equationLine[2],
10
) === 0
);
}
}
class segment {
constructor(point1, point2) {
this.point1 = point1;
this.point2 = point2;
}
center() {
/* return the center of the segment */
let res = new point(
(this.point1.x + this.point2.x) / 2,
(this.point1.y + this.point2.y) / 2
);
return res;
}
containPoint(point) {
let vector1 = new vector(this.point1, this.point2);
let vector2 = new vector(this.point1, point);
let scalarProd1 = vector1.scalarProduct(vector2);
let scalarProd2 = vector1.scalarProduct(vector1);
return scalarProd1 >= 0 && scalarProd1 <= scalarProd2;
}
intersect(other) {
let segmentVector = new vector(this.point1, this.point2);
let vector1 = new vector(this.point1, other.point1);
let vector2 = new vector(this.point1, other.point2);
let vector3 = new vector(this.point2, other.point1);
let vector4 = new vector(this.point2, other.point2);
let scalarProd1 = segmentVector.scalarProduct(vector1);
let scalarProd2 = segmentVector.scalarProduct(vector2);
let scalarProd3 = segmentVector.scalarProduct(vector3);
let scalarProd4 = segmentVector.scalarProduct(vector4);
return !(
(scalarProd1 < 0 && scalarProd2 < 0) ||
(scalarProd3 > 0 && scalarProd4 > 0)
);
}
}
class polygon {
constructor(vertices) {
this.vertices = vertices;
}
copy() {
let newPolygon = new polygon([]);
this.vertices.forEach((point) => {
newPolygon.push(point.copy());
});
return newPolygon;
}
translate(translationVector) {
/* tranlaction of the polygon of vector v. Methods don't return new polygon, but modify directly the attributes*/
this.vertices.forEach((point) => {
point.translate(translationVector);
});
}
edges() {
/* return the list of edges of the polygon */
let edges = [];
let nbVertices = this.vertices.length;
if (nbVertices > 2) {
for (let k = 0; k < nbVertices; k++) {
let edge = new segment(
this.vertices[k],
this.vertices[(k + 1) % nbVertices]
);
edges.push(edge);
}
} else {
let edge = new segment(this.vertices[0], this.vertices[1]);
edges.push(edge);
}
return edges;
}
isoBarycenter() {
/* return ths isoBarycenter (with coefficients = 1) of the polygon */
let barycenterAbscissa = 0;
let barycenterOrdinate = 0;
this.vertices.forEach((point) => {
barycenterAbscissa += point.x;
barycenterOrdinate += point.y;
});
let res = new point(
(1 / this.vertices.length) * barycenterAbscissa,
(1 / this.vertices.length) * barycenterOrdinate
);
return res;
}
static separation(other, edge, barycenter) {
let otherNbVertices = other.vertices.length;
let segmentLine = new straightLine(edge.point1, edge.point2);
let equation = segmentLine.equation();
let thisSide =
equation[0] * barycenter.x + equation[1] * barycenter.y + equation[2];
let pointSideSet = [];
let pointSide = [];
let pointOnSepartor = [];
for (let k = 0; k < otherNbVertices; k++) {
pointSide =
equation[0] * other.vertices[k].x +
equation[1] * other.vertices[k].y +
equation[2];
pointSideSet.push(pointSide);
if (keepNDec(pointSide, 10) === 0) {
pointOnSepartor.push(other.vertices[k]);
}
}
let commonPoint = false;
if (pointOnSepartor.length == 1) {
if (edge.containPoint(pointOnSepartor[0])) {
commonPoint = true;
}
} else if (pointOnSepartor.length == 2) {
let alignSegment = new segment(pointOnSepartor[0], pointOnSepartor[1]);
if (edge.intersect(alignSegment)) {
commonPoint = true;
}
}
if (commonPoint) {
return false;
} else {
let minPointSide = Math.min.apply(Math, pointSideSet);
let maxPointSide = Math.max.apply(Math, pointSideSet);
if (keepNDec(thisSide, 10) == 0) {
return keepNDec(minPointSide, 10) * keepNDec(maxPointSide, 10) >= 0;
} else {
return (
keepNDec(thisSide, 10) * keepNDec(maxPointSide, 10) <= 0 &&
keepNDec(minPointSide, 10) * keepNDec(thisSide, 10) <= 0
);
}
}
}
sat(other) {
/* Separating Axes Theorem (S. Gottschalk. Separating axis theorem. Technical Report TR96-024,Department
of Computer Science, UNC Chapel Hill, 1996) :
Two convex polytopes are disjoint iff there exists a separating axis orthogonal
to a face of either polytope or orthogonal to an edge from each polytope.
Our version of sat can also sepate segments which are degenerate polygons.
Be carrefull : work only for convex polygons.
*/
let thisEdges = this.edges();
let otherEdges = other.edges();
let thisBarycenter = this.isoBarycenter();
let otherBarycenter = other.isoBarycenter();
let isSeparated = false;
let cpt = 0;
do {
/* try to find a separator wicth edge of this */
isSeparated = polygon.separation(other, thisEdges[cpt], thisBarycenter);
cpt++;
} while ((cpt < thisEdges.length) & !isSeparated);
if (!isSeparated) {
/* if no edges of this ae separting, one try with edges of other */
cpt = 0;
do {
isSeparated = polygon.separation(
this,
otherEdges[cpt],
otherBarycenter
);
cpt++;
} while ((cpt < otherEdges.length) & !isSeparated);
}
return isSeparated;
}
}
class square extends polygon {
/* A square extend polygon class. Square attributes are
- a set of 4 points
- a center : the barycenter of the square
- a direction which is polar coordinates of the first edge of the square.
Two last attribute are commod in order to rotate the square according to its center.*/
constructor(element1, element2) {
/* there is tw way to constructs a square :
- Given the coordinates of the two limit point of one of its edge. In this case, the other point
are construct in direct order : edge 2 direction is edge 1 direction rotate from pi/2
- given its center and the polar coordinates of one of its edge. In this case, the other point
are construct in direct order : edge 2 direction is edge 1 direction rotate from pi/2. polar coordinate
is an array of a positive number (the length of the edge), and an angle in randiant*/
let point1;
let point2;
let point3;
let point4;
let direction;
let polarDirection;
let diagonal;
let center;
if (element2 instanceof point) {
point1 = element1;
point2 = element2;
direction = new vector(point1, point2);
polarDirection = direction.polarCoordinate();
point3 = point2.addVector(direction.orthogonalVector());
point4 = point3.addVector(
direction.orthogonalVector().orthogonalVector()
);
diagonal = new segment(point1, point3);
center = diagonal.center();
} else {
polarDirection = element2;
direction = new vector(
polarDirection[0] * Math.cos(polarDirection[1]),
polarDirection[0] * Math.sin(polarDirection[1])
);
// create a first square with good direction, and first point = (0,0)
point1 = new point(0, 0);
point2 = point1.addVector(direction);
point3 = point2.addVector(direction.orthogonalVector());
point4 = point3.addVector(
direction.orthogonalVector().orthogonalVector()
);
// translate the square to the good position
diagonal = new segment(point1, point3);
let initialCenter = diagonal.center();
center = element1;
let translationVector = new vector(initialCenter, center);
point1.translate(translationVector);
point2.translate(translationVector);
point3.translate(translationVector);
point4.translate(translationVector);
}
super([point1, point2, point3, point4]);
this.center = center;
this.polarDirection = polarDirection;
}
copy() {
let newSquare = new square([
this.vertices[0].copy(),
this.vertices[0].copy(),
]);
newSquare.center = this.center.copy();
newSquare.polarDirection = this.polarDirection.slice();
return newSquare;
}
rotate(angle) {
/* rotate the square according to its center. angle is in radiant */
this.polarDirection[1] += angle;
let direction = new vector(
this.polarDirection[0] * Math.cos(this.polarDirection[1]),
this.polarDirection[0] * Math.sin(this.polarDirection[1])
);
this.vertices[0] = new point(0, 0);
this.vertices[1] = this.vertices[0].addVector(direction);
this.vertices[2] = this.vertices[1].addVector(direction.orthogonalVector());
this.vertices[3] = this.vertices[2].addVector(
direction.orthogonalVector().orthogonalVector()
);
let diagonal = new segment(this.vertices[0], this.vertices[2]);
let initialCenter = diagonal.center();
let translationVector = new vector(initialCenter, this.center);
this.vertices[0].translate(translationVector);
this.vertices[1].translate(translationVector);
this.vertices[2].translate(translationVector);
this.vertices[3].translate(translationVector);
}
translate(transactionVector) {
// extend class translate of polygon by translative square center plus the points or the polygon
super.translate(transactionVector);
this.center.translate(transactionVector);
}
getLowestPointIndex() {
// indicate the index of the point(s) of minimal ordinates of the the square. If two points, we return
// first the point with lowest abscissa.
let lowestPoint = new point(Infinity, Infinity);
for (let k = 0; k < this.vertices.length; k++) {
if (keepNDec(this.vertices[k].y, 6) < keepNDec(lowestPoint.y, 6)) {
// comparision are mad with 6 decimal to avoid precision error of java script
lowestPoint = this.vertices[k];
}
}
let res = [];
for (let k = 0; k < this.vertices.length; k++) {
if (keepNDec(lowestPoint.y, 6) === keepNDec(this.vertices[k].y, 6)) {
// comparision are mad with 6 decimal to avoid precision error of java script
res.push(k);
}
}
if (res.length == 2) {
// return lowest point by abscissa order
if (this.vertices[res[0]].x > this.vertices[res[1]].x) {
res = [res[1], res[0]];
}
}
return res;
}
}
/*****************************************************************************************
* Games elements classes
*
* I have organize the game in three principal elements :
* - the heros element : on class which contains all the method of hero
* - the grid element which contain all elements about the level. There is more than one
* class : some small class for element as peak or platform, and a class grid
/*****************************************************************************************
/* Hero
One classe which contains all methods to manage the hero. It espaciallycontains method
move which compute new position of the hero after a frame.
*/
class hero {
constructor(
positionCenter,
positionPolarCoordinates,
vx,
vy0,
xJump,
yJump,
g,
t,
isJumping
) {
/* a hero is the set of a body which is a square, and foot which will be usefull to test if the hero land
on a block (the foot touch the roof of the block) or not. foot is and array of segments declared a
polygon to apply them sat algorithm. If the square is horizontal, the array contains only one segment : the
lowest, else it contains two segments : those around the lowest point */
/* Body hitBox */
let intialPosition = new point(positionCenter[0], positionCenter[1]);
this.body = new square(intialPosition, positionPolarCoordinates.slice());
/* foot hitBox */
let footPoint = this.body.getLowestPointIndex();
if (footPoint.length == 2) {
// In that case, the first point of the foot if the leftest because of mthode getLowestPointIndex
let footPoint1 = this.body.vertices[footPoint[0]].copy();
let footPoint2 = this.body.vertices[footPoint[1]].copy();
let foot1 = new polygon([footPoint1, footPoint2]);
this.foot = [foot1];
} else {
// In that case, the first point of the two foots is the lowest of the square
let footPoint1 = this.body.vertices[footPoint[0]].copy();
let footPoint2 =
this.body.vertices[positivMod(footPoint[0] + 1, 4)].copy();
let footPoint3 =
this.body.vertices[positivMod(footPoint[0] - 1, 4)].copy();
let foot1 = new polygon([footPoint1, footPoint2]);
let foot2 = new polygon([footPoint1.copy(), footPoint3]);
this.foot = [foot1, foot2];
}
/* Physical attributes
Those parameters are computed in order the hero do a jump of xJump unity heigh and yJump unity long.
We use classical newtonian physic for the trajectories which says that he gravity center of the hero
follow the next trajectory (with t=0 as begning of a jump)
x(t) = vx * t
y(t) = -1/2*g*t^2 + vy0 * t + y0
where
- vx is the horizontal speed or the hero : in the game it is a constant, so no need to take vx(0)
- g id the gravitional constant
- vy0 is the initial vertical speed when jump.
- y0 is the initial y coordinate of the center of the hero
This formula work equaly when the hero fall from a bloc (in that case vz0=0) and when the hero is on a
bloc (in that case, g and vy0 = 0).
On the game, vx is fixed, and we choose g and vy0 so that a jump is xJump long and yJump height.
xJump and yJump are fixed too
*/
this.vx = vx; // horizontal speed of the hero
this.vy0 = vy0; // vertical speed. when jump : (2*this.zJump)/((this.xJump/(2*this.vx))
this.xJump = xJump; // length of a jump
this.yJump = yJump; // height of a jump
this.g = g; // gravitional constant. value when jump or fall from a platform : (2*this.zJump)/((this.xJump/(2*this.vx))**2) ;
this.t = t; // counter of time when the hero begin jumping or falling in order to use equation. must be 0 at the begning of a jump or fall
this.isJumping = isJumping;
// use to avoid the gamer to do multiple jump, when jump, it become true and become false only when the hero land
this.startJumpPosition = this.body.center.copy();
/* hero status
code différent important state of the hero
*/
this.hasStarted = false; // if the game has not started
this.isDead = false; // for game over
this.haveFinished = false; // for win
/* For hero death animation
When the hero die, it become a set of particle. Those particles re set in setParticules method
*/
this.deathParticle = [];
}
rotate(angle) {
/* rotate the hero : body + foot */
this.body.rotate(angle);
let footPoint = this.body.getLowestPointIndex();
if (footPoint.length == 2) {
let footPoint1 = this.body.vertices[footPoint[0]].copy();
let footPoint2 = this.body.vertices[footPoint[1]].copy();
let foot1 = new polygon([footPoint1, footPoint2]);
this.foot = [foot1];
} else {
let footPoint1 = this.body.vertices[footPoint[0]].copy();
let footPoint2 =
this.body.vertices[positivMod(footPoint[0] + 1, 4)].copy();
let footPoint3 =
this.body.vertices[positivMod(footPoint[0] - 1, 4)].copy();
let foot1 = new polygon([footPoint1, footPoint2]);
let foot2 = new polygon([footPoint1.copy(), footPoint3]);
this.foot = [foot1, foot2];
}
}
translate(transactionVector) {
/* translate the hero : body + foot */
this.body.translate(transactionVector);
this.foot.forEach((footValue) => {
footValue.translate(transactionVector);
});
}
footContactWithRoof(previousFoot, platformInstance) {
/* Compute collision to know if the hero has land. The hero land on a platform if its foots have collisioned (do that word exists ?)
with platforms roof (see class plateform). However, because the game is not continuous but dicrete, and
that roof and foot are lines, it could arive that at t, foot is above a roof and a t+1, it is below. In that
case, the game will consider there is no collision. To avoid that. We construct a footPolygon which is the
polygon obtain by contatenate previous foot position and next foot position. Thank to that, if
footPolygon have collision wich the roof, it means that bettween t and t+1, the hero have land on the roof. */
let cpt = 0; // if at the end cpt > 0, there is a contact
let footPolygon;
for (let k = 0; k < this.foot.length; k++) {
let lineTest = new straightLine(
this.foot[k].vertices[1],
previousFoot[k][0]
);
/* Test if roof polygon is flat. In that case, footPolygon is a segment, and in order sat method of polygon work
one need to keep only two points. sat work only if there is no more than two points align in polygon*/
if (lineTest.containPoint(previousFoot[k][1])) {
footPolygon = new polygon([
this.foot[k].vertices[1],
previousFoot[k][0],
]);
/* only arrive when the hero have rectiling movment : when it is horizontal and placed on the ground.
In that case, foot have only one element and first point of the polygon is the leftest. So the segment
as defined is the larger possible segment*/
} else {
footPolygon = new polygon([
this.foot[k].vertices[0],
this.foot[k].vertices[1],
previousFoot[k][1],
previousFoot[k][0],
]);
}
/* the order of point is important. When moving, each point is translate from the same vector, so we need
to choose order of the point to not have cross polygon */
if (!footPolygon.sat(platformInstance.roof)) {
/* if one collision : it means that the hero land, we conslude verifying cpt > 0 */
cpt++;
}
}
return cpt > 0;
}
move(gridInstance) {
/* The gravity center of the hero follow the next trajectory (with t=0 as begning of a jump)
x(t) = vx * t
y(t) = -1/2*g*t^2 + vy0 * t + y0
where
- vx is the horizontal speed or the hero : in the game it is a constant, so no need to take vx(0)
- g id the gravitaional constant
- vy0 is the initial vertical speed when jump.
- y0 is the initial y coordinate of the center of the hero
This formula work equaly when the hero fall from a bloc (in that case vz0=0) and when the heri is on a bloc (in that case, g and vy0 = 0).
On this methode, we consider finite diffrecne of this equation : y(t+dt) - y(t) and x(t+dt) - x(t). Because of the
quadratiq nature of equation 2, the time t still appears in equation for y, and so we can't keep only dt value, we
need to use t.
On the game, vx is fixed, and we choose g and vz0 so that a jump is xJump long and yJump height. xJump and zJump are fixed too
See grid class to know what is gridInstance
*/
let previousFoot = [];
this.foot.forEach((foot) => {
previousFoot.push([foot.vertices[0].copy(), foot.vertices[1].copy()]);
});
// save previous foot to verify if the hero land (see method footContactWithRoof)
let tSauv = this.t;
let yPosSauv = this.body.center.y;
let xPosSauv = this.body.center.x;
// use to rectify position when landing
let dt = frameTimeDiff.dt;
// time interval between to frames
let translationVector = new vector(
this.vx * dt,
Math.max((-1 / 2) * this.g * dt * (2 * this.t + dt) + this.vy0 * dt, -1)
);
// limit y-translation to avoid the hero to go throught the floor
this.translate(translationVector);
// translate the hero with finite diffrence equation
this.t += dt;
// for next step, t become t+dt. If the movement is not a jump or a fall, t will be set to 0 next. Indeed
// t is not mandatory for rectilign horizontal movement, and that way, t will be 0 at the begning of a jump
// or a fall as expected.*/
/* Grid interaction check : We test collision of the hero with its neihbourg envirenement.
- collision with a peak = dead
- collision with a platform = dead if not collisions with the floor of the platform
*/
// Some times, the hero could touch for example a roof and a peak but the peak is same level (or above
// because of discret nature of a game). In that case, the user don't loose. To check that, we introduce
// the two following arrays
let deadContactElementCenter = [];
// if touch a death element, we add in that array the value of the center of element. At the end, if the
// hero didn't touch a floor above those coordintes, the hero die
let floorContactElementCenter = [];
// if touch a floor element, we add in that array the value of the center of element. At the end, if the
// hero didn't touch a floor above those coordintes, the hero die
let aroundGrid = gridInstance.grid.slice(
Math.max(Math.floor(this.body.center.x - 1), 0),
Math.floor(this.body.center.x + 2)
);
// only test element that the hero can touch, ie elemnts in the neighbourhood
aroundGrid.forEach((col) => {
// one test contact for each potential elements
if (col != undefined) {
// if columns grid is empty
col.forEach((element) => {
if (element instanceof platform) {
if (!this.body.sat(element.platform)) {
// if touch platform, test if touch the roof : if yes, we add an element in floorContactElementCenter,
// else in deadContactElementCenter
if (this.footContactWithRoof(previousFoot, element)) {
floorContactElementCenter.push(element.platform.center.y);
} else {
deadContactElementCenter.push(element.platform.center.y);
}
}
} else if (element instanceof peak) {
// if touch peak, it's always a dead contact
if (!this.body.sat(element.peak)) {
deadContactElementCenter.push(element.center.y);
}
} else if (element instanceof ending) {
// if touch finish, it's the end
if (!this.body.sat(element.ending)) {
this.haveFinished = true;
}
}
});
}
});
let maxDeadContactCenter, maxFloorContactCenter;
// max of the deadContactElementCenter and floorContactElementCenter. if maxFloorContactCenter > maxDeadContactCenter
// it's not game over, else it is
if (deadContactElementCenter.length > 0) {
maxDeadContactCenter = deadContactElementCenter.reduce(function (a, b) {
return Math.max(a, b);
});
} else {
maxDeadContactCenter = -Infinity;
}
if (floorContactElementCenter.length > 0) {
maxFloorContactCenter = floorContactElementCenter.reduce(function (a, b) {
return Math.max(a, b);
});
} else {
maxFloorContactCenter = -Infinity;
}
if (deadContactElementCenter.length > 0) {
if (floorContactElementCenter.length === 0) {
this.isDead = true;
} else {
if (maxDeadContactCenter > maxFloorContactCenter) {
this.isDead = true;
}
}
}
if (this.body.center.y < 0) {
/* The lower set of roof have coordinate 1, if the square fall under it, it means that it falls in a hole */
this.isDead = true;
let newCenter = new point(this.body.center.x, 0.5);
let translateVector = new vector(this.body.center, newCenter);
this.translate(translateVector);
}
if (!this.isDead) {
if (floorContactElementCenter.length > 0) {
// begin by adjust x position of the hero when contact
let newXPosition;
if (this.isJumping) {
// If landing, the hero could be below the roof. We begin by rectify its position in searching dt such
// the ordintate hero psition when landing is maxFloorContactCenter+1 y positin
let a = -this.g / 2;
let b = this.vy0 - this.g * tSauv;
let c = -(maxFloorContactCenter + 1 - yPosSauv);
let delta = b ** 2 - 4 * a * c;
let newDt = Math.max(
(-b - Math.sqrt(delta)) / (2 * a),
(-b + Math.sqrt(delta)) / (2 * a)
);
newXPosition = xPosSauv + newDt * this.vx;
} else {
// if just moving, no modification
newXPosition = this.body.center.x;
}
let newCenter = new point(newXPosition, maxFloorContactCenter + 1);
let translateVector = new vector(this.body.center, newCenter);
this.translate(translateVector);
// if the hero if below the roof, e replace it on it
this.rotate(2 * Math.PI - this.body.polarDirection[1]);
// if the hero is rotated, we reput it right
this.g = 0; // g is compensated by newton 3rd law
this.vy0 = 0; // the vertical movment stop
this.t = 0; // t is not mandatory in equation anymore, and will be 0 at the begning of a jump or a fall
this.isJumping = false; // the gamer can jump
} else {
this.g = (2 * this.yJump) / (this.xJump / (2 * this.vx)) ** 2; // no compensation by newton 3rd law
this.rotate(
-Math.PI / (((1 / frameTimeDiff.dt) * this.xJump) / (2 * this.vx) + 2)
); // to look pretty : the hero rotate when not on a roof
this.isJumping = true; // can't jump before the end of the jump /fall
}
}
}
jump() {
/* Modify physical constant in order to the next move (methode move) is a jump */
if (!this.isJumping) {
this.isJumping = true; // during a jump, the hero can't jump anymore
// can't jump if already jumping
this.g = (2 * this.yJump) / (this.xJump / (2 * this.vx)) ** 2; // no compensation by newton 3rd law
this.vy0 = (2 * this.yJump) / (this.xJump / (2 * this.vx)); // to look pretty : the hero rotate when not on a roof
this.t = 0; // at the begning of a jump, t must be = 0 in order to the equation are ok
this.startJumpPosition = this.body.center.copy();
}
}
setDeathParticle() {
/* When death, create explosion particles */
for (let k = 0; k < 40; k++) {
this.deathParticle.push({
position: this.body.center.copy(),
angle: 2 * Math.PI * Math.random(),
maxProjection: 2 * Math.random(),
});
}
}
}
/* Level elements :
4 kind of grid elements :
- platform on which one can land, or crash if foot hero don't touch platform roof
- peak on which one can die
- ending
- checkpoints
*/
class platform {
/* A platform is
- a square of edge length 1 and angle 0.
- the the roof which is the upper edge of the square. It is used to verify if the hero land on the platforme :
the foot and the roof enter in collision, or not (in that case, if collision, it's game over)
For each element one add col = floor(x-positon) attribute. Elements will
be organized on a grid which is an array. In array cell n, we will place all
element which col = n. That way, it is easy to get all element of the neibourhood
of the hero to test collisions
*/
constructor(x, y) {
/* x,y are the coordinates of the lowest leftest point of the platform */
this.col = Math.floor(x);
let platformCenter = new point(x + 1 / 2, y + 1 / 2);
this.platform = new square(platformCenter, [1, 0]);
this.roof = new polygon([
this.platform.vertices[2],
this.platform.vertices[3],
]);
}
}
class peak {
/* peak is a triangle. There is 4 kind of peak according to there orientation.
For each element one add col = floor(x-positon) attribute. Elements will
be organized on a grid which is an array. In array cell n, we will place all
element which col = n. That way, it is easy to get all element of the neibourhood
of the hero to test collisions
*/
constructor(x, y, orientation) {
/* x, y are the coordinates of the lowest leftest point of the square in which the triangle is inscribed */
let point1, point2, point3;
switch (orientation) {
case "up":
point1 = new point(x, y);
point2 = new point(x + 1, y);
point3 = new point(x + 1 / 2, y + 1);
break;
case "down":
point1 = new point(x, y + 1);
point2 = new point(x + 1, y + 1);
point3 = new point(x + 1 / 2, y);
break;
case "left":
point1 = new point(x + 1, y + 1);
point2 = new point(x + 1, y);
point3 = new point(x, y + 1 / 2);
break;
case "right":
point1 = new point(x, y + 1);
point2 = new point(x, y);
point3 = new point(x + 1, y + 1 / 2);
break;
}
this.col = Math.floor(x);
this.peak = new polygon([point1, point2, point3]);
this.center = new point(x + 1 / 2, y + 1 / 2);
}
}
class ending {
/* Ending is a unique element which indicate the end of the level
For each element one add col = floor(x-positon) attribute. Elements will
be organized on a grid which is an array. In array cell n, we will place all
element which col = n. That way, it is easy to get all element of the neibourhood
of the hero to test collisions
*/
constructor(position) {
/* Ending hitbox is a rectangle horizontal of width 1 and height 10. Position is the abscissa of left edge */
let point1 = new point(position, 0);
let point2 = new point(position + 1, 0);
let point3 = new point(position + 1, 10);
let point4 = new point(position, 10);
this.ending = new polygon([point1, point2, point3, point4]);
this.col = Math.floor(position);
}
}
class checkPoint {
/* checkPoints are elemnt places by the gamer to save the state of the game
For each element one add col = floor(x-positon) attribute. Elements will
be organized on a grid which is an array. In array cell n, we will place all
element which col = n. That way, it is easy to get all element of the neibourhood
of the hero to test collisions
*/
constructor(heroInstance) {
/* the position of the checkpoint depends of the position of the hero */
this.x = heroInstance.body.center.x;
this.y = heroInstance.body.center.y;
this.col = Math.floor(this.x);
}
update(heroInstance) {
this.x = heroInstance.body.center.x;
this.y = heroInstance.body.center.y;
this.col = Math.floor(this.x);
}
}
class grid {
/* grid is a discretisation of the level. All element have a col value which is the floor of there x-position.
Those element will be organized on the grid which is an array. In array cell n, we will place all element
which col = n. That way, it is easy to get all element of the neibourhood
of the hero to test collisions*/
constructor() {
/* by default a grid is empty, we add element using methodes */
this.grid = [];
}
addPlatform(x, y) {
let platformInstance = new platform(x, y);
/* Given a platform, place a platform in the grid */
if (this.grid[platformInstance.col] != undefined) {
this.grid[platformInstance.col].push(platformInstance);
} else {
this.grid[platformInstance.col] = [platformInstance];