-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmot-game-tutorial.js
executable file
·2411 lines (2070 loc) · 116 KB
/
mot-game-tutorial.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
jsPsych.plugins["mot-game-tutorial"] = (function() {
var plugin = {};
plugin.info = {
name: 'mot-game',
parameters: {
}
}
plugin.trial = function(display_element, trial) {
var par = trial
var numRegBalls = 1
var numExBalls = 0 ///hard coding these since it's the tutorial and will need a specific number of reg/exploding robots. RObots are called balls in this game because they were originally balls before more game design
var w=par.gameWidth, h=par.gameHeight;
display_element.innerHTML =
//"<img src='robomb-pngs/top-bar.png' style='position: absolute; left: 50%; margin-right:50%; transform: translate(-50%, 0); vertical-align: middle; width: " + w + "'><br /><br /><br /><br />" +
"<div id='gameContainer' style='position: absolute; top: 50%; left: 50%; margin-right:50%; transform: translate(-50%, -50%); height: " + h + "px; width: " + w + "px; vertical-align: middle'>" +
"<!-background image:--><img src='robomb-pngs/floor.png' height='" + h + "' width='" + w + "' style='position:absolute; margin:auto; z-index:-100'></img>" +
"<!--main canvas where game happens:-->" +
"<canvas id='mainCanvas' height='" + h + "' width = '" + w + "'></canvas>"
+
"<!--overlay canvas that doesn't need to be refreshed constantly:-->" +
//it may be good to not hard-code the top and left value but rather use variables...this will be decided later when we do more styling
"<canvas id='overlay' style='position:absolute; left: 0; top: 0; z-index:3' height='" + h + "' width = '" + w + "'></canvas>"
+
//it may be good to not hard-code the top and left value but rather use variables...this will be decided later when we do more styling
"<canvas id='occluderCanvas' style='position:absolute; left: 0; top: 0; z-index:2' height='" + h + "' width = '" + w + "'></canvas>" +
//canvas for timer display
"<canvas id='timerCanvas' style='position:absolute; left: 0; top: 0; z-index:5' height='" + h + "' width = '" + w + "'></canvas>" +
"<!--selection canvas for ball selection in defusal mode:-->" +
//it may be good to not hard-code the top and left value but rather use variables...this will be decided later when we do more styling
"<canvas id='selectionCanvas' style='position:absolute; left: 0; top: 0; z-index:1' height='" + h + "' width = '" + w + "'></canvas>" +
"<canvas id='livesCanvas' style='position:absolute; left: 0; top: 0; z-index:3' height='" + h + "' width = '" + w + "'></canvas>" +
"<div id='messageBox' style='width: 66%;top:50%; margin-left:50%; transform: translate(-50%, -50%); -moz-transform: translate(-50%, -50%); -webkit-transform: translate(-50%, -50%); -ms-transform: translate(-50%, -50%); -o-transform: translate(-50%, -50%); display:none; animation-name: messagePopUpAnimation; animation-duration: 4s; position:absolute; z-index:500; overflow: auto; user-select:none;'><img id='messageImg' src='robomb-pngs/alert-box.png' style='display:block; width:100%; margin: auto; pointer-events:none; user-select:none'></img><div id='msgText' style='position:absolute; width: 95%; top: 50%; margin-left:50%; transform: translate(-50%,-50%); -moz-transform: translate(-50%,-50%); -webkit-transform: translate(-50%,-50%); -ms-transform: translate(-50%,-50%); -o-transform: translate(-50%,-50%); font:28px Overpass, sans-serif; color: white; text-align: center; display:block'></div><div id='buttonDiv'></div>" +
"</div>" +
"<div id='bottomScreenText' style='display:none; animation-name: scrollIt; animation-duration: 12s; position:relative; top: -10%; width: 80%; margin-left: 10%; z-index:500; overflow: auto'><br /><div id='bottomText' style='positon: absolute; margin-left: 50%; color: #C6FDF9; font: 17px Overpass, sans serif'>You've held out until the robots could be quarantined. +1 life. However, they are set to go off soon. You have 10 seconds to defuse them by clicking the right ones. \nYou have one defusal kit per bomb, so don't waste any</div>'" +
"</div>" +
"<audio id='a-wallCreate' src='sounds/wall-create.wav'></audio>" + //wall creation sound. from: https://freesound.org/people/xixishi/sounds/265210/
"<audio id='a-collisionBwop' src='sounds/collision-bwop.mp3'></audio>" + //https://freesound.org/people/willy_ineedthatapp_com/sounds/167338/
"<audio id='a-correct' src='sounds/correct.wav'></audio>" + //https://freesound.org/people/Eponn/sounds/421002/
"<audio id='a-incorrect' src='sounds/incorrect.wav'></audio>" + //https://freesound.org/people/KevinVG207/sounds/331912/
"<audio id='a-wallHit' src='sounds/wall-hit.wav'></audio>" + //https://freesound.org/people/jeckkech/sounds/391668/
"<audio id='a-explosion' src='sounds/explosion.wav'></audio>" + //
"<audio id='a-teleport' src='sounds/teleport.wav'></audio>" + //https://freesound.org/people/fins/sounds/172206/
"<audio id='a-unteleport' src='sounds/unteleport.wav'></audio>" +// https://freesound.org/people/fins/sounds/172207/
//font:
//"<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Overpass+Mono:300,400,600,700|Overpass:100,100i,200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i&subset=latin-ext'> </link>" +
//message pop-up animation:
"<style>@keyframes fadeIn{from {opacity:0}; to {opacity:0.5}}</style> <style>@keyframes scrollIt{from {margin-left:-30%}; to {margin-left:30%}}</style> <style>@import url('https://fonts.googleapis.com/css?family=Overpass');</style>"
/*global variable declarations*/
var pressedSpace = false
var pressedR = false
var neverMadeAWallYet = true //for tutorial, to congratulate the user upon making the first wall
var timestampWallCreationEnabled = null
//happens when user presses space to continue or r to restart
var progressTutorial = function(e){
//document.removeEventListener('keyup', progressTutorial);
if(e.key == " " && !pressedSpace){
pressedSpace = true
notifyOfExplodingBalls()
}else if(e.key == "r" && !pressedR){
pressedR = true
data.restartTutorial = true
jsPsych.finishTrial(data)
}
}
document.body.style.backgroundColor = "black"
document.body.style.userSelect = "none"
document.body.style.MozUserSelect = 'none'
document.body.style.webkitUserSelect = 'none'
document.body.style.msUserSelect = 'none'
var data = {
levelDuration: par.duration,
timeDefusalStarted: 0,
defusalDuration: 0,
defusalMode: "neverNeeded", //"neverNeeded", "successful", "timeRanOut", or "incorrectGuess"
correctGuesses: null,
incorrectGuesses: null,
numWallsMade: 0,/*should it register each click?*/
numRegBalls: numRegBalls, //redundant
numExplodingBalls: numExBalls, //redundant with the following but makes life easier later for data analysis
ballInitialConditions: [], //an array of objects representing balls and their respective initial conditions
ballSpeed: par.ballSpeed,
maxObstacles: par.maxUserDefinedObstacles,
maxObstacleSegments:par.maxUserDefinedObstacleSegments,
maxSegmentLength:par.maxDistanceBetweenObstaclePixels,
minSegmentLength:par.minDistanceBetweenObstaclePixels,
createdPoints:[],
occluders: par.occluders,
occluderRectangles: par.occluderRectangles,
savedModel: par.savedModel,
//delete all the above that begin with par. and replace it with:
parameters: par
}
/*GAME CODE*/
var ballColor = "grey"
var distanceBetween = function(p1, p2){
var xdist = p2[0]-p1[0]
var ydist = p2[1]-p1[1]
return Math.sqrt(xdist*xdist + ydist*ydist)
}
//generates a random number according to Gaussian distribution with mean 0 and standard deviation SD. It uses the Box-Muller transform method.
//From https://stackoverflow.com/a/36481059
var randGaussian = function(mean, sd){
//they start at 0 so they're randomized anyway
var a=0, b=0;
//replace any 0 values with a random value becuase the BM transform uses the range (0,1)
while(a==0) a=Math.random();
while(b==0) b=Math.random();
var randWithMeanZeroAnSDOne = Math.sqrt(-2*Math.log(a)) * Math.cos(2*Math.PI*b)
return randWithMeanZeroAnSDOne * sd + mean
}
//point is in format [x,y]; rect is in format {x: , y: , width: , height: }
var pointIsWithinRectangle = function(point, rect){
var furthestRightCoordinate = rect.x + rect.width
var furthestDownCoordinate = rect.y + rect.height
if(point[0] >= Math.min(rect.x, furthestRightCoordinate) && point[0] <= Math.max(rect.x, furthestRightCoordinate) &&
point[1] >= Math.min(rect.y, furthestDownCoordinate) && point[1] <= Math.max(rect.y, furthestDownCoordinate))//Math.min and Math.max are used in case
//the rectangle has negative dimensions and
//furthest____Coordinate isn't actually
//the furthest _____ coordinate - it's the opposite
{
return true
} else {
return false
}
}
//pix has format [x,y]
function getPixelPositionRelativeToObject(pix, object, noTranslate) {
//noTranslate is for when the object is not translated via CSS. Translation is used to center elements by doing transform: translate(-50,-50), offsetting them by -width/2, -height/2
var translateLeft = (noTranslate === undefined || noTranslate == true) ? object.offsetWidth/2 : 0
var translateTop = (noTranslate === undefined || noTranslate == true) ? object.offsetHeight/2 : 0
var posx = pix[0]-object.offsetLeft + translateLeft
var posy = pix[1]-object.offsetTop + translateTop
//this resets any out-of-bounds pixels to within-bounds
if(posx >= object.width){posx = object.width-1}
if(posx <= 0){posx = 1}
if(posy >= object.height){posy = object.height-1}
if(posy <= 0){posy = 1}
return [posx, posy]
}
//adds a point much like a user click does, but with a fake mousedown event
function addReplayObstaclePointAfterTime (pt, time){
setTimeout(function(){
//fake "event" object (parentheses around it just to be sure the variables is super private since it's used by setTimeout and
//must not be changed before setTimeout finishes execution)
var event = {
type: pt.eventType,
x: pt.position[0],
y: pt.position[1],
isFromReplay: true
}
//mousedown events are recorded differently to the data and their position actually matches pageX and pageY coordinates. So they can be made accessible by pageX and pageY:
if(event.type == "mousedown"){
event.pageX = pt.position[0];
event.pageY = pt.position[1]
}
curLevel.model.addPixelsToUserObstacles(event)
}, time) //(the time created is measured after the gameplay part begins so initialFrameDuration is added)
}
function theLevel() { //now levels are based off parameters passed to jsPsych
var levelDuration = 15000;
var m = new model(numRegBalls,numExBalls,/*0.1*/par.ballSpeed)
var v = new view(model)
var c = new controller(m, v, levelDuration)
return new level(m, v, c, levelDuration)
}
var curLevel = theLevel();
/*var button = {
imgUp: 'robomb-pngs/btn-okay-up.png',
imgDn: 'robomb-pngs/btn-okay-down.png',
onClick: curLevel.view.closeAlertBox,
activateOnEnterOrSpace: true
}
curLevel.view.showAlertBox('test', [button])*/
function model(numNormalBalls, numExplodingBalls, speed) {
this.frozen = false //game pauses and model freezes
this.freeze = function(){this.frozen = true}
this.unFreeze = function(){this.frozen = false}
this.currentTime = 0
this.balls = []; //including exploding balls
this.getBalls = function(){return this.balls}
this.getExplodingBalls = function(){return this.explodingBalls}
this.removeBall = function(ball){
//remove it from explodingballs too if it's exploding:
if(ball.explosive){
this.explodingBalls.splice(this.explodingBalls.indexOf(ball),1)
setTimeout(function(){
if(curLevel.model.explodingBalls.length == 0 ){curLevel.controller.endGame("defusalModeNeverHappened")}
}, 400)
curLevel.model.balls.splice(this.balls.indexOf(ball),1)
}
}
this.explodingBalls = [];
this.occluderRects = par.occluderRectangles
this.getOccluderRects = function(){return this.occluderRects}
/*this.occluders = [new occluder("occluders/occluder1.png", w/3, h/2), new occluder("occluders/occluder1.png", 2*w/3, h/2)];*/
this.numExplodingBalls = function(){return this.explodingBalls.length}
this.lives = par.numLives
//usually callback is view.showLives()
this.decrementLives = function(callback){
this.lives--
if(par.exitWhenOutOfLives && this.lives <= 0){ jsPsych.finishTrial(data) }
callback();
}
this.incrementLives = function(callback){
//console.log(this.lives)
if(this.lives < par.maxLives){
this.lives++
}
callback();
}
var myself = this
this.resetAllBallDesigns = function(){
var balls = myself.getBalls()
for(var m=0, numBalls = balls.length; m<numBalls; m++){
balls[m].setImage(myself.defaultBallImg);//shrunken 5, 3, normal, -1
}
}
//initialize the balls:
this.ballRadius = 25 //default value
this.defaultBallImg = 'robomb-pngs/robot-normal.png' //same as 'small'
switch(par.ballRadius){
case 'smallest':
this.ballRadius = 12
this.defaultBallImg = 'robomb-pngs/shrunken-5.png'
break
case 'small':
this.ballRadius = 16
this.defaultBallImg = 'robomb-pngs/shrunken-3.png'
break
case 'large':
this.ballRadius = 25
this.defaultBallImg = 'robomb-pngs/robot-normal.png'
break
case 'largest':
this.ballRadius = 30
this.defaultBallImg = 'robomb-pngs/enlarged-1.png'
break
}
var randomCoordinatesForNormalBall = function(){
var horiMargin = w/6+myself.ballRadius
var vertMargin = h/4+myself.ballRadius
var x = Math.round(horiMargin + Math.random()*(w-2*horiMargin) - myself.ballRadius/2);
var y = Math.round(vertMargin + Math.random()*(h-2*vertMargin) - myself.ballRadius/2);
return [x,y]
}
var randomCoordinatesForExplodingBall = function(){
var horiMargin = w/4+myself.ballRadius
var vertMargin = h/3.4+myself.ballRadius
var x = Math.round(horiMargin + Math.random()*(w-2*horiMargin) - myself.ballRadius/2);
var y = Math.round(vertMargin + Math.random()*(h-2*vertMargin) - myself.ballRadius/2);
return [x,y]
}
/*now, initialize the balls. how that is done depends on whether it's replay mode
if(par.replayMode){
var balls = par.replayModeParameters.ballInitialConditions
//^these aren't actual ball objects; they're just the minimal data necessary to recreate the balls
//now, recreate the balls and add them to this.balls. ballInitialConditions is an array of "ball" objects
for(var i = 0, numBalls = balls.length; i < numBalls; i++){
var b = balls[i]
var explosiveParameter = b.explosive ? "e" : null //to be passed as parameter of whether to make the ball explosive. "e" makes the ball explosive
this.balls.push(new ball(b.position[0], b.position[1], b.radius, 0, explosiveParameter))
//now set the velocity of the ball (balls are initialized with speed, not velocity):
this.balls[this.balls.length-1/*most recently pushed ball*].setVelocity(b.velocity)
}
} else {*/
//now initialize balls, but make sure they aren't in occluders
for(var i = 0; i<numNormalBalls; i++){
//random x-y coordinates of a new ball:
var coords = randomCoordinatesForNormalBall()
//make sure the ball is in an acceptable place:
var badPosition = circleIsInAnOccluder(coords, this.ballRadius) || collidingWithBalls(coords, this.ballRadius, this.balls)
while(badPosition){
//reset the coordinates until the piwr and the coordinates are therefore valid
coords = randomCoordinatesForNormalBall()
badPosition = circleIsInAnOccluder(coords, this.ballRadius) || collidingWithBalls(coords, this.ballRadius, this.balls)
}
//make the ball (and add it to this.balls) if it's at a valid position:
this.balls.push(new ball(coords[0],coords[1],this.ballRadius, speed, i/*id is just i*/))
}
//initialize the exploding balls:
for(var i = 0; i<numExplodingBalls; i++){ //exploding balls can't be too close to the edges; that isn't fair
//random x-y coordinates of a new exploding ball:
var coords = randomCoordinatesForExplodingBall()
//make sure the ball isn't inside any occluders:
var badPosition = circleIsInAnOccluder(coords, this.ballRadius) || collidingWithBalls(coords, this.ballRadius, this.balls)
while(badPosition){
//reset the coordinates until they're not in the occluder
coords = randomCoordinatesForExplodingBall()
badPosition = circleIsInAnOccluder(coords, this.ballRadius) || collidingWithBalls(coords, this.ballRadius, this.balls)
}
var bal = new ball(coords[0], coords[1], this.ballRadius, speed, numNormalBalls + i/*explosive balls will have highest ids*/, "e")
this.balls.push(bal)
this.explodingBalls.push(bal)
}
/*Not using this inefficient wall type anymore:
/*MAKE SURE TO NOT MAKE THE WIDTHS NEGATIVE - THE wall.highestPoint, leftestPoint, etc. methods will not work if they
*are negative. It may be a good idea to make those methods work with negative values, but it seems that would be less
*efficient because more steps would be involved in those methods, which are called a lot. It also may be good to have
*the highestPoint, lowestPoint, etc. methods of the four walls at the edges of the canvas only be called once instead
*of for every ball in every update. Also make sure the balls are not already touching the walls upon initialization
this.walls = [new wall(0,0,w,1), new wall(0,0,1,h), new wall(0,h-2,w,1), new wall(w-2,0,1,h)], //default border walls of 0px
*/
/*USING THIS INSTEAD:*/
this.vertWallThickness = par.vertWallThickness
this.horiWallThickness = par.horiWallThickness
/*USER OBSTACLE: a set of pixels that the user selected to be included in the "obstacle." The user probably sees this as
*"obstacle" as multiple obstacles, but the program treats them all as one. It doesn't care how the pixels are grouped,
*just where they are.
*
*The obstacle has a radius. If a ball is closer than ball.radius + obstacle.radius, a collision will be registered */
this.maxObstacles = 1 //1 wall per exploding ball sounds good
this.userObstacles = [];
this.mostRecentObstacle = function(){return this.userObstacles[this.userObstacles.length-1]} //last element in the array
this.addNewObstacle = function(){
//play the wall creation sound:
document.getElementById('a-wallCreate').load()
document.getElementById('a-wallCreate').play()
this.userObstacles.push(new userObstacle())
data.numWallsMade++ //NOTE: For data collection, this may cause a problem: it will register a new obstacle even if it's just a point
}
this.removeExcessObstacles = function(){
while(this.userObstacles.length > this.maxObstacles){ /*alert("shift");*/ this.userObstacles.shift()}
}
this.addPixelsToUserObstacles = function(event){
//if(curLevel.model.userObstacles.length < 1){this.addNewObstacle()} //make a new user obstacle if none exist
if(event.type == "mousedown"){
//Make a new user obstacle if it was a mousedown not a mousemove. But first, collect the data.
data.createdPoints.push(
{position: getPixelPositionRelativeToObject([event.pageX, event.pageY], document.getElementById('gameContainer')),
timeCreated: event.timeStamp - timestampWallCreationEnabled,//make timeCreated the event's time relative to the time wall creation was initially enabled
eventType:event.type //mousedown events create new obstacles; mousemove events add points to them
})
//now, make the new obstacle:
this.addNewObstacle()
}
//if(userObstacle.atMaxLength()){this.addNewObstacle()} //make a new user obstacle if the current one has the maximum number of points
//if(curLevel.model.mostRecentObstacle() === undefined){curLevel.model.addNewObstacle()}
curLevel.model.mostRecentObstacle().addPixels(event)
curLevel.model.removeExcessObstacles() //remove any excess obstaclds if there are any real-time, while pixels are being added
}
//functions to help find intersections of balls and obstacles:
var equation = function(m,b){
this.m = m;
this.b = b;
//getters and setters are probably unecessary
}
//arguments: point in form [x,y] and vec in form [xComponent, yComponent]
function equationFromPointAnd2dVec(point, vec){
var rise = vec[1]
var run = vec[0]
var slope = rise/run
return equationFromPointAndSlope(point, slope)
}
function equationFromPointAndSlope(point, slope){
var intercept = point[1] - slope*point[0]
return new equation(slope, intercept)
}
function intersectionOf2Equations(e1, e2){
var x = (e2.b-e1.b)/(e1.m-e2.m)
var y = e1.m*x+e1.b
//console.assert(Math.round(y) == Math.round(e2.m*x+e2.b))//remove this for production if we reeally need an insignificant efficiency boost -
//just checking whether both equations give the same y value for the x value
return [x,y]
}
//returns the dot product of vectors (in the form of arrays) a and b
function dot(a,b){
var length = a.length
if(length == b.length){
var dotp = 0
for(var i = 0; i < length; i++){
dotp += a[i]*b[i]
}
return dotp
} else {
return null
}
}
//returns the norm of an n-dimensional vector a
function norm(a){
var s = 0
var length = a.length
for(var i=0; i<length; i++){
s+=a[i]*a[i]
}
return Math.sqrt(s)
}
/*//normalizes vector a (a is a js array)
function normalize(a){
var s = 0
var norm = norm(a)
var length = a.length
for(var i=0; i<length; i++){
s.push(a[i]/norm)
}
return s
}*/
//returns the cosine between two vectors (as arrays) of n dimensions
function nDimCosine(a,b) {
return dot(a,b)/(norm(a)*norm(b))
}
this.ballAndObstacleCollisionStatus = function(ball,ob){
var ballAndObSegmentCollision = {collisionHappening: false} //default value, if there's no collision just return this
for(var r = 0, pix = ob.getPixels(), numPix = pix.length; r<numPix-1; r++){
var wallPoint = pix[r]
var nextWallPoint = pix[r+1]
var segment = [wallPoint, nextWallPoint]
ballAndObSegmentCollision = this.ballAndObstacleSegmentCollisionStatus(ball, segment)
if(ballAndObSegmentCollision.collisionHappening){return ballAndObSegmentCollision} //if there's a collision, just pass the collision data
}
return ballAndObSegmentCollision //if none of the segment collisions happened, this returns the most recent segment's "collision" data, which,
//like all of the other segments' "collision" data, contains a false value for collisionHappening. it's just
//an arbitrary return value that has a similar format as the other return value this function gives but false
}
this.ballAndObstacleSegmentCollisionStatus = function(ball, segment){
var wallPoint = segment[0]
var nextWallPoint = segment[1]
this.closestDistanceToObstacle = null
var ballPoint = [ball.getX(), ball.getY()]
var ballVec = ball.getVelocity()
var ballTrajectoryEquation = equationFromPointAnd2dVec(ballPoint, ballVec)
var rad = ball.getRadius()
//compute distance between ball center and closest point on wall:
//first find intersection of ball velocity vector and wall:
//step 1. compute equations of lines (in slope-intercept form)
var wallVec = [nextWallPoint[0]-wallPoint[0], nextWallPoint[1]-wallPoint[1]]
var wallExtensionEquation = equationFromPointAnd2dVec(wallPoint, wallVec)
var intersection = intersectionOf2Equations(ballTrajectoryEquation,wallExtensionEquation)
var slopeOfWallNormal = -1/wallExtensionEquation.m
var shortestLineFromBallToWall = equationFromPointAndSlope(ballPoint, slopeOfWallNormal)
var collisionPoint = intersectionOf2Equations(shortestLineFromBallToWall, wallExtensionEquation)
//see whether the ball is traveling toward the collision point or away:
var ballToCollisionPointVec = [collisionPoint[0]-ballPoint[0], collisionPoint[1]-ballPoint[1]]
var ballVecLength = norm(ballVec)
var cos = (nDimCosine(ballVec, ballToCollisionPointVec))
var travellingTowardsWall = (cos > 0)
if(travellingTowardsWall){ //only collide if it's traveling towards the wall - there's a bug that in rare situations, balls moving away can collide and get stuck inside the obstacle.
//curLevel.view.showPoint(collisionPoint)
//curLevel.view.showPoint([ballPoint[0]+ballVec[0]*1000, ballPoint[1]+ballVec[1]*1000])
//console.log(towardsWall)
//Normal in the sense of perpendicular, UnNormalized in the sense of retaining its length
var wallNormalVector_UnNormalized = [ballPoint[0]-collisionPoint[0], ballPoint[1]-collisionPoint[1]]
//not using this:
/*Now that the intersection has been found, find the angle between the velocity and the wall*/
/*Make sure the SIGNS OF ALL THESE ANGLES ARE CORRECT:*/
/*
angleBetweenVelocityAndXAxis = -Math.atan2(ballTrajectoryEquation.rise, ballTrajectoryEquation.run)
angleBetweenWallAndYAxis = Math.PI/2 - Math.atan2(wallExtensionEquation.rise, wallExtensionEquation.run)
angleBetweenWallAndXAxis = -Math.atan2(wallExtensionEquation.rise, wallExtensionEquation.run)
angleBetweenVelocityAndWall = angleBetweenVelocityAndXAxis-angleBetweenWallAndXAxis//Math.PI/2 - angleBetweenVelocityAndXAxis - angleBetweenWallAndYAxis
//angleBetweenWallAndXAxis = Math.PI/2 - angleBetweenWallAndYAxis
ballToIntersectionDistance = distanceBetween(ballPoint, intersection)
//console.log(angleBetweenVelocityAndWall*57.3)
//See diagrams //NOTE: ADD DIAGRAMS
distanceBetweenCollisionAndIntersection = ballToIntersectionDistance * Math.sin(Math.PI/2 + angleBetweenVelocityAndWall)
HoriDistanceBetweenCollisionAndIntersection = Math.sqrt(distanceBetweenCollisionAndIntersection*distanceBetweenCollisionAndIntersection /
(wallExtensionEquation.m*wallExtensionEquation.m + 1))
//slope (m) = vertical-distance/horizontal-distance
VertDistanceBetweenCollisionAndIntersection = /*-/ABS?*/ /*wallExtensionEquation.m*HoriDistanceBetweenCollisionAndIntersection
collisionPoint = [intersection[0]-HoriDistanceBetweenCollisionAndIntersection, intersection[1]-VertDistanceBetweenCollisionAndIntersection]*/
//this is being used though:
//Consider the following scenario: the collisionPoint is not actually the closest point on the wall's extension. This happens when the ball collides with
// an endpoint of the wall. For these cases, set collisionPoint to the wall's endpoint:
var radPad = 0//par.userObstacleThickness
if(distanceBetween(ballPoint, wallPoint) <= rad+radPad) {
collisionPoint = wallPoint;
//treat the collision point as a small circle and collide off the tangent line. luckily, the vector normal to the tangent line
//has the same direction as that between the ballPoint and wallPoint(the collision point). it's magnitute doesn't matter because
//it will be normalized!
wallNormalVector_UnNormalized = [ballPoint[0]-collisionPoint[0], ballPoint[1]-collisionPoint[1]]
}
if(distanceBetween(ballPoint, nextWallPoint) <= rad+radPad) {
collisionPoint = nextWallPoint;
//get the new normal
wallNormalVector_UnNormalized = [ballPoint[0]-collisionPoint[0], ballPoint[1]-collisionPoint[1]]
}
//Check whether the collision point is actually within the wall and not just in its extension
//collision padding - how far away a ball needs to be from an obstacle for it to collide:
var obColPad = 0//par.userObstacleThickness
var collisionIsWithinSegment = (collisionPoint[0] >= Math.min(wallPoint[0], nextWallPoint[0])-obColPad) &&
(collisionPoint[0] <= Math.max(wallPoint[0], nextWallPoint[0])+obColPad) &&
(collisionPoint[1] >= Math.min(wallPoint[1], nextWallPoint[1])-obColPad) &&
(collisionPoint[1] <= Math.max(wallPoint[1], nextWallPoint[1])+obColPad)
//The ball can be touching the wall even if it doesn't intersect, so we need to find the distance to the closest point on the wall:
//The line between this and the ball is always pi/2 radians from the ball, so we can use the sin:
var ballToWallClosestDistance = distanceBetween(collisionPoint, ballPoint)
var ballWithinLineRange = false
if(Math.abs(ballToWallClosestDistance) < rad){
ballWithinLineRange = true
}
//view.showPoint(collisionPoint)
//console.log(angleBetweenVelocityAndWall*57.3)
//alert(ballToWallClosestDistance + ", " + collisionIsWithinSegmentPlusBallRadius + ", " + ballWithinLineRange + ", " + collisionPoint)
//if(!collisionIsWithinSegment && ballWithinLineRange){console.log(collisionPoint)}
if(collisionIsWithinSegment && ballWithinLineRange){
//view.showPoint(collisionPoint)
return {collisionHappening: true, wallVector: wallVec, ballVector: ballVec, wallNormal: wallNormalVector_UnNormalized}
} else {
return {collisionHappening: false, wallVector: wallVec, ballVector: ballVec, wallNormal: wallNormalVector_UnNormalized}
}
} else { //if it's not traveling towards the wall:
return {collisionHappening: false, wallVector: wallVec, ballVector: ballVec, wallNormal: wallNormalVector_UnNormalized}
}
}
this.update = function(newTime){
if(!this.frozen){ //don't update if it's frozen
var timestepDuration = newTime - this.currentTime
this.currentTime = newTime
/*bad motion algorithm:
//if it's in stochasticRobotPaths mode,
//every 500ms (with 60ms leeway), give the balls random velocities between __ and __ in each direction - combine this with the next for loop if it works; they're the same thing)
if((par.stochasticRobotPaths || par.classicMode) && Math.round(newTime % 500 < 60)){
for(var i = 0, numBalls = this.balls.length; i < numBalls; i++){
this.balls[i].setVelocity([0.24*(Math.random()-0.5), 0.24*(Math.random()-0.5)])
}
}*/
//Checks for collision with the four walls surrounding the game, NOT user-defined obstacles/walls
this.executeWallCollisions = function(){
for(var i = 0, numBalls = this.balls.length; i < numBalls; i++){
var ball = this.balls[i];
if(ball === undefined){continue}//go to the next ball if this one has been removed during the loop
var ballPoint = [ball.getX(), ball.getY()]
var vertCollisionDistance = ball.getRadius() + this.vertWallThickness //greatest distance that can trigger a collision
var horiCollisionDistance = ball.getRadius() + this.horiWallThickness
var collisionLeftWall = ballPoint[0]-horiCollisionDistance <= 0
var collisionRightWall = ballPoint[0]+horiCollisionDistance >= w
var collisionTopWall = ballPoint[1]-vertCollisionDistance <= 0
var collisionBottomWall = ballPoint[1]+vertCollisionDistance >= h
var vel = ball.getVelocity()
//flip motion in x-direction IF it's touching the left or rigth wall and going towards the respective wall: (sometimes, balls can end up too far inside the wall,
//and their velocity gets constantly flipped and they don't escapE
if((collisionLeftWall && vel[0] < 0) || (collisionRightWall && vel[0] > 0)){
ball.collide("wall")
ball.setVelocity([-vel[0], vel[1]])
}
//IF MULTIPLE COLLISIONS IN ONE UPDATE ARE A PROBLEM, PUT AN ELSE HERE
if((collisionTopWall && vel[1] < 0) || (collisionBottomWall && vel[1] > 0)) {
//flip motion in y-direction
ball.collide("wall")
ball.setVelocity([vel[0], -vel[1]])
}
/*NOT USING THIS INEFFICIENT WALL-COLLISION DETECTION ALGORITHM:
for(var k = 0, numWalls = this.walls.length; k < numWalls; k++){
var wall = this.walls[k]
var wallX = wall.getX()
var wallY = wall.getY()
var wallL = wall.getL()
var wallW = wall.getW()
var rad = ball.getRadius()
//var bh = ball.highestPoint()
//var bl = ball.lowestPoint()
//var be = ball.leftestPoint()
//var br = ball.rightestPoint()
var wh = wall.highestSide()
var wl = wall.lowestSide()
var we = wall.leftestSide()
var wr = wall.rightestSide()
//NOTE: you can increase the padding for better performance withhigher velocities
var padding = 0
function oneDimDistance(a,b) {return Math.abs(a-b)} //his is just encapsulation of a simple but soon-to-be frequently used procedure
var closestSideOfWallVertically = (oneDimDistance(ballPoint[1], wh) < oneDimDistance(ballPoint[1], wl)) ? wh : wl
var closestSideOfWallHorizontally = (oneDimDistance(ballPoint[0], we) < oneDimDistance(ballPoint[0], wr)) ? we : wr
var closestVertDistance = oneDimDistance(ballPoint[1], closestSideOfWallVertically)
var closestHoriDistance = oneDimDistance(ballPoint[0], closestSideOfWallHorizontally)
if(closestVertDistance <= rad+padding || closestHoriDistance <= rad+padding){
ball.collide("wall");
//multiply the velocity by -1 on appropriate axis. but how do we find the appropriate axis?:
//by looking for the side of the wall the ball is closest to
var closestWall = "both" //default value. this is usefeul when the ball collides with both at the same time (which is very rare)
if(closestVertDistance < closestHoriDistance){
closestWall = "vert"
} else if(closestHoriDistance < closestVertDistance){
closestWall = "hori"
}
//now, find and set the ball's velocity
var vel = ball.getVelocity()
switch(closestWall){
case "vert":
ball.setVelocity([vel[0], -vel[1]])
break
case "hori":
ball.setVelocity([-vel[0], vel[1]])
break
case "both":
ball.setVelocity([-vel[0], -vel[1]])
break
}
//move the ball one increment after setting its velocity (or leaving it):
}
}*/
}
}
this.executeObstacleCollisions = function(){
//iterate through all the balls and pixels (except last pixel because there will be no line drawn from it):
for(var i = 0, numBalls = this.balls.length; i < numBalls; i++){
var ball = this.balls[i]
if(ball === undefined){continue}//go to the next ball if this one has been removed during the loop
var rad = ball.getRadius()
var ballPoint = [ball.getX(), ball.getY()]
var ballVec = ball.getVelocity()
var ballTrajectoryEquation = equationFromPointAnd2dVec(ballPoint, ballVec)
for(var o = 0, obs = this.userObstacles, numObs = obs.length; o<numObs; o++){
var ob = obs[o]
//uncomment? for(var j = 0, pix = ob.getPixels(), numPix = pix.length; j<numPix-1; j++){
//compute distance between ball center and closest point on wall:
//first find intersection of ball velocity vector and wall:
//step 1. compute equations of lines (in slope-intercept form)
//uncomment these? or delete, we'll see
/*wallPoint = pix[j]
nextWallPoint = pix[j+1]
wallVec = [nextWallPoint[0]-wallPoint[0], nextWallPoint[1]-wallPoint[1]]
wallExtensionEquation = equationFromPointAnd2dVec(wallPoint, wallVec)
intersection = intersectionOf2Equations(ballTrajectoryEquation,wallExtensionEquation)
*/
var collisionData = this.ballAndObstacleCollisionStatus(ball,ob)
if(collisionData.collisionHappening){
//there's a collision
ball.collide("userObstacle")
//the wallNormalVector_UnNormalized vector's magnitude will need to be calculated
var wallNormalVector_UnNormalized = collisionData.wallNormal
var wallNormalVectorUnNormalizedMagnitude = Math.sqrt(wallNormalVector_UnNormalized[0]*wallNormalVector_UnNormalized[0] + wallNormalVector_UnNormalized[1]*wallNormalVector_UnNormalized[1])
var ballToWallClosestDistance = wallNormalVectorUnNormalizedMagnitude
//sometimes, the balls can end up in the walls. The user should be allowed to draw walls through balls,
//and the balls can occasionally find their way into walls. In these cases, balls should just pass through
//and their velocities should not be changed.
//This function will still try to chenge the balls' velocities and treat them as if they were normal ball
//that aren't already within obstacles. But the balls will block this change if they are within an obstacle
var ballIsInsideWall = Math.abs(ballToWallClosestDistance) + 5 < rad //5px are given as leeway
if(!ballIsInsideWall){
//perform collision bounceback:
//According to https://www.3dkingdoms.com/weekly/weekly.php?a=2, resulting velocity vector = -2*(V dot N)*N + V, where V is the velocity and N is the normalized normal vector
var wallNormalVector_Normalized = [wallNormalVector_UnNormalized[0]/wallNormalVectorUnNormalizedMagnitude, wallNormalVector_UnNormalized[1]/wallNormalVectorUnNormalizedMagnitude]
var velocityVector = collisionData.ballVector
var negativeTwoTimesDotP = -2*dot(velocityVector, wallNormalVector_Normalized)
var resultingVelocity = [wallNormalVector_Normalized[0]*negativeTwoTimesDotP+velocityVector[0], wallNormalVector_Normalized[1]*negativeTwoTimesDotP+velocityVector[1]]
ball.setVelocity(resultingVelocity, "userObstacle")
// this.color = "red"//"#"+((1<<24)*Math.random()|0).toString(16)
/*not using:
angleBetweenResultingVelocityAndXAxis = angleBetweenWallAndXAxis - angleBetweenVelocityAndWall
alert(angleBetweenWallAndXAxis + "," + angleBetweenVelocityAndWall)
console.log(angleBetweenWallAndXAxis*57.3)
//now, we know the angle of the velocity. we just need the magnitute:
velMagnitude = Math.sqrt(ballVec[0]*ballVec[0] + ballVec[1]*ballVec[1])
resultingVelXComponent = velMagnitude*Math.cos(angleBetweenResultingVelocityAndXAxis)
resultingVelYComponent = velMagnitude*Math.sin(angleBetweenResultingVelocityAndXAxis)
ball.setVelocity([resultingVelXComponent, resultingVelYComponent])*/
}
}
}
}
}
this.executeWallCollisions()
this.executeObstacleCollisions()
//move the balls
for(var i = 0, numBalls = this.balls.length; i < numBalls; i++){
this.balls[i].move(timestepDuration)
}
}
}
this.checkDefusalGuess = function(clickPoint){
//alert("Checkin'")
var clickIsInABall = false
var balls = curLevel.model.getBalls()
for(var k = 0, numBalls = balls.length; k < numBalls; k++){
if(distanceBetween(clickPoint, [balls[k].getX(), balls[k].getY()] ) <= balls[k].getRadius()){
var guessedBall = balls[k]
var clickIsInABall = true
if(curLevel.controller.ballWasAlreadyGuessed(guessedBall)){
curLevel.view.showTextOnBottom("you already guessed that robot. maybe we should have somebody else defuse these bombs who can remember")
} else if(guessedBall.explosive){
curLevel.controller.addGuessedBall(guessedBall)
return true
}
}
}
//after iterating through all of them and none of them beign correct guesses (because correct guesses would have resulted in return true)
if(clickIsInABall){
curLevel.controller.addGuessedBall(guessedBall)
return false
} else {return "notABall"}
}
}
//constructor for balls: (x and y are initial position)
function ball(x, y, radius, speed, id, isExplosive) {
this.id = id
this.occluded = false //only used for implosion/explosion/teleportation occlusion
this.collisionsEnabled = true //collisions allowed
this.colliding = false
this.obstacleSegmentsIAmInsideOf = []
//segment is of form [segmentStart, segmentEnd]
this.isWithinObstacleSegment = function(segment){
//if it's colliding, it's "inside" for all practical purposes -
//if it's colliding during obstacle creation, it should pass through the obstacle segment it's inside of
return curLevel.model.ballAndObstacleSegmentCollisionStatus(this,segment).collisionHappening
}
this.respondToObstacleSegmentCreation = function(segment) {
//Only say the ball is inside an obstacle if it is inside when the obstacle is created! Otherwise, there's no reason for the ball to be inside:
if(this.isWithinObstacleSegment(segment) && !this.obstacleSegmentsIAmInsideOf.includes(segment)){this.obstacleSegmentsIAmInsideOf.push(segment)}
}
this.imgElement = new Image();
this.imgElement.src = "robomb-pngs/robot-normal.png";
this.setImage = function(imgpath, callback) {
this.imgElement.src=imgpath
this.imgElement.onload = function(){
if(callback !== undefined){callback()}
}
}
this.getColor = function() {return this.color}
this.setColor = function(col) {this.color = col}
this.explosive = (isExplosive == "e")
this.collide = function(collisionType){
this.colliding = true
if(!this.collisionsEnabled){
} else if(collisionType == "wall" && this.explosive){
curLevel.model.removeBall(this)
//if it's in lives mode, have it decrement a life.
if(par.lives){
curLevel.model.decrementLives(/*callback:*/function(){curLevel.view.showLives(curLevel.model.lives)})
}
curLevel.controller.guessesRemaining-- //decrement number of guesses they have
//curLevel.defusalMode(); //COMMENT THIS TO DISABLE DEFUSAL MODE
} else if(collisionType == "userObstacle"){
}
//obstaclesSegmentsIAmInsideOf must be empty so it doesn't register a collision when it's inside
if(this.collisionsEnabled && this. obstacleSegmentsIAmInsideOf.length < 1){ //it seems unecessary to have this type of if statement with
//collisionsEnabled twice but I couldn't exit the function in the previous if statement to prevent
//this line from happening: (returning in that earlier conditional didn't actually exit the overall function)
this.onCollide(collisionType);
}
}
this.onCollide = function(collisionType){
//setTimeout(function(){b.collisionsEnabled = false}, 10)
//setTimeout(function(){b.collisionsEnabled = true}, 100)
//this.color = "#FF0000"
if(collisionType == "wall") {
this.callWallCollideAnimation()
this.wallCollideAudio.play()
} else if (collisionType == "userObstacle") {
this.callObstacleAnimation()
this.userObstacleCollideAudio.play()
}
}
this.wallCollideAnimation = (this.explosive) ? new wallExplodeAnimation() : new emptyAnimation()
this.userObstacleCollideAnimation = new userObstacleCollideAnimation()
this.occluderEnterAnimation = par.implodeExplodeMode ? new teleportBeginAnimation() : new emptyAnimation()
this.occluderExitAnimation = par.implodeExplodeMode ? new teleportEndAnimation() : new emptyAnimation()
this.wallCollideAudio = (this.explosive) ? new wallExplodeAudio() : new standardWallHitAudio()
this.userObstacleCollideAudio = new userObstacleCollideAudio()
this.occluderEnterAudio = par.implodeExplodeMode ? new teleportBeginAudio() : new emptyAudio()
this.occluderExitAudio = par.implodeExplodeMode ? new teleportEndAudio() : new emptyAudio()
this.callWallCollideAnimation = function(){this.wallCollideAnimation.showAnimation(this.x, this.y)}
this.callObstacleAnimation = function(){this.userObstacleCollideAnimation.showAnimation(this.x, this.y)}
this.callOccluderEnterAnimation = function(){this.occluderEnterAnimation.showAnimation(this.x, this.y)}
this.callOccluderExitAnimation = function(){this.occluderExitAnimation.showAnimation(this.x, this.y)}
this.radius = radius
this.getRadius = function(){return this.radius}
//all balls must have the same speed but random direction: therefore their:
//x velocity = their speed*cos(random angle), y velocity: speed*sin(same angle)
var randomAngle = Math.random()*2*Math.PI
this.velocity = [speed*Math.cos(randomAngle), speed*Math.sin(randomAngle)]
this.x = x//Math.random()*view.gameWidth
//this.x_prev = null, //previous position is important to calculate angle of movement
this.y = y//Math.random()* view.gameHeight
//this.y_prev = null
this.getX = function(){return this.x};
this.getY = function(){return this.y};
//this.getPrevX = function(){return this.x_prev};
//this.getPrevY = function(){return this.y_prev};
this.setX = function(new_x){
//this.x_prev = this.x
this.x = new_x
}
this.setY = function(new_y){
//y_prev = this.y
this.y = new_y
}
this.getVelocity = function(){return this.velocity};
//collisionType is optional parameter but should be used during any collision, especially those with user obstacles so it knows to collide off them
this.setVelocity = function(v, collisionType){
//don't allow setting velocity if ball is inside an obstacle.
//let the velocity stay the same until it exits the obstacle. it should only be in an obstacle if the obstacle was created over the ball
for(var p=0; p<this.obstacleSegmentsIAmInsideOf.length; p++){
var segment = this.obstacleSegmentsIAmInsideOf[p]
//if it's no longer within the obstacle:
if(!this.isWithinObstacleSegment(segment)){
//remove the obstacle from the array:
var idx = this.obstacleSegmentsIAmInsideOf.indexOf(segment)
this.obstacleSegmentsIAmInsideOf.splice(idx,1)
}
}
//now, set the velocity iff it's not within any obstacles (it should smoothly pass through obsacles it's already within, and it should only be
//within them if they were created on top of it), and if collisions are enabled. Otherwise, if it's not colliding with a user obstacle, it can be set regardless.
if((this.obstacleSegmentsIAmInsideOf.length < 1 && this.collisionsEnabled) || collisionType != "userObstacle"){
this.velocity = v; /*console.log("setting velocity")*/} /*else{console.log("setting velocity blocked" + this.obstacleSegmentsIAmInsideOf.length)}*/
}
this.highestRow = function(){return this.y - this.radius}
this.lowestRow = function(){return this.y + this.radius}
this.leftestColumn = function() {return this.x - this.radius}
this.rightestColumn = function() {return this.x + this.radius}
this.numShrinks = 0 //it has been shrunken 0 times
this.maxShrinks = 5
//move the ball one increment accorsing to its current velocity and position:
this.move = function(timestepDuration){
var td = Math.abs(timestepDuration)
//account for strange timestepDuration values like 0 or very high values:
if(td == 0 | td > 40){ //above 40 (25fps) can be buggy - balls move through obstacles in one frame
td = 33 //maybe it should be set to 40? but 33 looks good on my screen. maybe td > 40 shoudl read td > 33
}
//var stuckInWall = this.x-this.radius < 0 || this.x+radius > w || this.y-this.radius < 0 || this.y-this.radius > h
//only do the following modifications to acceleration and velocity if allowed (they will NOT be allowed in a frame where the ball
//is colliding.
if(!this.colliding){
if(par.stochasticRobotPaths){
pfsrp = par.parametersForStochasticRobotPaths
//Equation for velocity and acceleration taken from Vul, Frank, and Tenenbaum (2009), producing an Ornstein-Uhlenbeck process: (some modifications are here:
//the velocity is multiplied by velocityMultipler and force fields such as in Scholl & Pylyshyn (1999)). Acceleration is also disabled upon collision.
var acceleration = this.colliding ? 0 : [randGaussian(0,pfsrp.accelerationStandardDeviation), randGaussian(0,pfsrp.accelerationStandardDeviation)]
var velocity =[ (pfsrp.inertia*this.velocity[0] - pfsrp.springConstant*(par.gameWidth/2-this.x) + acceleration[0]),
(pfsrp.inertia*this.velocity[1] - pfsrp.springConstant*(par.gameHeight/2-this.y) + acceleration[1])
]
if(par.forceFields){
var forceFieldVector = [0,0];
var itemsWithFields = []
//balls should have fields
itemsWithFields.push.apply(curLevel.model.getBalls())
if(par.borderWallsHaveForceFields){
//add x and y value for walls. these will make the balls avoid walls
itemsWithFields.push({x:0,y:0}, {x:w,y:h})
}
//calculate force field by the distance between this robot/ball and other robot/balls. iterate through every ball:
for(var r = 0, numItemsWFields = itemsWithFields.length; r < numItemsWFields; r++){
//add the x and y squared distances to their respective components in the vector. At the end of the loop, the resulting vector will be:
//[sum of every 1/(x-distances squared), sum of every 1/(y-distances squared)]
var xDistance = this.x-itemsWithFields[r].x
var yDistance = this.y-itemsWithFields[r].y
//prevent extremely close distances which lead to ridiculously big force fields
var distanceForForcefieldLimit = 2
if(xDistance < distanceForForcefieldLimit){
xDistance = distanceForForcefieldLimit
}
if(yDistance < distanceForForcefieldLimit){
yDistance = distanceForForcefieldLimit
}
//prevent dividing by zeros:
if(!(xDistance == 0 || yDistance == 0)){
forceFieldVector[0] += par.forceFieldStrength/(xDistance*xDistance)
forceFieldVector[1] += par.forceFieldStrength/(yDistance*yDistance)
velocity[0]+=forceFieldVector[0]
velocity[1]+=forceFieldVector[1]
//for performance, forceFieldVector could be elimated and velocity added directly to instead
}
}
}
velocity[0]*=pfsrp.velocityMultipler
velocity[1]*=pfsrp.velocityMultipler
this.setVelocity(velocity)
}
} else {//if the ball is colliding:
this.colliding = false //it's about to uncollide because it's velocity has been set by whatever called move:
}