Compare commits

...
Author SHA1 Message Date
bence 55ec226407 Merge pull request 'feature/google-maps-in-mapeditor-added' (#33) from feature/google-maps-in-mapeditor-added into develop
default-pipeline default-pipeline #155
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/33
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-02 13:38:17 +02:00
bence 6ef04fcd4e fit both google and leaflet map to bounds if map already exists
default-pipeline default-pipeline #154
2021-05-02 13:29:48 +02:00
bence 3045d8acbb remove builtin streetViewControl from google map 2021-05-02 13:13:11 +02:00
balazs 19a1b79b0a toggling of street view cover added
default-pipeline default-pipeline #153
2021-05-02 12:56:25 +02:00
balazs d432a5b584 cleaned up the comment 2021-05-02 12:34:57 +02:00
balazs 8b9345eb36 removed unused css declarations; renamed invalidateSize to resize 2021-05-02 12:34:57 +02:00
balazs e1eb0077b1 hide the map selection and jump to location elements on mobile view 2021-05-02 12:34:57 +02:00
balazs 745bda11c0 refactoring and bug fixing related to the map switching 2021-05-02 12:34:57 +02:00
balazs 0bb31cbe14 added switching functionality between maps in mapeditor 2021-05-02 12:34:57 +02:00
balazs ef8ad69506 organised Leaflet map and Google Maps into wrappers 2021-05-02 12:23:37 +02:00
bence f10ad37a74 Merge pull request 'update readme' (#32) from feature/update-readme into develop
default-pipeline default-pipeline #152
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/32
2021-04-29 20:03:19 +02:00
bence 70b60d2fb8 create the first user during installation in development mode 2021-04-29 20:02:00 +02:00
bence 7e68580db6 update readme 2021-04-29 20:01:42 +02:00
balazs 8218883faf Merge pull request 'Feature In-Game Compass' (#27) from feature/in-game-compass into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/27
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-29 19:15:44 +02:00
balazs 145cb5f637 Merge branch 'develop' into feature/in-game-compass
default-pipeline default-pipeline #151
2021-04-29 19:00:11 +02:00
balazs 6d3f74f911 increased the left margin for mobile view
default-pipeline default-pipeline #150
2021-04-29 18:59:24 +02:00
balazs 3d663fb5d6 Merge pull request 'Quickfix for single player game cannot be restarted' (#30) from bugfix/quickfix-for-single-play-cannot-be-restarted into develop
default-pipeline default-pipeline #149
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/30
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-29 18:55:58 +02:00
balazs 759f654a0d Merge pull request 'feature/return-to-starting-point-button' (#26) from feature/return-to-starting-point-button into develop
default-pipeline default-pipeline #148
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/26
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-29 18:55:22 +02:00
bence c09cce3c97 Merge pull request 'MAPG-167 set background color' (#31) from feature/MAPG-167-set-background-color into develop
default-pipeline default-pipeline #147
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/31
2021-04-29 18:46:26 +02:00
bence 276e331a47 MAPG-167 set background color
default-pipeline default-pipeline #146
2021-04-29 18:43:30 +02:00
balazs 62f4fd7b84 readyToContinue is true by default and set to false only in the multigame initializing
default-pipeline default-pipeline #145
2021-04-29 13:57:32 +02:00
balazs b22f13887f clicking on compass sets the heading direction to north
default-pipeline default-pipeline #144
2021-04-29 12:39:44 +02:00
balazs 2a3f2212a7 replaced span with div for the compass item
default-pipeline default-pipeline #143
2021-04-29 12:12:17 +02:00
balazs 98df08e5fa Merge branch 'feature/return-to-starting-point-button' into feature/in-game-compass 2021-04-29 12:10:02 +02:00
balazs 8f0cfd4489 replaced spans with divs again and fixed the css issue
default-pipeline default-pipeline #142
2021-04-29 12:08:04 +02:00
balazs d42280eb10 only the image is rotated, not the whole div
default-pipeline default-pipeline #141
2021-04-29 09:18:24 +02:00
balazs 0739c65961 Merge branch 'feature/return-to-starting-point-button' into feature/in-game-compass 2021-04-29 09:11:32 +02:00
balazs 3122696364 added the visibility reset to resetRound and reverted the span div replacement in the view
default-pipeline default-pipeline #140
2021-04-29 09:07:44 +02:00
balazs 462f6cde80 Merge branch 'feature/return-to-starting-point-button' of gitea.e5tv.hu:esoko/mapguesser into feature/return-to-starting-point-button
default-pipeline default-pipeline #139
2021-04-29 09:05:32 +02:00
balazs 3f4fa83d87 reverted replacing spans with divs 2021-04-29 09:04:50 +02:00
balazs ea599809b8 Merge branch 'feature/return-to-starting-point-button' into feature/in-game-compass 2021-04-29 08:34:39 +02:00
balazs f0b0de405c Merge branch 'develop' into feature/return-to-starting-point-button
default-pipeline default-pipeline #138
2021-04-29 08:25:52 +02:00
balazs ffc4aaf110 fixed findings
default-pipeline default-pipeline #137
2021-04-29 08:24:20 +02:00
bence 58cdc1be8d Merge pull request 'fix typo' (#29) from feature/modify-author into develop
default-pipeline default-pipeline #134
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/29
2021-04-29 00:58:57 +02:00
bence 4039202268 fix typo
default-pipeline default-pipeline #133
2021-04-29 00:58:20 +02:00
bence 4677cc5229 Merge pull request 'generalize author attribution' (#28) from feature/modify-author into develop
default-pipeline default-pipeline #131
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/28
2021-04-29 00:53:26 +02:00
bence 8800724aa9 generalize author attribution
default-pipeline default-pipeline #130
2021-04-29 00:52:46 +02:00
balazs 08c38c6374 reverted style tailoring for guess
default-pipeline default-pipeline #129
2021-04-28 20:46:52 +02:00
balazs 854775011c tailored the style for the navigation buttons
default-pipeline default-pipeline #128
2021-04-28 20:42:42 +02:00
balazs e33d8c02e9 added compass button and coupled its transform:rotate style attribute with the POV heading value 2021-04-28 20:33:17 +02:00
balazs 3d52c967b6 reformatted: added new line breaks to the game.css
default-pipeline default-pipeline #127
2021-04-28 18:30:58 +02:00
balazs 5c8f3d6fb9 returns to start when clicked on home symbol
default-pipeline default-pipeline #126
2021-04-28 18:26:46 +02:00
balazs b9927f79fc added a new button to the view for the return to starting point feature 2021-04-28 17:58:48 +02:00
balazs 56f3a9b380 Merge pull request 'Stuck scoring at zero fixed by regular map area update in session' (#24) from bugfix/scoring-stuck-at-zero into develop
default-pipeline default-pipeline #124
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/24
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-27 20:36:41 +02:00
balazs a0d41c388b Merge branch 'develop' into bugfix/scoring-stuck-at-zero
default-pipeline default-pipeline #123
2021-04-27 17:56:52 +02:00
balazs 092cc63148 Merge pull request 'jump to coordinate feature added to the header stripe' (#25) from feature/jump-to-specific-coordinates-in-mapeditor into develop
default-pipeline default-pipeline #122
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/25
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-27 17:56:35 +02:00
balazs 95553296bd removed extra line break
default-pipeline default-pipeline #121
2021-04-27 17:54:47 +02:00
balazs 9330227879 Merge branch 'feature/jump-to-specific-coordinates-in-mapeditor' of gitea.e5tv.hu:esoko/mapguesser into feature/jump-to-specific-coordinates-in-mapeditor
default-pipeline default-pipeline #120
2021-04-27 17:50:10 +02:00
balazs faaff33841 removed #jumpForm and reformatted the code 2021-04-27 17:48:45 +02:00
balazs 508a6899d8 Merge branch 'develop' into bugfix/scoring-stuck-at-zero
default-pipeline default-pipeline #119
2021-04-27 17:36:18 +02:00
balazs be0f7b64b8 refactored comment
default-pipeline default-pipeline #118
2021-04-27 17:35:06 +02:00
balazs 15e3248ee1 Merge branch 'develop' into feature/jump-to-specific-coordinates-in-mapeditor
default-pipeline default-pipeline #117
2021-04-27 17:26:44 +02:00
balazs 04a897c958 Merge pull request 'folder name db changed to database in the path' (#23) from bugfix/change-path-to-renamed-folder into develop
default-pipeline default-pipeline #116
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/23
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-27 17:25:54 +02:00
balazs 845d77ebda area of map is updated in the session at each game preparation to avoid outdated map area and wrong score calculation
default-pipeline default-pipeline #115
2021-04-27 16:46:40 +02:00
balazs 8bc3fda49b jump to coordinate feature added to the header stripe
default-pipeline default-pipeline #114
2021-04-27 10:18:28 +02:00
balazs d78817180b folder name db changed to database in the path
default-pipeline default-pipeline #113
2021-04-26 20:21:21 +02:00
bence 0f810bd2ba Merge pull request 'feature/MAPG-228-deploy-signed-and-verified-tags-only' (#22) from feature/MAPG-228-deploy-signed-and-verified-tags-only into develop
default-pipeline default-pipeline #111
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/22
2021-04-25 15:16:46 +02:00
bence 9d1fc8b60c MAPG-228 prune local tags when fetching
default-pipeline default-pipeline #107
2021-04-25 15:11:49 +02:00
bence dd46fb3d32 MAPG-228 check if the Release* tag is signed and verified 2021-04-25 15:11:20 +02:00
bence 1a40e3a18d Merge pull request 'MAPG-226 change remaining time calculation' (#21) from feature/MAPG-226-change-timeout-algorithm into develop
default-pipeline default-pipeline #106
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/21
2021-04-10 19:00:20 +02:00
bence 1aa5d3b43e MAPG-226 change remaining time calculation 2021-04-10 18:59:27 +02:00
bence a418d5cf66 Merge pull request 'MAPG-213 move visibility workaround to Game.showResultMap' (#20) from bugfix/MAPG-213-result-map-not-show-on-mobile-after-reload into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/20
2021-04-04 23:41:05 +02:00
bence 5cae286a5b MAPG-213 move visibility workaround to Game.showResultMap 2021-04-04 23:40:27 +02:00
bence c5d2591371 Merge pull request 'MAPG-223 send error if member already guessed' (#19) from feature/MAPG-223-user-should-not-be-able-to-guess-after-round-is-over into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/19
2021-04-04 23:29:04 +02:00
bence cf59f937c9 MAPG-223 send error if member already guessed
default-pipeline default-pipeline #104
2021-04-04 23:28:44 +02:00
bence 257f59c96e Merge pull request 'MAPG-222 fixes: skip invalid guessPosition, hide showSummaryButton, handle countdown time if timeout is 0' (#18) from bugfix/MAPG-222-show-summary-can-be-clicked-when-game-is-restarted into develop
default-pipeline default-pipeline #98
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/18
2021-04-04 23:05:47 +02:00
bence c2f5806394 MAPG-222 fixes: skip invalid guessPosition, hide showSummaryButton, handle countdown time if timeout is 0
default-pipeline default-pipeline #96
2021-04-04 23:04:07 +02:00
bence ac0b71fe09 Merge pull request 'feature/MAPG-213-user-shouldn-t-be-able-to-re-guess-when-page-is-reloaded' (#17) from feature/MAPG-213-user-shouldn-t-be-able-to-re-guess-when-page-is-reloaded into develop
default-pipeline default-pipeline #94
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/17
2021-04-04 22:31:29 +02:00
bence 19f7b50002 MAPG-213 show results immediately if user already guessed
default-pipeline default-pipeline #91
2021-04-04 22:24:40 +02:00
bence 6342bb1e79 MAPG-213 send round data at initialization if user guessed 2021-04-04 22:18:00 +02:00
bence 3abdb6910e MAPG-213 clean up init
default-pipeline default-pipeline #90
2021-04-04 20:52:28 +02:00
bence 7d2588f7cd Merge pull request 'feature/MAPG-219-disable-continue-if-timeout-is-not-reached' (#16) from feature/MAPG-219-disable-continue-if-timeout-is-not-reached into develop
default-pipeline default-pipeline #88
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/16
2021-04-04 19:23:08 +02:00
bence be40acdb2d MAPG-219 disable continue button until round is over
default-pipeline default-pipeline #84
2021-04-04 18:48:31 +02:00
bence a2f2080959 MAPG-219 send 'end_round' when all members are finished
default-pipeline default-pipeline #83
2021-04-04 17:43:37 +02:00
bence cc49c56075 Merge pull request 'feature/MAPG-205-set-timeout-in-multiplayer-rooms' (#15) from feature/MAPG-205-set-timeout-in-multiplayer-rooms into develop
default-pipeline default-pipeline #82
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/15
2021-04-04 14:22:59 +02:00
bence 69329b9f81 MAPG-205 show countdown time
default-pipeline default-pipeline #80
2021-04-04 14:22:10 +02:00
bence 20914b466e MAPG-205 add elements for showing countdown 2021-04-04 14:20:12 +02:00
bence 3630637ec1 MAPG-205 handle round timeout on frontend 2021-04-04 14:20:12 +02:00
bence 42224b074e MAPG-205 set timeout for rounds 2021-04-04 14:20:12 +02:00
bence 0e8ed14305 MAPG-205 delete unnecessary empty lines 2021-04-03 23:39:20 +02:00
bence d3a12fb6f1 Merge pull request 'feature/MAPG-204-show-guesses-from-others' (#14) from feature/MAPG-204-show-guesses-from-others into develop
default-pipeline default-pipeline #76
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/14
2021-04-03 22:13:52 +02:00
bence 3f9d104adc MAPG-204 show guesses from others
default-pipeline default-pipeline #72
2021-04-03 22:10:21 +02:00
bence 9e5d20796c MAPG-204 clean up result sending 2021-04-03 18:24:43 +02:00
bence 7433813337 Merge pull request 'MAPG-208 error handling adaptations' (#13) from bugfix/MAPG-208-error-messages into develop
default-pipeline default-pipeline #75
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/13
2021-03-21 16:39:43 +01:00
bence 42854fe975 MAPG-208 error handling adaptations
default-pipeline default-pipeline #53
2021-03-21 14:37:08 +01:00
bence 71681f1e55 Merge pull request 'MAPG-207 don't use MapGuesser.showmodal for modal 'multi'' (#12) from bugfix/MAPG-207-prevent-closing-multi-modal into develop
default-pipeline default-pipeline #50
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/12
2021-03-21 12:47:35 +01:00
bence 1cf0ab44fd MAPG-207 don't use MapGuesser.showmodal for modal 'multi'
default-pipeline default-pipeline #49
2021-03-21 12:45:57 +01:00
bence fc9eb183f1 Merge pull request 'MAPG-203 fix wrong variable name' (#11) from bugfix/MAPG-203-fix-condition-in-js into develop
default-pipeline default-pipeline #46
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/11
2021-03-20 22:26:33 +01:00
bence d431a693fc MAPG-203 fix wrong variable name
default-pipeline default-pipeline #45
2021-03-20 22:24:47 +01:00
bence ace490b7e9 Merge pull request 'MAPG-203 use MULTI_WS_URL environment variable' (#10) from feature/MAPG-203-fine-tune-env-variables into develop
default-pipeline default-pipeline #43
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/10
2021-03-20 21:24:20 +01:00
bence e3771d36ce MAPG-203 use MULTI_WS_URL environment variable
default-pipeline default-pipeline #42
2021-03-20 21:21:40 +01:00
bence 6e03476173 Merge pull request 'bugfix/MAPG-203-fix-singleplayer' (#9) from bugfix/MAPG-203-fix-singleplayer into develop
default-pipeline default-pipeline #39
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/9
2021-03-20 21:00:14 +01:00
bence f3ec71d6dd MAPG-203 fix condition in game.js
default-pipeline default-pipeline #38
2021-03-20 20:59:10 +01:00
bence c2f0a1dd51 MAPG-203 fix signatures in GameController 2021-03-20 20:58:57 +01:00
bence a417fbd760 Merge pull request 'feature/MAPG-203-initial-multiplayer-implementation' (#8) from feature/MAPG-203-initial-multiplayer-implementation into develop
default-pipeline default-pipeline #37
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/8
2021-03-20 20:46:36 +01:00
bence a9cda56586 MAPG-203 install NPM packages on install and update
default-pipeline default-pipeline #36
2021-03-20 19:45:21 +01:00
bence 02fcbd2f9c MAPG-203 prepare GameFlowController for multiplayer 2021-03-20 19:45:21 +01:00
bence 2f665381c3 MAPG-203 prepare GameController for multiplayer 2021-03-20 19:45:21 +01:00
bence 573440868e MAPG-203 add new routes for multiplayer 2021-03-20 19:45:21 +01:00
bence 79490a0616 MAPG-203 add class for multiplayer internal connection (PHP-NodeJS TCP) 2021-03-20 19:45:21 +01:00
bence ed343f2359 MAPG-203 add new environment variables 2021-03-20 19:45:20 +01:00
bence 563f900423 MAPG-203 implement game mode selection UI 2021-03-20 19:45:20 +01:00
bence 24a10c534e MAPG-203 frontend adaptations for multiplayer 2021-03-20 19:44:48 +01:00
bence 0cbb7ba145 MAPG-203 add debugger config for NodeJS 2021-03-20 19:44:48 +01:00
bence 5dd2ce0d5a MAPG-203 add cleanup for multi_rooms to db:maintain 2021-03-20 19:44:48 +01:00
bence e5fb725c69 MAPG-203 add new table and model for multi_room 2021-03-20 19:44:48 +01:00
bence fc40c18679 MAPG-203 multiplayer handler server (NodeJS) 2021-03-20 19:44:48 +01:00
bence 4fabc39d44 MAPG-203 restructure Docker stack 2021-03-20 19:44:48 +01:00
bence 2120fbebbc MAPG-203 add Dockerfile for multiplayer (NodeJS) 2021-03-20 19:44:47 +01:00
bence b9f0529dce MAPG-203 add new Composer packages 2021-03-20 19:44:47 +01:00
bence a3ca7638c7 MAPG-203 add NPM packages for multiplayer 2021-03-20 15:21:07 +01:00
bence 8538d0a119 MAPG-203 add node_modules to gitignore 2021-03-17 23:09:07 +01:00
bence 0cc63ef936 Merge pull request 'MAPG-203 this.response.newPlace -> this.response.place' (#7) from bugfix/MAPG-203-fix-wrong-object-key into develop
default-pipeline default-pipeline #28
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/7
2021-03-17 22:54:45 +01:00
bence 10d239bfb4 MAPG-203 this.response.newPlace -> this.response.place
default-pipeline default-pipeline #25
2021-03-17 22:54:06 +01:00
bence e2493e1b7e Merge pull request 'MAPG-203 select places at the beginning of the game' (#6) from feature/MAPG-203-initial-multiplayer-implementation into develop
default-pipeline default-pipeline #24
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/6
2021-03-17 22:48:43 +01:00
bence edd8c0f8a4 MAPG-203 select places at the beginning of the game
default-pipeline default-pipeline #21
2021-03-17 22:47:11 +01:00
bence 01f0f7d4bc Merge pull request 'feature/MAPG-203-initial-multiplayer-implementation' (#5) from feature/MAPG-203-initial-multiplayer-implementation into develop
default-pipeline default-pipeline #17
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/5
2021-03-15 12:32:54 +01:00
bence c317a0abf4 MAPG-203 adapt game.js to the new endpoints
default-pipeline default-pipeline #15
2021-03-15 12:28:25 +01:00
bence 9c4b7448a9 MAPG-203 refactor GameFlowController 2021-03-15 12:28:06 +01:00
bence 60e20bd92b MAPG-203 rename game/newPlace endpoint 2021-03-15 12:26:15 +01:00
bence 2c4d809d49 MAPG-203 remove placesWithoutPano logic from PlaceRepository 2021-03-15 12:25:49 +01:00
bence bf27f15709 Merge pull request 'MAPG-2020 run PHPStan with 1G memory limit' (#4) from bugfix/MAPG-2020-phpstan-needs-bigger-memory-limit into develop
default-pipeline default-pipeline #12
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/4
2021-01-01 22:36:56 +01:00
bence db3094eb62 MAPG-2020 run PHPStan with 1G memory limit
default-pipeline default-pipeline #11
2021-01-01 22:34:32 +01:00
bence 77c71ab2da Merge pull request 'MAPG-201 update Git link to gitea.e5tv.hu' (#2) from feature/MAPG-201-migrate-to-own-git-server into develop
Check 2 Check 2 was successful
Check 3 Check 3 was not successful
Check 4 Check 4 was successful
Check 5 Check 5 was successful
Check Check was successful
Pipeline Pipeline #1
test_pipeline test_pipeline #3
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/2
2020-12-27 22:12:33 +00:00
bence 8327cd975a MAPG-201 update Git link to gitea.e5tv.hu 2020-12-27 23:08:30 +01:00
bence cc8148fcf9 Merged in bugfix/MAPG-196-fix-remote-static-root-detection (pull request #181)
MAPG-196 fix regex that recognizes remote static root
2020-07-06 20:00:49 +00:00
bence 2a7b5fb992 MAPG-196 fix regex that recognizes remote static root 2020-07-06 21:59:25 +02:00
48 changed files with 2302 additions and 299 deletions

No files matched your search

+4
View File
@@ -15,3 +15,7 @@ MAIL_PORT=2500
GOOGLE_OAUTH_CLIENT_ID=your_google_oauth_client_id
GOOGLE_OAUTH_CLIENT_SECRET=your_google_oauth_client_secret
GOOGLE_ANALITICS_ID=your_google_analytics_id
MULTI_INTERNAL_HOST=multi
MULTI_INTERNAL_PORT=5000
MULTI_WS_URL=mapguesser-dev.ch:8090
MULTI_WS_PORT=8090
+1
View File
@@ -1,3 +1,4 @@
.env
installed
vendor
node_modules
+8
View File
@@ -9,6 +9,14 @@
"pathMappings": {
"/var/www/mapguesser": "${workspaceRoot}",
}
},
{
"name": "Listen for NodeJS Inspector in Docker",
"type": "node",
"request": "attach",
"port": 9229,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/var/www/mapguesser"
}
]
}
+37 -10
View File
@@ -9,12 +9,12 @@ This is the MapGuesser Application project. This is a game about guessing where
The first step is obviously cloning the repository to your machine:
```
git clone https://bitbucket.org/esoko/mapguesser.git
git clone https://gitea.e5tv.hu/esoko/mapguesser.git
```
All the commands listed here should be executed from the repository root.
### (Optional) Setup Docker stack
### Setup Docker stack (recommended)
The easiest way to build up a fully working application with web server and database is to use Docker Compose with the included `docker-compose.yml`.
@@ -24,9 +24,17 @@ All you have to do is executing the following command:
docker-compose up -d
```
Attach shell to the container of `mapguesser_app`. All of the following commands should be executed there.
Attach shell to the container of `mapguesser_app`:
**If you don't use the Docker stack you need to install your environment manually. Check `docker-compose.yml` and `docker/Dockerfile` to see the system requirements.**
```
docker exec -it mapguesser_app_1 bash
```
All of the following commands should be executed there.
### Manual setup (alternative)
If you don't use the Docker stack you need to install your environment manually. Check `docker-compose.yml` and `docker/Dockerfile` to see the system requirements.
### Initialize project
@@ -40,15 +48,28 @@ composer create-project
The `.env` file contains several environment variables that are needed by the application to work properly. These should be configured for your environment.
**You should set here the API keys that enable playing the game. Without these API keys the application cannot work well. To get Google API keys visit this page: https://console.developers.google.com/**
One very important variable is `DEV`. This indicates that the application operates in development (staging) and not in production mode.
One very important variable is `DEV`. This indicates that the application operates in development (staging) and not in produciton mode.
**Hint:** If you install the application in the Docker stack for development (staging) environment, only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, multiplayer, etc.) are fine with the default value.
If you install the application in the Docker stack for development (staging) environment, only the variables for external dependencies (API keys, map attribution, etc.) should be adapted. All other variables (for DB connection, static root, mailing, etc.) are fine with the default value.
#### API keys
**You should set the API keys that enable playing the game. Without these API keys the application cannot work well. To get Google API keys visit this page: https://console.developers.google.com/**
Required Google APIs:
* **Maps JavaScript API**: for the interactive maps and street views
* **Maps Static API**: for the static map images
* **Street View Static API**: for the backend metadata requests
Required API keys:
* **GOOGLE_MAPS_SERVER_API_KEY**: this it used by the backend and should have access to **Street View Static API**
* **GOOGLE_MAPS_JS_API_KEY**: this is used by the frontend and should have access to **Maps JavaScript API** and **Maps Static API**
Additionally, a tile provider is also needed for map editor. This should be configured by `LEAFLET_TILESERVER_URL` and `LEAFLET_TILESERVER_ATTRIBUTION`. You can find some providers here: https://wiki.openstreetmap.org/wiki/Tile_servers. OpenStreetMap's tile server is fine for testing.
### (Production only) Create cron job
To maintain database (delete inactive users, old sessions etc.), the command `db:maintain` should be regularly executed. It is recommened to create a cron job that runs every hour:
To maintain database (delete inactive users, old sessions etc.), the command `db:maintain` should be regularly executed. It is recommended to create a cron job that runs every hour:
```
0 * * * * /path/to/your/installation/mapg db:maintain >>/var/log/cron-mapguesser.log 2>&1
@@ -56,13 +77,19 @@ To maintain database (delete inactive users, old sessions etc.), the command `db
### Finalize installation
After you set the environment variables in the `.env` file, execute the following command:
After you followed the above steps, execute the following command:
```
scripts/install.sh
```
**And you are done!** The application is ready to use and develop.
**Warning: Because of a known issue the image `mapguesser_multi` fails to run without the installation steps. You have to relauch `docker-compose up -d` after you finished the installation process.**
**And you are done!** The application is ready to use and develop. In development mode an administrative user is also created by the installation script, email is **mapg@mapg.dev**, password is **123456**. In production mode you should create the first administrative user with the following command:
```
./mapg user:add EMAIL PASSWORD admin
```
If you installed it in the Docker stack, you can reach it on http://localhost. The mails that are sent by the application can be found on http://localhost:8080/. If needed, the database server can be directly reached on localhost:3306.
+1 -1
View File
@@ -23,4 +23,4 @@ pipelines:
- apt-get update && apt-get install -y unzip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install
- vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json
- php -d memory_limit=1G vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json
+2 -1
View File
@@ -6,7 +6,8 @@
"require": {
"vlucas/phpdotenv": "^4.1",
"symfony/console": "^5.1",
"phpmailer/phpmailer": "^6.1"
"phpmailer/phpmailer": "^6.1",
"fzaninotto/faker": "^1.9"
},
"require-dev": {
"phpunit/phpunit": "^9",
Generated
+57 -2
View File
@@ -4,8 +4,63 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "34563bfc619f47473b2e37a5639dd63e",
"content-hash": "b71c0ffc0761a6b90f6242346b735a09",
"packages": [
{
"name": "fzaninotto/faker",
"version": "v1.9.2",
"source": {
"type": "git",
"url": "https://github.com/fzaninotto/Faker.git",
"reference": "848d8125239d7dbf8ab25cb7f054f1a630e68c2e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fzaninotto/Faker/zipball/848d8125239d7dbf8ab25cb7f054f1a630e68c2e",
"reference": "848d8125239d7dbf8ab25cb7f054f1a630e68c2e",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0"
},
"require-dev": {
"ext-intl": "*",
"phpunit/phpunit": "^4.8.35 || ^5.7",
"squizlabs/php_codesniffer": "^2.9.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"autoload": {
"psr-4": {
"Faker\\": "src/Faker/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "François Zaninotto"
}
],
"description": "Faker is a PHP library that generates fake data for you.",
"keywords": [
"data",
"faker",
"fixtures"
],
"support": {
"issues": "https://github.com/fzaninotto/Faker/issues",
"source": "https://github.com/fzaninotto/Faker/tree/v1.9.2"
},
"abandoned": true,
"time": "2020-12-11T09:56:16+00:00"
},
{
"name": "phpmailer/phpmailer",
"version": "v6.1.6",
@@ -2710,5 +2765,5 @@
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "1.1.0"
"plugin-api-version": "2.0.0"
}
@@ -0,0 +1,9 @@
CREATE TABLE `multi_rooms` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`room_id` varchar(6) NOT NULL,
`state` text NOT NULL,
`members` text NOT NULL,
`updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `room_id` (`room_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
+13 -4
View File
@@ -1,14 +1,23 @@
version: '3'
services:
app:
build: ./docker
build:
context: ./docker
dockerfile: Dockerfile-app
ports:
- 80:80
volumes:
- .:/var/www/mapguesser
links:
- 'mariadb'
- 'mail'
multi:
build:
context: ./docker
dockerfile: Dockerfile-multi
ports:
- 5000:5000
- 8090:8090
- 9229:9229
volumes:
- .:/var/www/mapguesser
mariadb:
image: mariadb:10.3
ports:
File renamed without changes.
+16
View File
@@ -0,0 +1,16 @@
FROM ubuntu:focal
ENV DEBIAN_FRONTEND noninteractive
# Install necessary packages
RUN apt update --fix-missing
RUN apt install -y curl build-essential
# Install Node.js and required packages
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt install -y nodejs
VOLUME /var/www/mapguesser
WORKDIR /var/www/mapguesser
ENTRYPOINT /usr/bin/node --inspect=0.0.0.0:9229 multi
+369
View File
@@ -0,0 +1,369 @@
'use strict';
process.title = 'mapguesser-multi';
class MultiGame {
static ROUND_TIMEOUT_DEFAULT = 120000;
static ROUND_TIMEOUT_MINIMUM = 15000;
static ROUND_TIMEOUT_DIVIDER = 1.5;
static ROUND_TIMEOUT_OFFSET = 500;
constructor() {
this.rooms = new Map();
}
cleanupRooms() {
this.rooms.forEach(function (room, roomId) {
var lastValidDate = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000);
if (room.updated < lastValidDate) {
this.rooms.delete(roomId);
}
});
}
connectToRoom(roomId, token, connection) {
if (!this.rooms.has(roomId) || !this.rooms.get(roomId).members.has(token)) {
return;
}
var room = this.rooms.get(roomId)
var member = room.members.get(token);
member.connection = connection;
this._sendInitialData(room, member, token);
}
createRoom(roomId) {
this.rooms.set(roomId, { members: new Map(), rounds: [], currentRound: -1, updated: new Date() });
return { ok: true };
}
joinRoom(roomId, token, userName) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
if (room.members.has(token)) {
return { error: 'member_already_joined' };
}
var data = { userName: userName };
var self = this;
room.members.forEach(function (member) {
self._sendToMember(member, 'member_joined', data);
});
room.members.set(token, { userName: userName, connection: null });
return { ok: true };
}
startGame(roomId, places) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
var rounds = [];
places.forEach(function (place) {
rounds.push({
place: place,
results: new Map(),
timeout: MultiGame.ROUND_TIMEOUT_DEFAULT,
timeoutStarted: null,
timeoutHandler: null
})
});
room.rounds = rounds;
this.nextRound(roomId, 0);
return { ok: true };
}
guess(roomId, token, guessPosition, distance, score) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
var round = room.rounds[room.currentRound];
if (round.results.has(token)) {
return { error: 'already_guessed' };
}
var member = room.members.get(token);
var allResults = this._collectResultsInRound(room, round);
this._broadcastGuess(room, member.userName, guessPosition, distance, score);
round.results.set(token, { guessPosition: guessPosition, distance: distance, score: score });
this._setNewTimeout(room, round);
return { allResults: allResults };
}
nextRound(roomId, currentRound) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
room.currentRound = currentRound;
var round = room.rounds[room.currentRound];
round.timeoutStarted = new Date();
var self = this;
round.timeoutHandler = setTimeout(function () {
self._endRound(room, round);
}, round.timeout + MultiGame.ROUND_TIMEOUT_OFFSET);
var data = {};
data.place = { panoId: round.place.panoId, pov: round.place.pov };
data.timeout = round.timeout;
var self = this;
room.members.forEach(function (member) {
self._sendToMember(member, 'new_round', data);
});
return { ok: true };
}
_setNewTimeout(room, round) {
clearTimeout(round.timeoutHandler);
if (room.members.size === round.results.size) {
round.timeout = 0;
round.timeoutStarted = new Date();
this._endRound(room, round);
} else {
round.timeout = round.timeout - (new Date() - round.timeoutStarted);
if (round.timeout > MultiGame.ROUND_TIMEOUT_DIVIDER * MultiGame.ROUND_TIMEOUT_MINIMUM) {
round.timeout = Math.round(round.timeout / MultiGame.ROUND_TIMEOUT_DIVIDER);
} else if (round.timeout > MultiGame.ROUND_TIMEOUT_MINIMUM) {
round.timeout = MultiGame.ROUND_TIMEOUT_MINIMUM;
}
round.timeoutStarted = new Date();
var self = this;
round.timeoutHandler = setTimeout(function () {
self._endRound(room, round);
}, round.timeout + MultiGame.ROUND_TIMEOUT_OFFSET);
this._broadcastTimeout(room, round);
}
}
_endRound(room, round) {
var allResults = this._collectResultsInRound(room, round);
var self = this;
room.members.forEach(function (member, token) {
var result = { guessPosition: null, distance: null, score: 0 };
if (round.results.has(token)) {
result = round.results.get(token);
} else {
round.results.set(token, result);
}
var data = { position: round.place.position, result: result, allResults: allResults };
self._sendToMember(member, 'end_round', data);
});
}
_sendInitialData(room, member, token) {
var data = {};
if (room.currentRound >= 0) {
var round = room.rounds[room.currentRound];
data.place = round.place;
data.timeout = round.timeout - (new Date() - round.timeoutStarted);
}
data.history = [];
for (var i = 0; i <= room.currentRound; ++i) {
var round = room.rounds[i];
if (i === room.currentRound && !round.results.has(token)) {
continue;
}
var result = { guessPosition: null, distance: null, score: 0 };
var allResults = [];
round.results.forEach(function (currentResult, currentToken) {
if (token === currentToken) {
result = currentResult;
return;
}
allResults.push({ userName: room.members.get(currentToken).userName, guessPosition: currentResult.guessPosition, distance: currentResult.distance, score: currentResult.score });
});
data.history.push({
position: round.place.position,
result: result,
allResults: allResults
});
}
data.members = [];
room.members.forEach(function (currentMember) {
data.members.push({ userName: currentMember.userName, me: member === currentMember });
});
data.readyToContinue = room.currentRound >= 0 && room.members.size === room.rounds[room.currentRound].results.size
this._sendToMember(member, 'initialize', data);
}
_collectResultsInRound(room, round) {
var results = [];
round.results.forEach(function (result, token) {
results.push({
userName: room.members.get(token).userName,
guessPosition: result.guessPosition,
distance: result.distance,
score: result.score
});
});
return results;
}
_broadcastTimeout(room, round) {
var self = this;
room.members.forEach(function (member) {
self._sendToMember(member, 'timeout_changed', { timeout: round.timeout });
});
}
_broadcastGuess(room, userName, guessPosition, distance, score) {
var data = { userName: userName, guessPosition: guessPosition, distance: distance, score: score };
var round = room.rounds[room.currentRound];
var self = this;
room.members.forEach(function (member, token) {
if (!round.results.has(token)) {
return;
}
self._sendToMember(member, 'guess', data);
});
}
_sendToMember(member, type, data) {
if (!member.connection) {
return;
}
if (member.connection.readyState !== ws.OPEN) {
member.connection = null;
return;
}
member.connection.send(JSON.stringify({ type: type, data: data }));
}
}
require('dotenv').config();
var
net = require('net'),
ws = require('ws');
var multiGame = new MultiGame();
//TODO: following should be in a separate class/function
var tcpServer = net.createServer(function (socket) {
socket.on('data', function (data) {
try {
data = JSON.parse(data);
} catch (e) {
console.error('Cannot parse data: ' + data);
return;
}
var response;
switch (data.func) {
case 'create_room':
response = multiGame.createRoom(data.args.roomId);
break;
case 'join_room':
response = multiGame.joinRoom(data.args.roomId, data.args.token, data.args.userName);
break;
case 'start_game':
response = multiGame.startGame(data.args.roomId, data.args.places);
break
case 'guess':
response = multiGame.guess(data.args.roomId, data.args.token, data.args.guessPosition, data.args.distance, data.args.score);
break;
case 'next_round':
response = multiGame.nextRound(data.args.roomId, data.args.currentRound);
break;
}
socket.write(JSON.stringify(response));
socket.end();
});
});
tcpServer.on('listening', function () {
console.log('[INFO] TCP server started');
});
tcpServer.listen(process.env.MULTI_INTERNAL_PORT);
var wsServer = new ws.Server({ port: process.env.MULTI_WS_PORT });
wsServer.on('connection', function (connection, request) {
console.log('[INFO] New WS connection: ' + request.connection.remoteAddress);
connection.on('message', function (data) {
try {
data = JSON.parse(data);
} catch (e) {
console.error('Cannot parse data: ' + data);
return;
}
switch (data.func) {
case 'connect_to_room':
multiGame.connectToRoom(data.args.roomId, data.args.token, connection);
break;
}
});
connection.on('close', function () {
console.log('[INFO] WS connection ended: ' + request.connection.remoteAddress);
});
});
wsServer.on('listening', function () {
console.log('[INFO] WS server started');
});
setInterval(function () {
multiGame.cleanupRooms();
}, 24 * 60 * 60 * 1000);
+17
View File
@@ -0,0 +1,17 @@
{
"name": "mapguesser-multi",
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
},
"ws": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw=="
}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "mapguesser-multi",
"version": "",
"description": "MapGuesser Application - Multiplayer",
"main": "index.js",
"dependencies": {
"dotenv": "^8.2.0",
"ws": "^7.4.4"
},
"scripts": {},
"author": "Pőcze Bence and The MapGuesser Contributors <bence@pocze.ch>",
"license": "GNU AGPL 3.0"
}
+107 -2
View File
@@ -79,7 +79,7 @@
line-height: 1;
}
#distanceInfo>p:nth-child(2), #scoreInfo>p:nth-child(2) {
#distanceInfo>p:nth-child(2), #distanceInfo>p:nth-child(3), #scoreInfo>p:nth-child(2) {
display: none;
}
@@ -102,13 +102,94 @@
display: none;
}
#startMultiGameButton {
display: none;
}
#players > p {
font-size: 14px;
font-weight: bold;
}
#countdown {
position: absolute;
top: 5px;
right: 5px;
height: 28px;
line-height: 28px;
padding: 0 8px;
background-color: #eeeeee;
border: solid 1px #555555;
border-radius: 3px;
opacity: 0.95;
z-index: 5;
visibility: hidden;
}
#countdown.yellow {
background-color: #f7c789;
border: solid 1px #e8a349;
}
#countdown.red {
background-color: #f7a5a5;
border: solid 1px #aa5e5e;
}
#countdown p {
font-size: 16px;
line-height: inherit;
}
#countdown.yellow p {
color: #9c4308;
}
#countdown.red p {
color: #701919;
}
#navigation {
position: absolute;
z-index: 2;
}
#navigation .navigationItem {
margin-top: 10px;
opacity: 70%;
cursor: pointer;
}
#navigation .navigationItem:hover {
opacity: 100%;
}
#navigation .navigationItem div {
position: absolute;
}
#navigation .circleBackground {
width: 100%;
height: 100%;
opacity: 50%;
}
#navigation .navigationIcon {
width: 75%;
height: 75%;
margin: auto;
margin-top: 50%;
transform: translateY(-50%);
-ms-transform: translateY(-50%);
}
@media screen and (max-width: 599px) {
#mapName {
display: none;
}
#showGuessButtonContainer {
position: absolute;
left: 20px;
left: 65px;
bottom: 30px;
right: 20px;
z-index: 2;
@@ -125,6 +206,14 @@
#scoreBarBase {
width: 100%;
}
#navigation {
bottom: 25px;
left: 10px;
width: 45px;
}
#navigation .navigationItem {
height: 45px;
}
}
@media screen and (min-width: 600px) {
@@ -168,6 +257,14 @@
#scoreBarBase {
width: 60%;
}
#navigation {
bottom: 50px;
left: 20px;
width: 60px;
}
#navigation .navigationItem {
height: 60px;
}
@media screen and (max-height: 424px) {
#guess {
top: 10px;
@@ -182,5 +279,13 @@
right: 20px;
bottom: 30px;
}
#navigation {
bottom: 30px;
left: 10px;
width: 45px;
}
#navigation .navigationItem {
height: 45px;
}
}
}
+11 -1
View File
@@ -1,4 +1,4 @@
#map {
.map {
position: absolute;
top: 0;
left: 0;
@@ -7,6 +7,13 @@
z-index: 1;
}
#mapSelection img {
display: inline;
width: 1em;
height: 1em;
vertical-align: -0.15em;
}
/* modify the cursor for the Leaflet map */
.leaflet-container {
cursor: crosshair;
@@ -65,6 +72,9 @@
#placeControl {
top: calc(50% + 10px);
}
.hideOnMobile {
display: none;
}
}
@media screen and (min-width: 1000px), (max-height: 599px) {
+5
View File
@@ -12,6 +12,10 @@ html, body {
padding: 0;
}
body {
background-color: #cccccc;
}
button::-moz-focus-inner, input::-moz-focus-inner {
padding: 0;
border: 0;
@@ -374,6 +378,7 @@ header>p>span:not(:last-child) {
}
main {
background-color: #ffffff;
padding: 6px 12px;
}
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 16 16">
<circle cx="50%" cy="50%" r="50%" fill="black" />
</svg>

After

Width:  |  Height:  |  Size: 146 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<polygon id="needleNorth" points="50,0 33,50 66,50" style="fill:red;stroke:black;stroke-width:1" />
<polygon id="needleSouth" points="50,100 33,50 66,50" style="fill:white;stroke:black;stroke-width:1" />
Sorry, your browser does not support inline SVG.
</svg>

After

Width:  |  Height:  |  Size: 328 B

+5
View File
@@ -0,0 +1,5 @@
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" fill="#3183ce" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="m8 3.293 6 6V13.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 13.5V9.293l6-6zm5-.793V6l-2-2V2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5z"/>
<path fill-rule="evenodd" d="M7.293 1.5a1 1 0 0 1 1.414 0l6.647 6.646a.5.5 0 0 1-.708.708L8 2.207 1.354 8.854a.5.5 0 1 1-.708-.708L7.293 1.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 556 B

+4
View File
@@ -0,0 +1,4 @@
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15.817.113A.5.5 0 0 1 16 .5v14a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 15.01l-4.902.98A.5.5 0 0 1 0 15.5v-14a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0L10.5.99l4.902-.98a.5.5 0 0 1 .415.103zM10 1.91l-4-.8v12.98l4 .8V1.91zm1 12.98 4-.8V1.11l-4 .8v12.98zm-6-.8V1.11l-4 .8v12.98l4-.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 530 B

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,10 @@
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path
fill="#3183ce"
fill-rule="evenodd"
stroke="#19456d"
stroke-width="0.3"
stroke-linecap="round"
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
</svg>

After

Width:  |  Height:  |  Size: 529 B

+5
View File
@@ -0,0 +1,5 @@
<!-- Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" viewBox="0 0 16 16">
<path d="m14.12 10.163 1.715.858c.22.11.22.424 0 .534L8.267 15.34a.598.598 0 0 1-.534 0L.165 11.555a.299.299 0 0 1 0-.534l1.716-.858 5.317 2.659c.505.252 1.1.252 1.604 0l5.317-2.66zM7.733.063a.598.598 0 0 1 .534 0l7.568 3.784a.3.3 0 0 1 0 .535L8.267 8.165a.598.598 0 0 1-.534 0L.165 4.382a.299.299 0 0 1 0-.535L7.733.063z"/>
<path d="m14.12 6.576 1.715.858c.22.11.22.424 0 .534l-7.568 3.784a.598.598 0 0 1-.534 0L.165 7.968a.299.299 0 0 1 0-.534l1.716-.858 5.317 2.659c.505.252 1.1.252 1.604 0l5.317-2.659z"/>
</svg>

After

Width:  |  Height:  |  Size: 718 B

+590 -96
View File
@@ -5,6 +5,8 @@
NUMBER_OF_ROUNDS: 5,
MAX_SCORE: 1000,
mapBounds: null,
multi: { token: null, owner: false },
rounds: [],
scoreSum: 0,
panoId: null,
@@ -15,41 +17,281 @@
adaptGuess: false,
googleLink: null,
initialize: function () {
readyToContinue: true,
timeoutEnd: null,
countdownHandler: null,
MultiConnector: {
connection: null,
reconnectCounter: 0,
connect: function () {
if (Game.MultiConnector.connection && Game.MultiConnector.connection.readyState !== WebSocket.CLOSED) {
return;
}
Game.MultiConnector.connection = new WebSocket((MapGuesser.isSecure ? 'wss' : 'ws') + '://' + multiUrl);
Game.MultiConnector.connection.onopen = function () {
document.getElementById('loading').style.visibility = 'hidden';
Game.MultiConnector.reconnectCounter = 0;
Game.MultiConnector.connection.send(JSON.stringify({ func: 'connect_to_room', args: { roomId: roomId, token: Game.multi.token } }));
};
Game.MultiConnector.connection.onclose = Game.MultiConnector.noConnection;
Game.MultiConnector.connection.onerror = function (event) {
console.error('WebSocket error in Game.MultiConnector:', event);
};
Game.MultiConnector.connection.onmessage = function (message) {
var json;
try {
json = JSON.parse(message.data);
} catch (e) {
console.error('Cannot parse message!');
console.error(message.data);
return;
}
switch (json.type) {
case 'initialize':
Game.MultiConnector.initialize(json.data);
break;
case 'member_joined':
Game.MultiConnector.memberJoined(json.data);
break;
case 'new_round':
Game.MultiConnector.newRound(json.data);
break;
case 'guess':
Game.MultiConnector.guess(json.data);
break;
case 'timeout_changed':
Game.MultiConnector.timeoutChanged(json.data);
break;
case 'end_round':
Game.MultiConnector.endRound(json.data);
break;
}
};
},
noConnection: function () {
if (Game.MultiConnector.reconnectCounter === 2) {
console.error('Could not reconnect WebSocket for Game.MultiConnector...')
}
setTimeout(function () {
Game.MultiConnector.reconnectCounter++;
console.log('Reconnecting WebSocket for Game.MultiConnector... ' + Game.MultiConnector.reconnectCounter);
Game.MultiConnector.connect();
}, 1000 + Math.min(Game.MultiConnector.reconnectCounter * 500, 9000));
},
initialize: function (data) {
Game.readyToContinue = false;
if (data.history.length === 0 && !data.place) {
var div = document.getElementById('players');
for (var i = 0; i < data.members.length; ++i) {
var member = data.members[i];
var p = document.createElement('p');
p.innerHTML = member.userName + (member.me ? ' (me)' : '');
div.appendChild(p);
}
}
for (var i = 0; i < data.history.length; ++i) {
var round = data.history[i];
Game.rounds.push({ position: round.position, guessPosition: round.result.guessPosition, realMarker: null, guessMarkers: [] });
Game.addPositionToResultMap(true);
if (round.result.guessPosition) {
Game.addGuessPositionToResultMap(round.result.guessPosition, null, true);
}
Game.scoreSum += round.result.score;
for (var j = 0; j < round.allResults.length; ++j) {
var result = round.allResults[j];
if (result.guessPosition) {
Game.addGuessPositionToResultMap(result.guessPosition, result, true);
}
}
}
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
if (data.timeout) {
Game.startCountdown(data.timeout);
}
if (data.place) {
Game.readyToContinue = data.readyToContinue;
Game.panoId = data.place.panoId;
Game.pov = data.place.pov;
document.getElementById('multi').style.visibility = 'hidden';
if (Game.rounds.length === 0 || !Game.rounds[Game.rounds.length - 1].position) {
document.getElementById('panoCover').style.visibility = 'hidden';
Game.startNewRound();
} else {
Game.loadPano(Game.panoId, Game.pov);
Game.showResultFromHistory(data.history[data.history.length - 1].result);
}
}
document.getElementById('loading').style.visibility = 'hidden';
},
memberJoined: function (data) {
var div = document.getElementById('players');
var p = document.createElement('p');
p.innerHTML = data.userName;
div.appendChild(p);
},
newRound: function (data) {
if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
Game.reset();
}
Game.readyToContinue = false;
Game.panoId = data.place.panoId;
Game.pov = data.place.pov;
document.getElementById('multi').style.visibility = 'hidden';
Game.resetRound();
Game.startNewRound();
Game.startCountdown(data.timeout);
},
guess: function (data) {
var resultBounds = Game.map.getBounds();
Game.addGuessPositionToResultMap(data.guessPosition, data);
resultBounds.extend(data.guessPosition);
Game.map.fitBounds(resultBounds);
},
timeoutChanged: function (data) {
Game.startCountdown(data.timeout);
},
endRound: function (data) {
Game.readyToContinue = true;
Game.startCountdown(0);
if (Game.rounds[Game.rounds.length - 1].guessPosition || Game.rounds[Game.rounds.length - 1].position) {
if (Game.multi.owner) {
document.getElementById('continueButton').disabled = false;
document.getElementById('startNewGameButton').disabled = false;
}
return;
}
document.getElementById('guessButton').disabled = true;
document.getElementById('panoCover').style.visibility = 'visible';
Game.receiveResult(data.position, data.result.guessPosition, { distance: data.result.distance, score: data.result.score }, data.allResults);
}
},
prepare: function () {
var data = new FormData();
var userNames;
if (roomId) {
var userNames = localStorage.userNames ? JSON.parse(localStorage.userNames) : {};
if (!userNames.hasOwnProperty(roomId)) {
userNames[roomId] = prompt('Your name: ');
localStorage.userNames = JSON.stringify(userNames);
}
data.append('userName', userNames[roomId]);
}
document.getElementById('loading').style.visibility = 'visible';
var url = roomId ? '/multiGame/' + roomId + '/prepare.json' : '/game/' + mapId + '/prepare.json';
MapGuesser.httpRequest('POST', url, function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
document.getElementById('mapName').innerHTML = this.response.mapName;
Game.mapBounds = this.response.bounds;
Game.initialize();
if (roomId) {
Game.multi.token = this.response.token;
Game.multi.owner = this.response.owner;
document.getElementById('multi').style.visibility = 'visible';
if (Game.multi.owner) {
document.getElementById('startMultiGameButton').style.display = 'block';
}
document.getElementById('loading').style.visibility = 'visible';
Game.MultiConnector.connect();
}
}, data);
},
initialize: function () {
document.getElementById('panoCover').style.visibility = 'visible';
document.getElementById('currentRound').innerHTML = '1/' + String(Game.NUMBER_OF_ROUNDS);
document.getElementById('currentScoreSum').innerHTML = '0/0';
Game.map.setOptions({
draggableCursor: 'crosshair'
});
Game.map.fitBounds(mapBounds);
Game.map.fitBounds(Game.mapBounds);
MapGuesser.httpRequest('GET', '/game/' + mapId + '/newPlace.json', function () {
if (roomId) {
// if it is multiplayer mode, data is sent via WS
return;
}
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/game/' + mapId + '/initialData.json', function () {
document.getElementById('loading').style.visibility = 'hidden';
document.getElementById('panoCover').style.visibility = 'hidden';
if (this.response.error) {
//TODO: handle this error
Game.handleErrorResponse(this.response.error);
return;
}
Game.panoId = this.response.panoId;
Game.pov = this.response.pov;
Game.panoId = this.response.place.panoId;
Game.pov = this.response.place.pov;
if (this.response.history) {
for (var i = 0; i < this.response.history.length; ++i) {
var round = this.response.history[i];
Game.rounds.push({ position: round.position, guessPosition: round.guessPosition, realMarker: null, guessMarker: null, line: null });
Game.addRealGuessPair(round.position, round.guessPosition, true);
Game.scoreSum += round.score;
}
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
for (var i = 0; i < this.response.history.length; ++i) {
var round = this.response.history[i];
Game.rounds.push({ position: round.position, guessPosition: round.result.guessPosition, realMarker: null, guessMarkers: [] });
Game.addPositionToResultMap(true);
Game.addGuessPositionToResultMap(round.result.guessPosition, null, true);
Game.scoreSum += round.result.score;
}
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
Game.startNewRound();
});
},
@@ -63,10 +305,16 @@
for (var i = 0; i < Game.rounds.length; ++i) {
var round = Game.rounds[i];
if (round.realMarker && round.guessMarker && round.line) {
if (round.realMarker) {
round.realMarker.setMap(null);
round.guessMarker.setMap(null);
round.line.setMap(null);
}
for (var j = 0; j < round.guessMarkers.length; ++j) {
var guessMarker = round.guessMarkers[j];
guessMarker.marker.setMap(null);
guessMarker.line.setMap(null);
if (guessMarker.info) {
guessMarker.info.close();
}
}
}
@@ -76,16 +324,21 @@
var distanceInfo = document.getElementById('distanceInfo');
distanceInfo.children[0].style.display = null;
distanceInfo.children[1].style.display = null;
distanceInfo.children[2].style.display = null;
var scoreInfo = document.getElementById('scoreInfo');
scoreInfo.children[0].style.display = null;
scoreInfo.children[1].style.display = null;
document.getElementById('continueButton').style.display = null;
document.getElementById('showSummaryButton').style.display = null;
document.getElementById('startNewGameButton').style.display = null;
document.getElementById('showGuessButton').style.visibility = null;
document.getElementById('guess').style.visibility = null;
document.getElementById('guess').classList.remove('result');
// needs to be set visible after the show guess map hid it in mobile view
document.getElementById("navigation").style.visibility = 'visible';
Game.initialize();
},
@@ -96,39 +349,71 @@
var lastRound = Game.rounds[Game.rounds.length - 1];
lastRound.realMarker.setVisible(false);
lastRound.guessMarker.setVisible(false);
lastRound.line.setVisible(false);
for (var i = 0; i < lastRound.guessMarkers.length; ++i) {
var guessMarker = lastRound.guessMarkers[i];
guessMarker.marker.setVisible(false);
guessMarker.line.setVisible(false);
if (guessMarker.info) {
guessMarker.info.close();
}
}
}
document.getElementById('panoCover').style.visibility = 'hidden';
document.getElementById('showGuessButton').style.visibility = null;
document.getElementById('guess').style.visibility = null;
document.getElementById('guess').classList.remove('result')
document.getElementById('guess').classList.remove('result');
// needs to be set visible after the show guess map hid it in mobile view
document.getElementById("navigation").style.visibility = 'visible';
Game.map.setOptions({
draggableCursor: 'crosshair'
});
Game.map.fitBounds(mapBounds);
Game.map.fitBounds(Game.mapBounds);
if (roomId) {
// if it is multiplayer mode, data is sent via WS
return;
}
Game.startNewRound();
},
startNewRound: function () {
Game.rounds.push({ position: null, guessPosition: null, realMarker: null, guessMarker: null, line: null });
Game.rounds.push({ position: null, guessPosition: null, realMarker: null, guessMarkers: [] });
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
Game.loadPano(Game.panoId, Game.pov);
// update the compass
const heading = Game.panorama.getPov().heading;
document.getElementById("compass").style.transform = "translateY(-50%) rotate(" + heading + "deg)";
},
handleErrorResponse: function (error) {
// for the time being we only handle the "no_session_found" error and reset the game
switch (error) {
case 'no_session_found':
MapGuesser.showModalWithContent('Error', 'Your session is invalid!', [{
type: 'button',
classNames: [],
text: 'Restart game',
onclick: function () {
window.location.reload();
}
}]);
break;
MapGuesser.httpRequest('GET', '/game/' + mapId + '/json', function () {
mapBounds = this.response.bounds;
case 'game_already_started':
MapGuesser.showModalWithContent('Error', 'This game is already started, you cannot join.');
break;
Game.reset();
});
default:
MapGuesser.showModalWithContent('Error', 'Error code: \'' + error + '\'');
break
}
},
loadPano: function (panoId, pov) {
@@ -141,7 +426,106 @@
Game.panorama.setPano(panoId);
},
evaluateGuess: function () {
receiveResult: function (position, guessPosition, result, allResults) {
Game.scoreSum += result.score;
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
var resultBounds = new google.maps.LatLngBounds();
Game.rounds[Game.rounds.length - 1].position = position;
Game.addPositionToResultMap();
resultBounds.extend(position);
if (guessPosition) {
Game.addGuessPositionToResultMap(guessPosition);
resultBounds.extend(guessPosition);
}
if (allResults) {
for (var i = 0; i < allResults.length; ++i) {
var currentResult = allResults[i];
if (currentResult.guessPosition) {
Game.addGuessPositionToResultMap(currentResult.guessPosition, currentResult);
resultBounds.extend(currentResult.guessPosition);
}
}
}
Game.showResultMap(result, resultBounds);
},
showResultFromHistory: function (result) {
var round = Game.rounds[Game.rounds.length - 1];
var resultBounds = new google.maps.LatLngBounds();
round.realMarker.setVisible(true);
resultBounds.extend(round.position);
for (var j = 0; j < round.guessMarkers.length; ++j) {
var guessMarker = round.guessMarkers[j];
guessMarker.marker.setVisible(true);
guessMarker.line.setVisible(true);
resultBounds.extend(guessMarker.marker.getPosition());
}
Game.showResultMap(result, resultBounds);
},
showResultMap: function (result, resultBounds) {
// TODO: refactor - it is necessary for mobile
if (window.getComputedStyle(document.getElementById('guess')).visibility === 'hidden') {
document.getElementById('showGuessButton').click();
}
if (Game.adaptGuess) {
document.getElementById('guess').classList.remove('adapt');
}
if (Game.guessMarker) {
Game.guessMarker.setMap(null);
Game.guessMarker = null;
}
document.getElementById('guess').classList.add('result');
Game.map.setOptions({
draggableCursor: 'grab'
});
Game.map.fitBounds(resultBounds);
var distanceInfo = document.getElementById('distanceInfo');
if (result.distance === null) {
distanceInfo.children[0].style.display = 'none';
distanceInfo.children[1].style.display = 'block';
} else {
distanceInfo.children[0].style.display = 'block';
distanceInfo.children[1].style.display = 'none';
document.getElementById('distance').innerHTML = Util.printDistanceForHuman(result.distance);
}
document.getElementById('score').innerHTML = result.score;
var scoreBarProperties = Game.calculateScoreBarProperties(result.score, Game.MAX_SCORE);
var scoreBar = document.getElementById('scoreBar');
scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
scoreBar.style.width = scoreBarProperties.width;
if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
document.getElementById('continueButton').style.display = 'none';
document.getElementById('showSummaryButton').style.display = 'block';
} else if (roomId) {
if (Game.multi.owner) {
if (!Game.readyToContinue) {
document.getElementById('continueButton').disabled = true;
}
} else {
document.getElementById('continueButton').style.display = 'none';
}
}
},
guess: function () {
if (!Game.guessMarker) {
return;
}
@@ -150,63 +534,34 @@
Game.rounds[Game.rounds.length - 1].guessPosition = guessPosition;
document.getElementById('guessButton').disabled = true;
if (Game.adaptGuess) {
document.getElementById('guess').classList.remove('adapt');
}
document.getElementById('loading').style.visibility = 'visible';
document.getElementById('panoCover').style.visibility = 'visible';
var data = new FormData();
data.append('lat', String(guessPosition.lat));
data.append('lng', String(guessPosition.lng));
MapGuesser.httpRequest('POST', '/game/' + mapId + '/guess.json', function () {
document.getElementById('loading').style.visibility = 'visible';
var url = roomId ? '/multiGame/' + roomId + '/guess.json' : '/game/' + mapId + '/guess.json';
MapGuesser.httpRequest('POST', url, function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
Game.guessMarker.setMap(null);
Game.guessMarker = null;
Game.receiveResult(this.response.position, guessPosition, this.response.result, this.response.allResults);
document.getElementById('loading').style.visibility = 'hidden';
document.getElementById('guess').classList.add('result');
Game.scoreSum += this.response.result.score;
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
Game.rounds[Game.rounds.length - 1].position = this.response.result.position;
Game.addRealGuessPair(this.response.result.position, guessPosition);
var resultBounds = new google.maps.LatLngBounds();
resultBounds.extend(this.response.result.position);
resultBounds.extend(guessPosition);
Game.map.setOptions({
draggableCursor: 'grab'
});
Game.map.fitBounds(resultBounds);
document.getElementById('distance').innerHTML = Util.printDistanceForHuman(this.response.result.distance);
document.getElementById('score').innerHTML = this.response.result.score;
var scoreBarProperties = Game.calculateScoreBarProperties(this.response.result.score, Game.MAX_SCORE);
var scoreBar = document.getElementById('scoreBar');
scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
scoreBar.style.width = scoreBarProperties.width;
if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
document.getElementById('continueButton').style.display = 'none';
document.getElementById('showSummaryButton').style.display = 'block';
if (this.response.place) {
Game.panoId = this.response.place.panoId;
Game.pov = this.response.place.pov;
}
Game.panoId = this.response.panoId;
Game.pov = this.response.pov;
}, data);
},
addRealGuessPair: function (position, guessPosition, hidden) {
addPositionToResultMap: function (hidden) {
var round = Game.rounds[Game.rounds.length - 1];
var position = round.position;
round.realMarker = new google.maps.Marker({
map: Game.map,
@@ -227,16 +582,25 @@
round.realMarker.addListener('click', function () {
window.open('https://www.google.com/maps/search/?api=1&query=' + this.getPosition().toUrlValue(), '_blank');
});
},
round.guessMarker = new google.maps.Marker({
addGuessPositionToResultMap: function (guessPosition, result, hidden) {
var round = Game.rounds[Game.rounds.length - 1];
var position = round.position;
var guessMarker = { marker: null, line: null, info: null };
var markerSvg = result ? 'marker-gray-empty.svg' : 'marker-blue-empty.svg';
var markerLabel = result ? result.userName.charAt(0).toUpperCase() : '?';
guessMarker.marker = new google.maps.Marker({
map: Game.map,
visible: !hidden,
position: guessPosition,
zIndex: Game.rounds.length,
clickable: false,
clickable: !!result,
draggable: false,
icon: {
url: STATIC_ROOT + '/img/markers/marker-gray-empty.svg?rev=' + REVISION,
url: STATIC_ROOT + '/img/markers/' + markerSvg + '?rev=' + REVISION,
size: new google.maps.Size(24, 32),
scaledSize: new google.maps.Size(24, 32),
anchor: new google.maps.Point(12, 32),
@@ -247,11 +611,11 @@
fontFamily: 'Roboto',
fontSize: '16px',
fontWeight: '500',
text: '?'
text: markerLabel
}
});
round.line = new google.maps.Polyline({
guessMarker.line = new google.maps.Polyline({
map: Game.map,
visible: !hidden,
path: [
@@ -274,6 +638,19 @@
draggable: false,
editable: false
});
if (result) {
guessMarker.info = new google.maps.InfoWindow({
content: '<p class="small bold">' + result.userName + '</p>' +
'<p class="small">' + Util.printDistanceForHuman(result.distance) + ' | ' + result.score + ' points</p>',
});
guessMarker.marker.addListener('click', function () {
guessMarker.info.open(Game.map, this);
});
}
round.guessMarkers.push(guessMarker);
},
calculateScoreBarProperties: function (score, maxScore) {
@@ -294,12 +671,19 @@
showSummary: function () {
var distanceInfo = document.getElementById('distanceInfo');
distanceInfo.children[0].style.display = 'none';
distanceInfo.children[1].style.display = 'block';
distanceInfo.children[1].style.display = 'none';
distanceInfo.children[2].style.display = 'block';
var scoreInfo = document.getElementById('scoreInfo');
scoreInfo.children[0].style.display = 'none';
scoreInfo.children[1].style.display = 'block';
document.getElementById('showSummaryButton').style.display = null;
document.getElementById('startNewGameButton').style.display = 'block';
if (!roomId || Game.multi.owner) {
document.getElementById('startNewGameButton').style.display = 'block';
if (!Game.readyToContinue) {
document.getElementById('startNewGameButton').disabled = true;
}
}
var resultBounds = new google.maps.LatLngBounds();
@@ -320,12 +704,17 @@
fontWeight: '500',
text: String(i + 1)
});
round.realMarker.setVisible(true);
round.guessMarker.setVisible(true);
round.line.setVisible(true);
round.realMarker.setVisible(true);
resultBounds.extend(round.position);
resultBounds.extend(round.guessPosition);
for (var j = 0; j < round.guessMarkers.length; ++j) {
var guessMarker = round.guessMarkers[j];
guessMarker.marker.setVisible(true);
guessMarker.line.setVisible(true);
resultBounds.extend(guessMarker.marker.getPosition());
}
}
Game.map.fitBounds(resultBounds);
@@ -356,6 +745,52 @@
Game.googleLink.href = 'https://maps.google.com/maps';
}
}, 1);
},
startCountdown: function (timeout) {
if (Game.countdownHandler) {
clearInterval(Game.countdownHandler);
}
Game.countdownElement = document.getElementById('countdown');
Game.countdownTimeElement = document.getElementById('countdownTime');
Game.setCountdownTime(Math.round(timeout / 1000));
if (timeout <= 0) {
return;
}
Game.timeoutEnd = new Date(new Date().getTime() + timeout);
Game.countdownHandler = setInterval(function () {
var timeLeft = Math.round((Game.timeoutEnd - new Date()) / 1000);
Game.setCountdownTime(timeLeft);
if (timeLeft <= 0) {
document.getElementById('panoCover').style.visibility = 'visible';
clearInterval(Game.countdownHandler);
}
}, 1000);
},
setCountdownTime: function (time) {
if (time <= 0) {
Game.countdownElement.style.visibility = 'hidden';
return;
}
if (time <= 15) {
Game.countdownElement.className = 'red';
} else if (time <= 30) {
Game.countdownElement.className = 'yellow';
} else {
Game.countdownElement.className = '';
}
Game.countdownElement.style.visibility = 'visible';
Game.countdownTimeElement.innerHTML = time;
}
};
@@ -373,13 +808,7 @@
}
};
MapGuesser.sessionAvailableHooks.reinitializeGame = function () {
MapGuesser.httpRequest('GET', '/game/' + mapId + '/json', function () {
mapBounds = this.response.bounds;
Game.initialize();
});
};
MapGuesser.sessionAvailableHooks.reinitializeGame = Game.prepare;
if (!('ontouchstart' in document.documentElement)) {
Game.adaptGuess = true;
@@ -392,7 +821,7 @@
});
Game.map.addListener('click', function (e) {
if (Game.rounds[Game.rounds.length - 1].guessPosition) {
if (Game.rounds[Game.rounds.length - 1].guessPosition || Game.rounds[Game.rounds.length - 1].position) {
return;
}
@@ -407,7 +836,7 @@
clickable: false,
draggable: true,
icon: {
url: STATIC_ROOT + '/img/markers/marker-gray-empty.svg?rev=' + REVISION,
url: STATIC_ROOT + '/img/markers/marker-blue-empty.svg?rev=' + REVISION,
size: new google.maps.Size(24, 32),
scaledSize: new google.maps.Size(24, 32),
anchor: new google.maps.Point(12, 32),
@@ -438,26 +867,49 @@
Game.panorama.addListener('pov_changed', function () {
Game.rewriteGoogleLink();
const heading = Game.panorama.getPov().heading;
document.getElementById("compass").style.transform = "translateY(-50%) rotate(" + heading + "deg)";
});
Game.initialize();
if (COOKIES_CONSENT) {
Game.prepare();
}
document.getElementById('showGuessButton').onclick = function () {
this.style.visibility = 'hidden';
document.getElementById('guess').style.visibility = 'visible';
document.getElementById('navigation').style.visibility = 'hidden';
}
document.getElementById('closeGuessButton').onclick = function () {
document.getElementById('showGuessButton').style.visibility = null;
document.getElementById('guess').style.visibility = null;
document.getElementById('navigation').style.visibility = 'visible';
}
document.getElementById('guessButton').onclick = function () {
Game.evaluateGuess();
Game.guess();
}
document.getElementById('continueButton').onclick = function () {
Game.resetRound();
if (roomId) {
if (!Game.multi.owner || !Game.readyToContinue) {
return;
}
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/multiGame/' + roomId + '/nextRound.json', function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
});
} else {
Game.resetRound();
}
}
document.getElementById('showSummaryButton').onclick = function () {
@@ -465,6 +917,48 @@
}
document.getElementById('startNewGameButton').onclick = function () {
Game.reset();
if (roomId) {
if (!Game.multi.owner) {
return;
}
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/multiGame/' + roomId + '/initialData.json', function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
});
} else {
Game.reset();
}
}
document.getElementById('startMultiGameButton').onclick = function () {
if (!roomId || !Game.multi.owner) {
return;
}
document.getElementById('multi').style.visibility = 'hidden';
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/multiGame/' + roomId + '/initialData.json', function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
});
}
document.getElementById('returnToStart').onclick = function () {
Game.loadPano(Game.panoId, Game.pov);
}
document.getElementById('compassContainer').onclick = function () {
Game.panorama.setPov({ heading: 0, pitch: Game.panorama.getPov().pitch });
}
})();
+352 -74
View File
@@ -7,7 +7,6 @@
description: null
},
map: null,
markers: null,
panorama: null,
selectedMarker: null,
added: {},
@@ -118,16 +117,13 @@
MapEditor.resetSelected();
MapEditor.selectedMarker = marker;
MapEditor.map.invalidateSize(true);
MapEditor.map.resize();
MapEditor.map.panTo(marker.getLatLng());
MapEditor.panorama.setVisible(false);
if (marker.placeId) {
MapEditor.markers.removeLayer(MapEditor.selectedMarker);
MapEditor.map.addLayer(MapEditor.selectedMarker);
marker.setIcon(IconCollection.iconBlue);
marker.setZIndexOffset(2000);
MapEditor.map.changeMarkerIcon(marker, MapEditor.map.iconCollection.iconBlue);
document.getElementById('deleteButton').style.display = 'block';
@@ -165,13 +161,13 @@
var placeId = MapEditor.selectedMarker.placeId
if (places[placeId].id && !del) {
MapEditor.map.removeLayer(MapEditor.selectedMarker);
MapEditor.markers.addLayer(MapEditor.selectedMarker);
MapEditor.selectedMarker.setIcon(places[placeId].noPano ? IconCollection.iconRed : IconCollection.iconGreen);
MapEditor.selectedMarker.setZIndexOffset(1000);
MapEditor.map.changeMarkerIcon(
MapEditor.selectedMarker,
places[placeId].noPano ? MapEditor.map.iconCollection.iconRed : MapEditor.map.iconCollection.iconGreen
);
} else {
delete places[placeId];
MapEditor.map.removeLayer(MapEditor.selectedMarker);
MapEditor.map.removeMarker(MapEditor.selectedMarker);
}
document.getElementById('deleteButton').style.display = 'none';
@@ -223,7 +219,7 @@
MapEditor.resetSelected(del);
MapEditor.selectedMarker = null;
MapEditor.map.invalidateSize(true);
MapEditor.map.resize();
},
deletePlace: function () {
@@ -239,6 +235,7 @@
delete MapEditor.added[placeId];
delete MapEditor.edited[placeId];
delete places[placeId];
document.getElementById('added').innerHTML = String(Object.keys(MapEditor.added).length);
document.getElementById('edited').innerHTML = String(Object.keys(MapEditor.edited).length);
@@ -313,24 +310,6 @@
}
};
var IconCollection = {
iconGreen: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconRed: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-red.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconBlue: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
};
var Util = {
getHighResData: function () {
if (window.devicePixelRatio >= 4) {
@@ -340,64 +319,319 @@
} else {
return { ppi: 72, tileSize: 512, zoomOffset: -1, minZoom: 2, maxZoom: 20 };
}
},
extractCoordinates: function (coordinatesStr) {
var coordinates = { valid: false, latlng: { lat: 0., lng: 0. } };
var delimiters = [',', ' ', ';'];
coordinatesStr = coordinatesStr.trim();
if (coordinatesStr.length == 0) {
return coordinates;
}
for (var delimiter of delimiters) {
if (coordinatesStr.indexOf(delimiter) != -1) {
var coordinatesArr = coordinatesStr.split(delimiter);
coordinates.latlng.lat = parseFloat(coordinatesArr[0]);
coordinates.latlng.lng = parseFloat(coordinatesArr[1]);
if (!isNaN(coordinates.latlng.lat) && !isNaN(coordinates.latlng.lng)) {
coordinates.valid = true;
return coordinates;
}
}
}
return coordinates;
}
};
MapEditor.map = L.map('map', {
zoomControl: false
});
var LMapWrapper = {
map: null,
markers: null,
divId: null,
iconCollection: {
iconGreen: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconRed: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-red.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
}),
iconBlue: L.icon({
iconUrl: STATIC_ROOT + '/img/markers/marker-blue.svg?rev=' + REVISION,
iconSize: [24, 32],
iconAnchor: [12, 32]
})
},
MapEditor.map.on('click', function (e) {
var marker = L.marker(e.latlng, {
icon: IconCollection.iconBlue,
zIndexOffset: 2000
})
.addTo(MapEditor.map)
.on('click', function () {
init: function (divId, places) {
document.getElementById(divId).style.display = "block";
if (!LMapWrapper.map) {
LMapWrapper.divId = divId;
LMapWrapper.map = L.map(LMapWrapper.divId, {
center: { lat: 0., lng: 0. },
zoom: 2,
zoomControl: false
});
LMapWrapper.map.on('click', function (e) {
LMapWrapper.placeMarker(e.latlng);
});
var highResData = Util.getHighResData();
L.tileLayer(tileUrl, {
attribution: tileAttribution,
subdomains: '1234',
ppi: highResData.ppi,
tileSize: highResData.tileSize,
zoomOffset: highResData.zoomOffset,
minZoom: highResData.minZoom,
maxZoom: highResData.maxZoom
}).addTo(LMapWrapper.map);
if (mapId) {
LMapWrapper.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
}
}
LMapWrapper.loadMarkers(places);
document.getElementById('streetViewCoverSelector').disabled = true;
},
hide: function () {
document.getElementById(LMapWrapper.divId).style.display = 'none';
},
loadMarkers: function (places) {
if (!LMapWrapper.markers) {
LMapWrapper.markers = L.markerClusterGroup({
maxClusterRadius: 50
});
} else {
LMapWrapper.markers.clearLayers();
}
for (var placeId in places) {
if (!places.hasOwnProperty(placeId)) {
continue;
}
var place = places[placeId];
var marker = L.marker({ lat: place.lat, lng: place.lng }, {
icon: place.noPano ? LMapWrapper.iconCollection.iconRed : LMapWrapper.iconCollection.iconGreen,
zIndexOffset: 1000
})
.addTo(LMapWrapper.markers)
.on('click', function () {
MapEditor.select(this);
});
marker.placeId = placeId;
}
LMapWrapper.map.addLayer(LMapWrapper.markers);
},
// TODO: check whether marker is already existing on the map for the coordinates
// or alternatively block saving for matching coordinates
placeMarker: function (latLng) {
var marker = L.marker(latLng, {
icon: LMapWrapper.iconCollection.iconBlue,
zIndexOffset: 2000
})
.addTo(LMapWrapper.markers)
.on('click', function () {
MapEditor.select(this);
});
MapEditor.select(marker);
},
panTo: function (latLng) {
LMapWrapper.map.panTo(latLng);
},
resize: function () {
LMapWrapper.map.invalidateSize(true);
},
changeMarkerIcon: function (marker, icon) {
marker.setIcon(icon);
marker.setZIndexOffset(2000);
},
removeMarker: function (marker) {
LMapWrapper.markers.removeLayer(marker);
},
toggleStreetViewCover: function () { }
};
var GMapWrapper = {
map: null,
markers: null,
divId: null,
streetViewCover: null,
streetViewCoverOn: false,
iconCollection: {
iconGreen: {
url: STATIC_ROOT + '/img/markers/marker-green.svg?rev' + REVISION,
scaledSize: new google.maps.Size(24, 32), // scaled size
origin: new google.maps.Point(0, 0), // origin
anchor: new google.maps.Point(12, 32) // anchor
},
iconRed: {
url: STATIC_ROOT + '/img/markers/marker-red.svg?rev' + REVISION,
scaledSize: new google.maps.Size(24, 32), // scaled size
origin: new google.maps.Point(0, 0), // origin
anchor: new google.maps.Point(12, 32) // anchor
},
iconBlue: {
url: STATIC_ROOT + '/img/markers/marker-blue.svg?rev' + REVISION,
scaledSize: new google.maps.Size(24, 32), // scaled size
origin: new google.maps.Point(0, 0), // origin
anchor: new google.maps.Point(12, 32) // anchor
}
},
init: function (divId, places) {
document.getElementById(divId).style.display = "block";
if (!GMapWrapper.map) {
GMapWrapper.divId = divId;
GMapWrapper.map = new google.maps.Map(document.getElementById(GMapWrapper.divId), {
center: { lat: 0., lng: 0. },
zoom: 2,
fullscreenControl: false,
zoomControl: true,
zoomControlOptions: {
position: google.maps.ControlPosition.LEFT_BOTTOM
},
draggableCursor: 'crosshair'
});
GMapWrapper.streetViewCover = new google.maps.StreetViewCoverageLayer();
GMapWrapper.map.addListener('click', function (mapsMouseEvent) {
GMapWrapper.placeMarker({
lat: mapsMouseEvent.latLng.lat(),
lng: mapsMouseEvent.latLng.lng()
});
});
if (mapId) {
GMapWrapper.map.fitBounds({ south: mapBounds.south, west: mapBounds.west, north: mapBounds.north, east: mapBounds.east });
}
}
GMapWrapper.loadMarkers(places);
document.getElementById('streetViewCoverSelector').disabled = false;
},
hide: function () {
document.getElementById(GMapWrapper.divId).style.display = 'none';
},
loadMarkers: function (places) {
if (!GMapWrapper.markers) {
GMapWrapper.markers = new MarkerClusterer(GMapWrapper.map, [], {
imagePath: STATIC_ROOT + '/img/markers/m',
imageExtension: 'png?rev' + REVISION
});
} else {
GMapWrapper.markers.clearMarkers();
}
for (var placeId in places) {
if (!places.hasOwnProperty(placeId)) {
continue;
}
var place = places[placeId];
var marker = new google.maps.Marker({
position: {
lat: place.lat,
lng: place.lng
},
icon: place.noPano ? GMapWrapper.iconCollection.iconRed : GMapWrapper.iconCollection.iconGreen
});
marker.getLatLng = function () { return { lat: this.getPosition().lat(), lng: this.getPosition().lng() } };
marker.setLatLng = function (coords) { this.setPosition(coords) };
marker.addListener('click', function () {
MapEditor.select(this);
});
marker.placeId = placeId;
GMapWrapper.markers.addMarker(marker);
}
},
// TODO: check whether marker is already existing on the map for the coordinates
// or alternatively block saving for matching coordinates
placeMarker: function (latLng) {
var marker = new google.maps.Marker({
map: GMapWrapper.map,
position: latLng,
icon: GMapWrapper.iconCollection.iconBlue,
});
marker.getLatLng = function () { return { lat: this.getPosition().lat(), lng: this.getPosition().lng() } };
marker.setLatLng = function (coords) { this.setPosition(coords) };
marker.addListener('click', function () {
MapEditor.select(this);
});
MapEditor.select(marker);
});
GMapWrapper.markers.addMarker(marker);
var highResData = Util.getHighResData();
MapEditor.select(marker);
},
L.tileLayer(tileUrl, {
attribution: tileAttribution,
subdomains: '1234',
ppi: highResData.ppi,
tileSize: highResData.tileSize,
zoomOffset: highResData.zoomOffset,
minZoom: highResData.minZoom,
maxZoom: highResData.maxZoom
}).addTo(MapEditor.map);
panTo: function (latLng) {
GMapWrapper.map.panTo(latLng);
},
MapEditor.map.fitBounds(L.latLngBounds({ lat: mapBounds.south, lng: mapBounds.west }, { lat: mapBounds.north, lng: mapBounds.east }));
resize: function () {
google.maps.event.trigger(GMapWrapper.map, 'resize');
},
MapEditor.markers = L.markerClusterGroup({
maxClusterRadius: 50
});
changeMarkerIcon: function (marker, icon) {
marker.setIcon(icon);
},
for (var placeId in places) {
if (!places.hasOwnProperty(placeId)) {
continue;
removeMarker: function (marker) {
GMapWrapper.markers.removeMarker(marker);
},
toggleStreetViewCover: function () {
if (GMapWrapper.streetViewCoverOn) {
GMapWrapper.streetViewCover.setMap(null);
GMapWrapper.streetViewCoverOn = false;
} else {
GMapWrapper.streetViewCover.setMap(GMapWrapper.map);
GMapWrapper.streetViewCoverOn = true;
}
}
};
var place = places[placeId];
var marker = L.marker({ lat: place.lat, lng: place.lng }, {
icon: place.noPano ? IconCollection.iconRed : IconCollection.iconGreen,
zIndexOffset: 1000
})
.addTo(MapEditor.markers)
.on('click', function () {
MapEditor.select(this);
});
marker.placeId = place.id;
}
MapEditor.map.addLayer(MapEditor.markers);
// initialize content of #map with google maps
MapEditor.map = GMapWrapper;
MapEditor.map.init('gmap', places);
MapEditor.panorama = new google.maps.StreetViewPanorama(document.getElementById('panorama'), {
// switch off fullscreenControl because positioning doesn't work
@@ -440,4 +674,48 @@
document.getElementById('deleteButton').onclick = function () {
MapEditor.deletePlace();
};
document.getElementById('jumpButton').onclick = function (e) {
var coordinatesStr = document.getElementById("jumpCoordinates").value;
var coordinates = Util.extractCoordinates(coordinatesStr);
if (coordinates.valid) {
MapEditor.map.placeMarker(coordinates.latlng);
}
};
document.getElementById('jumpCoordinates').onkeyup = function (e) {
var coordinatesStr = document.getElementById("jumpCoordinates").value;
var coordinates = Util.extractCoordinates(coordinatesStr);
var jumpButton = document.getElementById("jumpButton");
if (coordinates.valid) {
jumpButton.disabled = false;
if (e.key == 'Enter') {
MapEditor.map.placeMarker(coordinates.latlng);
}
}
else {
jumpButton.disabled = true;
}
};
document.getElementById('mapSelector').onclick = function () {
MapEditor.closePlace();
MapEditor.map.hide();
if (MapEditor.map === GMapWrapper) {
MapEditor.map = LMapWrapper;
MapEditor.map.init('lmap', places);
} else {
MapEditor.map = GMapWrapper;
MapEditor.map.init('gmap', places);
}
}
document.getElementById('streetViewCoverSelector').onclick = function () {
MapEditor.map.toggleStreetViewCover();
}
})();
+1
View File
@@ -1,4 +1,5 @@
var MapGuesser = {
isSecure: window.location.protocol === 'https:',
cookiesAgreed: false,
sessionAvailableHooks: {},
+36
View File
@@ -73,4 +73,40 @@
window.onresize = function () {
Maps.calculateDescriptionDivHeights();
};
document.getElementById('multiForm').onsubmit = function (e) {
e.preventDefault();
var roomId = this.elements.roomId.value;
if (roomId.length !== 6) {
return;
}
window.location.href = '/multiGame/' + this.elements.roomId.value;
};
document.getElementById('multiButton').onclick = function () {
MapGuesser.showModal('multi');
document.getElementById('createNewRoomButton').href = '/multiGame/new/' + this.dataset.mapId;
document.getElementById('multiForm').elements.roomId.select();
}
document.getElementById('closePlayModeButton').onclick = function () {
MapGuesser.hideModal();
};
document.getElementById('closeMultiButton').onclick = function () {
MapGuesser.hideModal();
};
var buttons = document.getElementById('mapContainer').getElementsByClassName('playButton');
for (var i = 0; i < buttons.length; i++) {
var button = buttons[i];
button.onclick = function () {
MapGuesser.showModal('playMode');
document.getElementById('singleButton').href = '/game/' + this.dataset.mapId;
document.getElementById('multiButton').dataset.mapId = this.dataset.mapId;
};
}
})();
+17 -2
View File
@@ -59,10 +59,25 @@ def getRevisionForRef(ref):
return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip()
def getLatestReleaseTag():
return subprocess.check_output(["git", "for-each-ref", "refs/tags/Release*", "--count=1", "--sort=-creatordate", "--format=%(refname:short)"], cwd=REPO).decode().strip()
process = subprocess.Popen(["git", "for-each-ref", "refs/tags/Release*", "--sort=-creatordate", "--format=%(refname:short)"], stdout=subprocess.PIPE, cwd=REPO)
for line in process.stdout:
tag = line.decode().rstrip()
if isTagVerified(tag):
return tag
print(f"[WARNING] Tag '{tag}' is not verified, skipping.")
raise Exception("No verified 'Release*' tag found!")
def isTagVerified(tag):
process = subprocess.run(["git", "tag", "--verify", tag], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=REPO)
return process.returncode == 0
def updateRepoFromRemote():
subprocess.call(["git", "fetch", "origin", "--prune"], cwd=REPO)
subprocess.call(["git", "fetch", "origin", "--prune", "--prune-tags"], cwd=REPO)
def checkoutWorktree(worktreePath, ref):
subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath)
+7 -1
View File
@@ -9,11 +9,14 @@ if [ -f ${ROOT_DIR}/installed ]; then
exit 1
fi
echo "Installing NPM packages..."
(cd ${ROOT_DIR}/multi && npm install)
echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
echo "Installing MapGuesser DB..."
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/db/mapguesser.sql
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/database/mapguesser.sql
echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg db:migrate)
@@ -24,6 +27,9 @@ if [ -z "${DEV}" ] || [ "${DEV}" -eq "0" ]; then
echo "Linking view files..."
(cd ${ROOT_DIR} && ./mapg view:link)
else
echo "Creating the first user..."
(cd ${ROOT_DIR} && ./mapg user:add mapg@mapg.dev 123456 admin)
fi
touch ${ROOT_DIR}/installed
+3
View File
@@ -11,6 +11,9 @@ else
(cd ${ROOT_DIR} && composer install --dev)
fi
echo "Installing NPM packages..."
(cd ${ROOT_DIR}/multi && npm install)
echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install)
+12
View File
@@ -5,6 +5,7 @@ use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserRepository;
@@ -22,6 +23,8 @@ class MaintainDatabaseCommand extends Command
private UserPasswordResetterRepository $userPasswordResetterRepository;
private MultiRoomRepository $multiRoomRepository;
public function __construct()
{
parent::__construct();
@@ -30,6 +33,7 @@ class MaintainDatabaseCommand extends Command
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->multiRoomRepository = new MultiRoomRepository();
}
public function configure(): void
@@ -43,6 +47,7 @@ class MaintainDatabaseCommand extends Command
try {
$this->deleteInactiveExpiredUsers();
$this->deleteExpiredPasswordResetters();
$this->deleteExpiredRooms();
$this->deleteExpiredSessions();
} catch (\Exception $e) {
$output->writeln('<error>Maintenance failed!</error>');
@@ -89,6 +94,13 @@ class MaintainDatabaseCommand extends Command
}
}
private function deleteExpiredRooms(): void
{
foreach ($this->multiRoomRepository->getAllExpired() as $multiRoom) {
$this->pdm->deleteFromDb($multiRoom);
}
}
private function deleteExpiredSessions(): void
{
//TODO: model may be used for sessions too
+125 -7
View File
@@ -1,20 +1,37 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use Faker\Factory;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\Model\MultiRoom;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Response\Redirect;
class GameController
{
private IRequest $request;
private PersistentDataManager $pdm;
private MultiConnector $multiConnector;
private MultiRoomRepository $multiRoomRepository;
private MapRepository $mapRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->multiConnector = new MultiConnector();
$this->multiRoomRepository = new MultiRoomRepository();
$this->mapRepository = new MapRepository();
}
@@ -22,30 +39,131 @@ class GameController
{
$mapId = (int) $this->request->query('mapId');
return new HtmlContent('game', $this->prepareGame($mapId));
return new HtmlContent('game', ['mapId' => $mapId]);
}
public function getGameJson(): IContent
public function getNewMultiGame(): IRedirect
{
$mapId = (int) $this->request->query('mapId');
$map = $this->mapRepository->getById($mapId);
$roomId = bin2hex(random_bytes(3));
$token = $this->getMultiToken($roomId);
return new JsonContent($this->prepareGame($mapId));
$room = new MultiRoom();
$room->setRoomId($roomId);
$room->setStateArray([
'mapId' => $mapId,
'area' => $map->getArea(),
'rounds' => [],
'currentRound' => -1
]);
$room->setMembersArray(['owner' => $token, 'all' => []]);
$room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room);
$this->multiConnector->sendMessage('create_room', ['roomId' => $roomId]);
return new Redirect(
\Container::$routeCollection
->getRoute('multiGame')
->generateLink(['roomId' => $roomId]),
IRedirect::TEMPORARY
);
}
private function prepareGame(int $mapId): array
public function getMultiGame(): IContent
{
$map = $this->mapRepository->getById($mapId);
$roomId = $this->request->query('roomId');
return new HtmlContent('game', ['roomId' => $roomId]);
}
public function prepareGame(): IContent
{
$mapId = (int) $this->request->query('mapId');
$map = $this->mapRepository->getById($mapId);
$session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$session->set('state', [
'mapId' => $mapId,
'area' => $map->getArea(),
'rounds' => []
'rounds' => [],
'currentRound' => -1
]);
} else { // update the area of the map in the session in any case
$state['area'] = $map->getArea();
$session->set('state', $state);
}
return ['mapId' => $mapId, 'mapName' => $map->getName(), 'bounds' => $map->getBounds()->toArray()];
return new JsonContent([
'mapId' => $mapId,
'mapName' => $map->getName(),
'bounds' => $map->getBounds()->toArray()
]);
}
public function prepareMultiGame(): IContent
{
$roomId = $this->request->query('roomId');
$userName = $this->request->post('userName');
if (empty($userName)) {
$faker = Factory::create();
$userName = $faker->userName;
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$map = $this->mapRepository->getById($state['mapId']);
$token = $this->getMultiToken($roomId);
$members = $room->getMembersArray();
if (!in_array($token, $members['all'])) {
if ($state['currentRound'] >= 0) {
return new JsonContent(['error' => 'game_already_started']);
}
$members['all'][] = $token;
}
$room->setMembersArray($members);
$room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room);
$this->multiConnector->sendMessage('join_room', [
'roomId' => $roomId,
'token' => $token,
'userName' => $userName
]);
return new JsonContent([
'roomId' => $roomId,
'token' => $token,
'owner' => $members['owner'] == $token,
'mapId' => $state['mapId'],
'mapName' => $map->getName(),
'bounds' => $map->getBounds()->toArray()
]);
}
private function getMultiToken(string $roomId): string
{
$session = $this->request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
$token = bin2hex(random_bytes(16));
$session->set('multiState', [
'roomId' => $roomId,
'token' => $token
]);
} else {
$token = $multiState['token'];
}
return $token;
}
}
+175 -75
View File
@@ -1,10 +1,13 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Util\Geo\Position;
use MapGuesser\Response\JsonContent;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\PlaceRepository;
class GameFlowController
@@ -14,129 +17,226 @@ class GameFlowController
private IRequest $request;
private PersistentDataManager $pdm;
private MultiConnector $multiConnector;
private MultiRoomRepository $multiRoomRepository;
private PlaceRepository $placeRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->multiConnector = new MultiConnector();
$this->multiRoomRepository = new MultiRoomRepository();
$this->placeRepository = new PlaceRepository();
}
public function getNewPlace(): IContent
public function initialData(): IContent
{
$mapId = (int) $this->request->query('mapId');
$session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
return new JsonContent(['error' => 'no_session_found']);
}
if (count($state['rounds']) === 0) {
$placesWithoutPano = [];
$place = $this->placeRepository->getRandomForMapWithValidPano($mapId, [], $placesWithoutPano);
$this->addNewRoundToState($state, $place, $placesWithoutPano);
if (!isset($state['currentRound']) || $state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS) {
$this->startNewGame($state, $mapId);
$session->set('state', $state);
}
$data = [
'panoId' => $place->getPanoIdCached(),
'pov' => $place->getPov()->toArray()
];
} else {
$rounds = count($state['rounds']);
$last = $state['rounds'][$rounds - 1];
$response = [];
$history = [];
for ($i = 0; $i < $rounds - 1; ++$i) {
$round = $state['rounds'][$i];
$history[] = [
'position' => $round['position']->toArray(),
$last = $state['rounds'][$state['currentRound']];
$response['place'] = [
'panoId' => $last['panoId'],
'pov' => $last['pov']->toArray()
];
$response['history'] = [];
for ($i = 0; $i < $state['currentRound']; ++$i) {
$round = $state['rounds'][$i];
$response['history'][] = [
'position' => $round['position']->toArray(),
'result' => [
'guessPosition' => $round['guessPosition']->toArray(),
'distance' => $round['distance'],
'score' => $round['score']
];
}
$data = [
'history' => $history,
'panoId' => $last['panoId'],
'pov' => isset($last['pov']) ? // should be checked not to break with old sessions
$last['pov']->toArray() :
['heading' => 0.0, 'pitch' => 0.0, 'zoom' => 0.0],
]
];
}
return new JsonContent($data);
return new JsonContent($response);
}
public function evaluateGuess(): IContent
public function multiInitialData(): IContent
{
$roomId = $this->request->query('roomId');
$session = $this->request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']);
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$members = $room->getMembersArray();
if ($members['owner'] !== $multiState['token']) {
return new JsonContent(['error' => 'not_owner_of_room']);
}
if ($state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS - 1) {
$this->startNewGame($state, $state['mapId']);
$room->setStateArray($state);
$room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room);
}
$places = [];
foreach ($state['rounds'] as $round) {
$places[] = [
'position' => $round['position']->toArray(),
'panoId' => $round['panoId'],
'pov' => $round['pov']->toArray()
];
}
$this->multiConnector->sendMessage('start_game', ['roomId' => $roomId, 'places' => $places]);
return new JsonContent(['ok' => true]);
}
public function guess(): IContent
{
$mapId = (int) $this->request->query('mapId');
$session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
return new JsonContent(['error' => 'no_session_found']);
}
$last = $state['rounds'][count($state['rounds']) - 1];
$position = $last['position'];
$last = $state['rounds'][$state['currentRound']];
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
$distance = $this->calculateDistance($position, $guessPosition);
$score = $this->calculateScore($distance, $state['area']);
$result = $this->evalueteGuess($last['position'], $guessPosition, $state['area']);
$last['guessPosition'] = $guessPosition;
$last['distance'] = $distance;
$last['score'] = $score;
$state['rounds'][count($state['rounds']) - 1] = $last;
$last['distance'] = $result['distance'];
$last['score'] = $result['score'];
if (count($state['rounds']) < static::NUMBER_OF_ROUNDS) {
$exclude = [];
$response = [
'position' => $last['position']->toArray(),
'result' => $result
];
foreach ($state['rounds'] as $round) {
$exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]);
}
$state['rounds'][$state['currentRound']] = $last;
$state['currentRound'] += 1;
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
$next = $state['rounds'][$state['currentRound']];
$placesWithoutPano = [];
$place = $this->placeRepository->getRandomForMapWithValidPano($mapId, $exclude, $placesWithoutPano);
$this->addNewRoundToState($state, $place, $placesWithoutPano);
$panoId = $place->getPanoIdCached();
$pov = $place->getPov()->toArray();
} else {
$state['rounds'] = [];
$panoId = null;
$pov = null;
$response['place'] = [
'panoId' => $next['panoId'],
'pov' => $next['pov']->toArray()
];
}
$session->set('state', $state);
return new JsonContent([
'result' => [
'position' => $position->toArray(),
'distance' => $distance,
'score' => $score
],
'panoId' => $panoId,
'pov' => $pov
]);
return new JsonContent($response);
}
private function addNewRoundToState(array &$state, Place $place, array $placesWithoutPano): void
public function multiGuess(): IContent
{
$state['rounds'][] = [
'placesWithoutPano' => $placesWithoutPano,
'placeId' => $place->getId(),
'position' => $place->getPosition(),
'panoId' => $place->getPanoIdCached(),
'pov' => $place->getPov()
$roomId = $this->request->query('roomId');
$session = $this->request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']);
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$last = $state['rounds'][$state['currentRound']];
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
$result = $this->evalueteGuess($last['position'], $guessPosition, $state['area']);
$responseFromMulti = $this->multiConnector->sendMessage('guess', [
'roomId' => $roomId,
'token' => $multiState['token'],
'guessPosition' => $guessPosition->toArray(),
'distance' => $result['distance'],
'score' => $result['score']
]);
if (isset($responseFromMulti['error'])) {
return new JsonContent(['error' => $responseFromMulti['error']]);
}
$response = [
'position' => $last['position']->toArray(),
'result' => $result,
'allResults' => $responseFromMulti['allResults']
];
return new JsonContent($response);
}
public function multiNextRound(): IContent
{
$roomId = $this->request->query('roomId');
$session = $this->request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']);
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$members = $room->getMembersArray();
if ($members['owner'] !== $multiState['token']) {
return new JsonContent(['error' => 'not_owner_of_room']);
}
$state['currentRound'] += 1;
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
$this->multiConnector->sendMessage('next_round', ['roomId' => $roomId, 'currentRound' => $state['currentRound']]);
}
$room->setStateArray($state);
$room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room);
return new JsonContent(['ok' => true]);
}
private function evalueteGuess(Position $realPosition, Position $guessPosition, float $area)
{
$distance = $this->calculateDistance($realPosition, $guessPosition);
$score = $this->calculateScore($distance, $area);
return ['distance' => $distance, 'score' => $score];
}
private function startNewGame(array &$state, int $mapId): void
{
$places = $this->placeRepository->getRandomNForMapWithValidPano($mapId, static::NUMBER_OF_ROUNDS);
$state['rounds'] = [];
$state['currentRound'] = 0;
foreach ($places as $place) {
$state['rounds'][] = [
'placeId' => $place->getId(),
'position' => $place->getPosition(),
'panoId' => $place->getPanoIdCached(),
'pov' => $place->getPov()
];
}
}
private function calculateDistance(Position $realPosition, Position $guessPosition): float
+22
View File
@@ -0,0 +1,22 @@
<?php namespace MapGuesser\Multi;
class MultiConnector
{
public function sendMessage(string $func, array $args = [])
{
$message = json_encode([
'func' => $func,
'args' => $args
]);
$connection = fsockopen($_ENV['MULTI_INTERNAL_HOST'], $_ENV['MULTI_INTERNAL_PORT']);
fwrite($connection, $message);
$response = '';
while (!feof($connection)) {
$response .= fgets($connection);
}
fclose($connection);
return json_decode($response, true);
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
class MultiRoom extends Model
{
protected static string $table = 'multi_rooms';
protected static array $fields = ['room_id', 'state', 'members', 'updated'];
private string $roomId = '';
private array $state = [];
private array $members = [];
private DateTime $updated;
public function setRoomId(string $roomId): void
{
$this->roomId = $roomId;
}
public function setStateArray(array $state): void
{
$this->state = $state;
}
public function setMembersArray(array $members): void
{
$this->members = $members;
}
public function setState(string $state): void
{
$this->state = unserialize($state);
}
public function setMembers(string $members): void
{
$this->members = unserialize($members);
}
public function setUpdatedDate(DateTime $updated): void
{
$this->updated = $updated;
}
public function setUpdated(string $updated): void
{
$this->updated = new DateTime($updated);
}
public function getRoomId(): string
{
return $this->roomId;
}
public function getStateArray(): array
{
return $this->state;
}
public function getState(): string
{
return serialize($this->state);
}
public function getMembersArray(): array
{
return $this->members;
}
public function getMembers(): string
{
return serialize($this->members);
}
public function getUpdatedDate(): DateTime
{
return $this->updated;
}
public function getUpdated(): string
{
return $this->updated->format('Y-m-d H:i:s');
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php namespace MapGuesser\Repository;
use DateTime;
use Generator;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\MultiRoom;
use MapGuesser\PersistentData\PersistentDataManager;
class MultiRoomRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $id): ?MultiRoom
{
return $this->pdm->selectFromDbById($id, MultiRoom::class);
}
public function getByRoomId(string $roomId): ?MultiRoom
{
$select = new Select(\Container::$dbConnection);
$select->where('room_id', '=', $roomId);
return $this->pdm->selectFromDb($select, MultiRoom::class);
}
public function getAllExpired(): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('updated', '<', (new DateTime('-7 day'))->format('Y-m-d H:i:s'));
yield from $this->pdm->selectMultipleFromDb($select, MultiRoom::class);
}
}
+16 -4
View File
@@ -29,17 +29,29 @@ class PlaceRepository
}
//TODO: use Map instead of id
public function getRandomForMapWithValidPano(int $mapId, array $exclude = [], array &$placesWithoutPano): Place
public function getRandomNForMapWithValidPano(int $mapId, int $n, array $exclude = []): array
{
$placesWithoutPano = [];
$places = [];
for ($i = 1; $i <= $n; ++$i) {
$place = $this->getRandomForMapWithValidPano($mapId, $exclude);
$places[] = $place;
$exclude[] = $place->getId();
}
return $places;
}
//TODO: use Map instead of id
public function getRandomForMapWithValidPano(int $mapId, array $exclude = []): Place
{
do {
$place = $this->selectRandomFromDbForMap($mapId, $exclude);
$panoId = $place->getFreshPanoId();
if ($panoId === null) {
$placesWithoutPano[] = $exclude[] = $place->getId();
$exclude[] = $place->getId();
}
} while ($panoId === null);
+20 -2
View File
@@ -6,12 +6,27 @@
@js(node_modules/leaflet/dist/leaflet.js)
@js(node_modules/leaflet.markercluster/dist/leaflet.markercluster.js)
@js(https://maps.googleapis.com/maps/api/js?key=<?= $_ENV['GOOGLE_MAPS_JS_API_KEY'] ?>)
@js(https://unpkg.com/@googlemaps/markerclustererplus/dist/index.min.js)
@js(js/map_editor.js)
@extends(templates/layout_full)
@section(subheader)
@section(subheader)
<span><a href="javascript:;" id="mapName" title="Edit map data"><?= $mapName ?></a></span><!--
--><span class="inline hideOnMobile" id="mapSelection">
<button id="mapSelector">
<img src="<?= $_ENV['STATIC_ROOT'] ?>/img/map.svg?rev=<?= REVISION ?>" alt="Map Selector" />
</button>
</span><!--
--><span class="inline hideOnMobile" id="mapSelection">
<button id="streetViewCoverSelector">
<img src="<?= $_ENV['STATIC_ROOT'] ?>/img/street-view-cover.svg?rev=<?= REVISION ?>" alt="Street View Conver" />
</button>
</span><!--
--><span class="inline hideOnMobile">
<input type="text" id="jumpCoordinates" placeholder="Insert coordinates here" />
<button id="jumpButton" disabled >Jump</button>
</span><!--
--><span><!--
<?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?>
--><svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
@@ -53,7 +68,10 @@
@endsection
@section(main)
<div id="map"></div>
<div id="map" class="map">
<div id="lmap" class="map"></div>
<div id="gmap" class="map"></div>
</div>
<div id="panorama"></div>
<div id="noPano">
<p class="bold">No panorama is available for this location.</p>
+37 -5
View File
@@ -5,13 +5,25 @@
@extends(templates/layout_full)
@section(pagemodal)
<div id="multi" class="modal">
<h2>Multiplayer (beta)</h2>
<p class="marginTop">Waiting for players...</p>
<div id="players" class="marginTop"></div>
<button id="startMultiGameButton" class="button fullWidth marginTop green">Start game</button>
</div>
@endsection
@section(subheader)
<span id="mapName" class="bold"><?= $mapName ?></span><!--
--><span>Round <span id="currentRound" class="bold"></span></span><!--
--><span>Score <span id="currentScoreSum" class="bold"></span></span>
<span id="mapName" class="bold">Loading map...</span><!--
--><span>Round <span id="currentRound" class="bold"></span></span><!--
--><span>Score <span id="currentScoreSum" class="bold"></span></span>
@endsection
@section(main)
<div id="countdown">
<p id="countdownTime" class="mono bold"></p>
</div>
<div id="panoCover"></div>
<div id="panorama"></div>
<div id="showGuessButtonContainer">
@@ -28,6 +40,7 @@
<div id="resultInfo">
<div id="distanceInfo">
<p>You were <span id="distance" class="bold"></span> close.</p>
<p>You didn't guess in this round.</p>
<p class="bold">Game finished.</p>
</div>
<div id="scoreInfo">
@@ -46,11 +59,30 @@
<button id="startNewGameButton" class="fullWidth">Play this map again</button>
</div>
</div>
<div id="navigation">
<div id="compassContainer" class="navigationItem">
<div>
<img src="<?= $_ENV['STATIC_ROOT'] ?>/img/circle_background.svg?rev=<?= REVISION ?>" alt="Circle Background" class="circleBackground" />
</div>
<div>
<img id="compass" src="<?= $_ENV['STATIC_ROOT'] ?>/img/compass.svg?rev=<?= REVISION ?>" alt="compass icon" class="navigationIcon" />
</div>
</div>
<div id="returnToStart" class="navigationItem">
<div>
<img src="<?= $_ENV['STATIC_ROOT'] ?>/img/circle_background.svg?rev=<?= REVISION ?>" alt="Circle Background" class="circleBackground" />
</div>
<div>
<img src="<?= $_ENV['STATIC_ROOT'] ?>/img/house-fill.svg?rev=<?= REVISION ?>" alt="Return to Start icon" class="navigationIcon" />
</div>
</div>
</div>
@endsection
@section(pageScript)
<script>
var mapId = <?= $mapId ?>;
var mapBounds = <?= json_encode($bounds) ?>;
var multiUrl = '<?= $_ENV['MULTI_WS_URL'] ?>';
var roomId = <?= isset($roomId) ? '\'' . $roomId . '\'' : 'null' ?>;
var mapId = <?= isset($mapId) ? '\'' . $mapId . '\'' : 'null' ?>;
</script>
@endsection
+33 -7
View File
@@ -5,6 +5,32 @@ TODO: condition!
@extends(templates/layout_normal)
@section(pagemodal)
<div id="playMode" class="modal">
<h2>Play map</h2>
<a id="singleButton" class="button fullWidth marginTop" href="" title="Single player">Single player</a>
<p class="bold center marginTop marginBottom">OR</p>
<button id="multiButton" class="fullWidth green" data-map-id="">Multiplayer (beta)</button>
<div class="right">
<button id="closePlayModeButton" class="gray marginTop" type="button">Close</button>
</div>
</div>
<div id="multi" class="modal">
<h2>Multiplayer (beta)</h2>
<form id="multiForm" class="marginTop" data-no-submit="true">
<a id="createNewRoomButton" class="button fullWidth green" href="" title="Create new room">Create new room</a>
<p class="bold center marginTop marginBottom">OR</p>
<div class="inputWithButton">
<input type="text" name="roomId" placeholder="Room to connect" required minlength="6" maxlength="6"><!--
--><button id="authenticateWithGoogleButton" type="submit">Connect</button>
</div>
<div class="right">
<button id="closeMultiButton" class="gray marginTop" type="button">Close</button>
</div>
</form>
</div>
@endsection
@section(main)
<div id="mapContainer">
<?php foreach ($maps as $map): ?>
@@ -35,15 +61,15 @@ TODO: condition!
<p class="small center"><?= $map['description'] ?></p>
</div>
</div>
<?php if ($isAdmin): ?>
<div class="buttonContainer">
<a class="button fullWidth noRightRadius" href="/game/<?= $map['id']; ?>" title="Play map '<?= $map['name'] ?>'">Play this map</a>
<div class="buttonContainer">
<?php if ($isAdmin): ?>
<button class="button fullWidth noRightRadius playButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Play map '<?= $map['name'] ?>'">Play this map</button>
<a class="button yellow fullWidth noLeftRadius noRightRadius" href="/admin/mapEditor/<?= $map['id']; ?>" title="Edit map '<?= $map['name'] ?>'">Edit</a>
<button class="button red fullWidth noLeftRadius deleteButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Delete map '<?= $map['name'] ?>'">Delete</button>
</div>
<?php else: ?>
<a class="button fullWidth" href="/game/<?= $map['id']; ?>" title="Play map '<?= $map['name'] ?>'">Play this map</a>
<?php endif; ?>
<?php else: ?>
<button class="button fullWidth playButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Play map '<?= $map['name'] ?>'">Play this map</button>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php if ($isAdmin): ?>
+1 -1
View File
@@ -29,6 +29,6 @@
</main>
<footer>
<p><span class="bold"><?= $_ENV['APP_NAME'] ?></span> <?= str_replace('Release_', '', VERSION) ?></p><!--
--><p>&copy; Pőcze Bence <?= (new DateTime(REVISION_DATE))->format('Y') ?></p>
--><p>&copy; The MapGuesser Contributors <?= (new DateTime(REVISION_DATE))->format('Y') ?></p>
</footer>
@endsection
+1 -1
View File
@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#198231">
<title><?= $_ENV['APP_NAME'] ?></title>
<?php if (preg_match('/^http(s)?/', $_ENV['STATIC_ROOT']) === 1): ?>
<?php if (preg_match('/^(http(s)?:)?\/\//', $_ENV['STATIC_ROOT']) === 1): ?>
<link href="<?= $_ENV['STATIC_ROOT'] ?>" rel="preconnect">
<?php endif; ?>
<link href="https://fonts.gstatic.com" rel="preconnect">
+11 -3
View File
@@ -50,9 +50,17 @@ Container::$routeCollection->group('account', function (MapGuesser\Routing\Route
//Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']);
Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('game', '{mapId}', [MapGuesser\Controller\GameController::class, 'getGame']);
$routeCollection->get('game-json', '{mapId}/json', [MapGuesser\Controller\GameController::class, 'getGameJson']);
$routeCollection->get('newPlace-json', '{mapId}/newPlace.json', [MapGuesser\Controller\GameFlowController::class, 'getNewPlace']);
$routeCollection->post('guess-json', '{mapId}/guess.json', [MapGuesser\Controller\GameFlowController::class, 'evaluateGuess']);
$routeCollection->post('game.prepare-json', '{mapId}/prepare.json', [MapGuesser\Controller\GameController::class, 'prepareGame']);
$routeCollection->post('game.initialData-json', '{mapId}/initialData.json', [MapGuesser\Controller\GameFlowController::class, 'initialData']);
$routeCollection->post('game.guess-json', '{mapId}/guess.json', [MapGuesser\Controller\GameFlowController::class, 'guess']);
});
Container::$routeCollection->group('multiGame', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('multiGame.new', 'new/{mapId}', [MapGuesser\Controller\GameController::class, 'getNewMultiGame']);
$routeCollection->get('multiGame', '{roomId}', [MapGuesser\Controller\GameController::class, 'getMultiGame']);
$routeCollection->post('multiGame.prepare-json', '{roomId}/prepare.json', [MapGuesser\Controller\GameController::class, 'prepareMultiGame']);
$routeCollection->post('multiGame.initialData-json', '{roomId}/initialData.json', [MapGuesser\Controller\GameFlowController::class, 'multiInitialData']);
$routeCollection->post('multiGame.nextRound-json', '{roomId}/nextRound.json', [MapGuesser\Controller\GameFlowController::class, 'multiNextRound']);
$routeCollection->post('multiGame.guess-json', '{roomId}/guess.json', [MapGuesser\Controller\GameFlowController::class, 'multiGuess']);
});
Container::$routeCollection->group('admin', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('admin.mapEditor', 'mapEditor/{mapId?}', [MapGuesser\Controller\MapAdminController::class, 'getMapEditor']);