rapid-router 5.18.0__py2.py3-none-any.whl → 7.6.8__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. example_project/rapid_router_test_settings.py +19 -7
  2. example_project/settings.py +21 -8
  3. example_project/urls.py +5 -6
  4. game/__init__.py +1 -1
  5. game/admin.py +7 -2
  6. game/character.py +8 -0
  7. game/decor.py +40 -0
  8. game/end_to_end_tests/base_game_test.py +34 -27
  9. game/end_to_end_tests/editor_page.py +15 -0
  10. game/end_to_end_tests/game_page.py +88 -20
  11. game/end_to_end_tests/selenium_test_case.py +1 -20
  12. game/end_to_end_tests/test_cow_crashes.py +3 -5
  13. game/end_to_end_tests/test_level_editor.py +273 -10
  14. game/end_to_end_tests/test_level_selection.py +25 -3
  15. game/end_to_end_tests/test_play_through.py +222 -127
  16. game/end_to_end_tests/test_python_levels.py +41 -7
  17. game/end_to_end_tests/test_saving_workspace.py +2 -1
  18. game/forms.py +7 -1
  19. game/level_management.py +26 -11
  20. game/messages.py +899 -337
  21. game/migrations/0001_squashed_0025_levels_ordering_pt1.py +19 -1
  22. game/migrations/0026_levels_pt2.py +13 -2
  23. game/migrations/0032_cannot_turn_left_level.py +13 -2
  24. game/migrations/0033_recursion_level.py +13 -2
  25. game/migrations/0034_joes_level.py +13 -2
  26. game/migrations/0035_disable_route_score_level_70.py +0 -2
  27. game/migrations/0036_level_score_73.py +0 -2
  28. game/migrations/0037_level_score_79.py +0 -2
  29. game/migrations/0038_level_score_40.py +0 -1
  30. game/migrations/0042_level_score_73.py +0 -2
  31. game/migrations/0048_add_cow_field_and_blocks.py +0 -2
  32. game/migrations/0049_level_score_34.py +0 -2
  33. game/migrations/0050_level_score_40.py +0 -2
  34. game/migrations/0051_level_score_49.py +0 -1
  35. game/migrations/0086_loop_levels.py +13 -2
  36. game/migrations/0092_disable_algo_score_in_custom_levels.py +28 -0
  37. game/migrations/0093_alter_level_character_name.py +18 -0
  38. game/migrations/0094_add_hint_lesson_subtitle_to_levels.py +28 -0
  39. game/migrations/0095_level_commands.py +18 -0
  40. game/migrations/0096_alter_level_commands.py +18 -0
  41. game/migrations/0097_add_python_den_levels.py +1515 -0
  42. game/migrations/0098_add_episode_link_fields.py +44 -0
  43. game/migrations/0099_python_episodes_links.py +103 -0
  44. game/migrations/0100_reorder_python_levels.py +179 -0
  45. game/migrations/0101_rename_episodes.py +45 -0
  46. game/migrations/0102_reoder_episodes_13_14.py +136 -0
  47. game/migrations/0103_level_1015_solution.py +26 -0
  48. game/migrations/0104_remove_level_direct_drive.py +17 -0
  49. game/migrations/0105_delete_invalid_attempts.py +18 -0
  50. game/migrations/0106_fields_to_snake_case.py +48 -0
  51. game/migrations/0107_rename_worksheet_link_episode_student_worksheet_link.py +18 -0
  52. game/migrations/0108_episode_indy_worksheet_link.py +18 -0
  53. game/migrations/0109_create_episodes_23_and_24.py +99 -0
  54. game/migrations/0110_remove_episode_indy_worksheet_link_and_more.py +100 -0
  55. game/migrations/0111_create_worksheets.py +149 -0
  56. game/migrations/0112_worksheet_locked_classes.py +21 -0
  57. game/migrations/0113_level_needs_approval.py +18 -0
  58. game/migrations/0114_default_and_non_student_levels_no_approval.py +31 -0
  59. game/migrations/0115_level_level__default_does_not_need_approval.py +22 -0
  60. game/migrations/0116_update_worksheet_video_links.py +68 -0
  61. game/migrations/0117_update_solutions_to_if_else.py +61 -0
  62. game/models.py +127 -17
  63. game/permissions.py +51 -19
  64. game/python_den_urls.py +26 -0
  65. game/random_road.py +9 -9
  66. game/serializers.py +12 -17
  67. game/static/django_reverse_js/js/reverse.js +171 -0
  68. game/static/game/css/LilitaOne-Regular.ttf +0 -0
  69. game/static/game/css/backgrounds.css +8 -12
  70. game/static/game/css/dataTables.custom.css +3 -2
  71. game/static/game/css/editor.css +47 -0
  72. game/static/game/css/game.css +37 -43
  73. game/static/game/css/game_screen.css +16 -0
  74. game/static/game/css/level_editor.css +5 -0
  75. game/static/game/css/level_selection.css +17 -2
  76. game/static/game/image/Python_Den_hero_student.png +0 -0
  77. game/static/game/image/Python_levels_page.svg +1954 -0
  78. game/static/game/image/characters/front_view/Electric_van.svg +448 -0
  79. game/static/game/image/characters/top_view/Electric_van.svg +448 -0
  80. game/static/game/image/decor/city/solar_panel.svg +1200 -0
  81. game/static/game/image/decor/farm/solar_panel.svg +86 -0
  82. game/static/game/image/decor/grass/solar_panel.svg +86 -0
  83. game/static/game/image/decor/snow/solar_panel.svg +173 -0
  84. game/static/game/image/electric_van.svg +448 -0
  85. game/static/game/image/icons/description.svg +1 -0
  86. game/static/game/image/icons/hint.svg +1 -0
  87. game/static/game/image/icons/python.svg +1 -1
  88. game/static/game/image/pigeon.svg +684 -0
  89. game/static/game/image/python_den_header.svg +19 -0
  90. game/static/game/js/animation.js +65 -24
  91. game/static/game/js/blockly/msg/js/bg.js +52 -1
  92. game/static/game/js/blockly/msg/js/ca.js +52 -1
  93. game/static/game/js/blockly/msg/js/en-gb.js +2 -0
  94. game/static/game/js/blockly/msg/js/en.js +2 -0
  95. game/static/game/js/blockly/msg/js/es.js +52 -1
  96. game/static/game/js/blockly/msg/js/fr.js +2 -0
  97. game/static/game/js/blockly/msg/js/hi.js +2 -0
  98. game/static/game/js/blockly/msg/js/it.js +52 -1
  99. game/static/game/js/blockly/msg/js/pl.js +52 -1
  100. game/static/game/js/blockly/msg/js/pt-br.js +52 -1
  101. game/static/game/js/blockly/msg/js/ru.js +52 -1
  102. game/static/game/js/blockly/msg/js/ur.js +52 -1
  103. game/static/game/js/blocklyCustomBlocks.js +93 -52
  104. game/static/game/js/button.js +12 -0
  105. game/static/game/js/cow.js +11 -7
  106. game/static/game/js/drawing.js +68 -29
  107. game/static/game/js/editor.js +23 -0
  108. game/static/game/js/game.js +74 -110
  109. game/static/game/js/level_editor.js +646 -274
  110. game/static/game/js/level_moderation.js +33 -2
  111. game/static/game/js/level_selection.js +1 -1
  112. game/static/game/js/loadLanguages.js +2 -2
  113. game/static/game/js/model.js +32 -2
  114. game/static/game/js/pythonControl.js +14 -1
  115. game/static/game/js/scoreboard.js +0 -37
  116. game/static/game/js/scoreboardSharedLevels.js +48 -0
  117. game/static/game/js/skulpt/skulpt-stdlib.js +1 -1
  118. game/static/game/js/sound.js +52 -5
  119. game/static/game/raphael_image/characters/top_view/Electric_van.svg +448 -0
  120. game/static/game/raphael_image/decor/city/solar_panel.svg +1200 -0
  121. game/static/game/raphael_image/decor/farm/solar_panel.svg +86 -0
  122. game/static/game/raphael_image/decor/grass/solar_panel.svg +86 -0
  123. game/static/game/raphael_image/decor/snow/solar_panel.svg +173 -0
  124. game/static/game/raphael_image/pigeon.svg +685 -0
  125. game/static/game/sass/game.scss +2 -2
  126. game/static/game/sound/clown_horn.mp3 +0 -0
  127. game/static/game/sound/clown_horn.ogg +0 -0
  128. game/static/game/sound/electric_van_starting.mp3 +0 -0
  129. game/static/game/sound/electric_van_starting.ogg +0 -0
  130. game/static/game/sound/pigeon.mp3 +0 -0
  131. game/static/game/sound/pigeon.ogg +0 -0
  132. game/static/game/sound/sleigh_bells.mp3 +0 -0
  133. game/static/game/sound/sleigh_bells.ogg +0 -0
  134. game/static/game/sound/sleigh_crash.mp3 +0 -0
  135. game/static/game/sound/sleigh_crash.ogg +0 -0
  136. game/templates/game/base.html +34 -14
  137. game/templates/game/basenonav.html +11 -5
  138. game/templates/game/game.html +142 -38
  139. game/templates/game/level_editor.html +340 -236
  140. game/templates/game/level_moderation.html +19 -6
  141. game/templates/game/level_selection.html +18 -110
  142. game/templates/game/python_den_level_selection.html +291 -0
  143. game/templates/game/python_den_worksheet.html +101 -0
  144. game/templates/game/scoreboard.html +83 -64
  145. game/tests/test_level_editor.py +94 -26
  146. game/tests/test_level_selection.py +149 -46
  147. game/tests/test_python_den_worksheet.py +85 -0
  148. game/tests/test_scoreboard.py +34 -7
  149. game/tests/utils/level.py +32 -26
  150. game/theme.py +5 -5
  151. game/urls.py +199 -61
  152. game/views/language_code_conversions.py +86 -86
  153. game/views/level.py +155 -63
  154. game/views/level_editor.py +88 -55
  155. game/views/level_moderation.py +23 -0
  156. game/views/level_selection.py +116 -47
  157. game/views/level_solutions.py +491 -106
  158. game/views/scoreboard.py +76 -51
  159. game/views/worksheet.py +25 -0
  160. rapid_router-7.6.8.dist-info/METADATA +174 -0
  161. {rapid_router-5.18.0.dist-info → rapid_router-7.6.8.dist-info}/RECORD +164 -104
  162. {rapid_router-5.18.0.dist-info → rapid_router-7.6.8.dist-info}/WHEEL +1 -1
  163. example_project/manage.py +0 -10
  164. game/static/game/image/actions/go.svg +0 -18
  165. game/static/game/js/js-reverse.js +0 -14
  166. game/static/game/js/pqselect.min.js +0 -9
  167. game/static/game/js/widget-scroller.js +0 -906
  168. rapid_router-5.18.0.dist-info/METADATA +0 -17
  169. {rapid_router-5.18.0.dist-info → rapid_router-7.6.8.dist-info/licenses}/LICENSE.md +0 -0
  170. {rapid_router-5.18.0.dist-info → rapid_router-7.6.8.dist-info}/top_level.txt +0 -0
@@ -8,6 +8,8 @@ ocargo.LevelEditor = function(levelId) {
8
8
  /* Constants */
9
9
  /*************/
10
10
 
11
+ const TAB_PANE_WIDTH = 500;
12
+
11
13
  var LIGHT_RED_URL = ocargo.Drawing.raphaelImageDir + 'trafficLight_red.svg';
12
14
  var LIGHT_GREEN_URL = ocargo.Drawing.raphaelImageDir + 'trafficLight_green.svg';
13
15
 
@@ -85,6 +87,7 @@ ocargo.LevelEditor = function(levelId) {
85
87
  var originNode = null;
86
88
  var houseNodes = [];
87
89
  var currentTheme = THEMES.grass;
90
+ var needsApproval = false;
88
91
 
89
92
  // Reference to the Raphael elements for each square
90
93
  var grid;
@@ -189,6 +192,8 @@ ocargo.LevelEditor = function(levelId) {
189
192
  tabs.character = new ocargo.Tab($('#character_radio'), $('#character_radio + label'), $('#character_pane'));
190
193
  tabs.blocks = new ocargo.Tab($('#blocks_radio'), $('#blocks_radio + label'), $('#blocks_pane'));
191
194
  tabs.random = new ocargo.Tab($('#random_radio'), $('#random_radio + label'), $('#random_pane'));
195
+ tabs.description = new ocargo.Tab($('#description_radio'), $('#description_radio + label'), $('#description_pane'));
196
+ tabs.hint = new ocargo.Tab($('#hint_radio'), $('#hint_radio + label'), $('#hint_pane'));
192
197
  tabs.load = new ocargo.Tab($('#load_radio'), $('#load_radio + label'), $('#load_pane'));
193
198
  tabs.save = new ocargo.Tab($('#save_radio'), $('#save_radio + label'), $('#save_pane'));
194
199
  tabs.share = new ocargo.Tab($('#share_radio'), $('#share_radio + label'), $('#share_pane'));
@@ -202,6 +207,8 @@ ocargo.LevelEditor = function(levelId) {
202
207
  setupCharacterTab();
203
208
  setupBlocksTab();
204
209
  setupRandomTab();
210
+ setupDescriptionTab();
211
+ setupHintTab();
205
212
  setupLoadTab();
206
213
  setupSaveTab();
207
214
  setupShareTab();
@@ -318,28 +325,25 @@ ocargo.LevelEditor = function(levelId) {
318
325
  }
319
326
  });
320
327
 
321
- $('.decor_button').click(function(e){
322
- new InternalDecor(e.target.id);
323
- });
328
+ $('.decor_button').mousedown(handleDraggableDecorMouseDown);
324
329
 
325
- $('#trafficLightRed').click(function() {
326
- new InternalTrafficLight({"redDuration": 3, "greenDuration": 3, "startTime": 0,
327
- "startingState": ocargo.TrafficLight.RED,
328
- "sourceCoordinate": null, "direction": null});
330
+ $('#trafficLightRed').mousedown(function(e) {
331
+ handleDraggableTrafficLightsMouseDown(e, ocargo.TrafficLight.RED);
329
332
  });
330
333
 
331
- $('#trafficLightGreen').click(function() {
332
- new InternalTrafficLight({"redDuration": 3, "greenDuration": 3, "startTime": 0,
333
- "startingState": ocargo.TrafficLight.GREEN,
334
- "sourceCoordinate": null, "direction": null});
334
+ $('#trafficLightGreen').mousedown(function(e) {
335
+ handleDraggableTrafficLightsMouseDown(e, ocargo.TrafficLight.GREEN);
335
336
  });
336
337
 
337
338
  if(COW_LEVELS_ENABLED) {
338
339
  if (Object.keys(cowGroups).length == 0) {
339
340
  addCowGroup();
340
341
  }
341
- $('#cow').click(function () {
342
- new InternalCow({group: cowGroups["group1"]});
342
+ $('#cow').mouseover(function(e) {
343
+ e.target.style.cursor = "pointer";
344
+ })
345
+ $('#cow').mousedown(function(e) {
346
+ handleDraggableCowMouseDown(e, "group1")
343
347
  });
344
348
  }
345
349
  }
@@ -412,7 +416,8 @@ ocargo.LevelEditor = function(levelId) {
412
416
 
413
417
  for (var i = 0; i < BLOCKS.length; i++) {
414
418
  var type = BLOCKS[i];
415
- var block = Blockly.mainWorkspace.newBlock(type);
419
+ let usePigeons = type === "cow_crossing" && currentTheme == THEMES.city
420
+ var block = usePigeons ? Blockly.mainWorkspace.newBlock("pigeon_crossing_IMAGE_ONLY") : Blockly.mainWorkspace.newBlock(type);
416
421
  block.initSvg();
417
422
  block.render();
418
423
 
@@ -478,6 +483,18 @@ ocargo.LevelEditor = function(levelId) {
478
483
  });
479
484
  }
480
485
 
486
+ function setupDescriptionTab() {
487
+ tabs.description.setOnChange(function() {
488
+ transitionTab(tabs.description);
489
+ });
490
+ }
491
+
492
+ function setupHintTab() {
493
+ tabs.hint.setOnChange(function() {
494
+ transitionTab(tabs.hint);
495
+ });
496
+ }
497
+
481
498
  function goToMapTab() {
482
499
  tabs.map.select();
483
500
  }
@@ -615,17 +632,27 @@ ocargo.LevelEditor = function(levelId) {
615
632
  return;
616
633
  }
617
634
 
618
- var newName = $('#levelNameInput').val();
635
+ const nameInput = $('#levelNameInput')
636
+ const newName = nameInput.val();
619
637
  if (!newName || newName === "") {
620
- // TODO error message?
638
+ ocargo.Drawing.startPopup(
639
+ "Oh no!",
640
+ "No level title!",
641
+ "Sorry, you need to specify a title for your" +
642
+ " level to be saved.",
643
+ );
621
644
  return;
622
645
  }
623
646
 
624
- var regex = /^(\w?[ ]?)*$/;
625
- var validString = regex.exec($('#levelNameInput').val());
647
+ const regex = /^[\w ]*$/;
648
+ const validString = regex.exec(nameInput.val());
626
649
  if (!validString) {
627
- ocargo.Drawing.startPopup(gettext('Oh no!'), gettext('You used some invalid characters.'),
628
- gettext('Try saving your level again using only letters and numbers.'));
650
+ ocargo.Drawing.startPopup(
651
+ "Oh no!",
652
+ "You used some invalid characters.",
653
+ "Try saving your level again using only" +
654
+ " letters and numbers."
655
+ );
629
656
  return;
630
657
  }
631
658
 
@@ -634,33 +661,37 @@ ocargo.LevelEditor = function(levelId) {
634
661
  }
635
662
 
636
663
  // Test to see if we already have the level saved
637
- var table = $("#saveLevelTable");
638
- var existingId = -1;
664
+ const table = $("#saveLevelTable");
665
+ let existingId = -1;
639
666
 
640
- for (var i = 0; i < table[0].rows.length; i++) {
641
- var row = table[0].rows[i];
642
- var existingName = row.cells[0].innerHTML;
667
+ for (let i = 0; i < table[0].rows.length; i++) {
668
+ const row = table[0].rows[i];
669
+ const existingName = row.cells[0].innerHTML;
643
670
  if (existingName === newName) {
644
671
  existingId = row.getAttribute('value');
645
672
  break;
646
673
  }
647
674
  }
648
675
 
649
- if (existingId != -1) {
676
+ if (existingId !== -1) {
650
677
  if (!saveState.isCurrentLevel(existingId)) {
651
- var onYes = function(){
678
+ const onYes = function(){
652
679
  saveLevelLocal(existingId);
653
680
  $("#myModal").hide()
654
681
  $("#ocargo-modal").hide()
655
682
  };
656
- var onNo = function(){
683
+ const onNo = function(){
657
684
  $("#myModal").hide()
658
685
  $("#ocargo-modal").hide()
659
686
  };
660
- ocargo.Drawing.startYesNoPopup(gettext('Overwriting'), gettext('Warning'),
661
- interpolate(gettext('Level %(level_name)s already exists. Are you sure you want to overwrite it?'), {
662
- level_name: newName
663
- }, true), onYes, onNo);
687
+ ocargo.Drawing.startYesNoPopup(
688
+ "Overwriting",
689
+ "Warning",
690
+ `Level ${newName} already exists. Are
691
+ you sure you want to overwrite it?`,
692
+ onYes,
693
+ onNo
694
+ );
664
695
  } else {
665
696
  saveLevelLocal(existingId);
666
697
  }
@@ -701,7 +732,7 @@ ocargo.LevelEditor = function(levelId) {
701
732
  }
702
733
 
703
734
  function setupShareTab() {
704
- // Setup the behaviour for when the tab is selected
735
+ // Set up the behaviour for when the tab is selected
705
736
  tabs.share.setOnChange(function() {
706
737
  if (!isIndependentStudent() || !isLoggedIn("share") || !canShare() || !isLevelOwned()) {
707
738
  restorePreviousTab();
@@ -880,7 +911,7 @@ ocargo.LevelEditor = function(levelId) {
880
911
  var color = COW_GROUP_COLOR_PALETTE[(currentCowGroupId - 1) % COW_GROUP_COLOR_PALETTE.length];
881
912
  var style = 'background-color: ' + color;
882
913
  var value = 'group' + currentCowGroupId++;
883
- var type = ocargo.Cow.WHITE;
914
+ var type = currentTheme == THEMES.city ? ocargo.Cow.PIGEON : ocargo.Cow.WHITE;
884
915
 
885
916
  cowGroups[value] = {
886
917
  id: value,
@@ -1031,7 +1062,7 @@ ocargo.LevelEditor = function(levelId) {
1031
1062
  } else {
1032
1063
  closeTrashcan();
1033
1064
  }
1034
- }
1065
+ }
1035
1066
 
1036
1067
  function openTrashcan() {
1037
1068
  $('#trashcanLidOpen').css('display', 'block');
@@ -1196,8 +1227,8 @@ ocargo.LevelEditor = function(levelId) {
1196
1227
  if (cows) {
1197
1228
  for (var i = 0; i < cows.length; i++) {
1198
1229
  var internalCow = cows[i];
1199
- if (internalCow.controlledNode) {
1200
- mark(internalCow.controlledNode.coordinate, internalCow.data.group.color, 0.3, true);
1230
+ if (internalCow.coordinate) {
1231
+ mark(internalCow.coordinate, internalCow.data.group.color, 0.3, true);
1201
1232
  }
1202
1233
  }
1203
1234
  }
@@ -1312,7 +1343,7 @@ ocargo.LevelEditor = function(levelId) {
1312
1343
  nodes[0] = temp;
1313
1344
  originNode = nodes[0];
1314
1345
  } else if (mode === modes.ADD_HOUSE_MODE && existingNode) {
1315
- // Check if same as starting node
1346
+ // Check if same as starting node
1316
1347
  if (isOriginCoordinate(coordMap)) {
1317
1348
  originNode = null;
1318
1349
  }
@@ -1358,11 +1389,11 @@ ocargo.LevelEditor = function(levelId) {
1358
1389
  mark(coordMap, 'blue', 0.3, true);
1359
1390
  } else if (mode === modes.MARK_ORIGIN_MODE && canPlaceCFC(node)) {
1360
1391
  mark(coordMap, 'red', 0.5, true);
1361
- }
1392
+ }
1362
1393
  } else if (node && houseNodes.includes(node) && mode === modes.DELETE_HOUSE_MODE) {
1363
1394
  mark(coordMap, 'blue', 0.3, true);
1364
1395
  }
1365
- }
1396
+ }
1366
1397
  };
1367
1398
  }
1368
1399
 
@@ -1440,6 +1471,23 @@ ocargo.LevelEditor = function(levelId) {
1440
1471
  };
1441
1472
  }
1442
1473
 
1474
+ function draggedObjectOnGrid(e, dragged_object) {
1475
+ // object location is relative to the whole page, so need to factor in paper and padding size, grid canvas scroll amount, width of toolbar, etc.
1476
+ return e.pageX >= (TAB_PANE_WIDTH + PAPER_PADDING)
1477
+ && (e.pageY + paper.scrollTop() + dragged_object.height / 2) <= (PAPER_HEIGHT + PAPER_PADDING)
1478
+ && (e.pageX + paper.scrollLeft() + dragged_object.width / 2) <= (TAB_PANE_WIDTH + PAPER_WIDTH + PAPER_PADDING)
1479
+ }
1480
+
1481
+ function getAbsCoordinates(e) {
1482
+ const absX = (e.pageX + paper.scrollLeft() - TAB_PANE_WIDTH) / GRID_SPACE_SIZE;
1483
+ const absY = (e.pageY + paper.scrollTop()) / GRID_SPACE_SIZE;
1484
+ return [absX, absY];
1485
+ }
1486
+
1487
+ function draggedCursorOverGrid(absX, absY) {
1488
+ return absY <= SEMI_EXTENDED_PAPER_HEIGHT / 100 && absX <= EXTENDED_PAPER_WIDTH / 100 && absX >= 0
1489
+ }
1490
+
1443
1491
  function setupDecorListeners(decor) {
1444
1492
  var image = decor.image;
1445
1493
 
@@ -1515,6 +1563,47 @@ ocargo.LevelEditor = function(levelId) {
1515
1563
  addReleaseListeners(image.node);
1516
1564
  }
1517
1565
 
1566
+ function handleDraggableDecorMouseDown(e){
1567
+ e.preventDefault();
1568
+
1569
+ window.dragged_decor = {};
1570
+ dragged_decor.pageX0 = e.pageX;
1571
+ dragged_decor.pageY0 = e.pageY;
1572
+ dragged_decor.elem = this;
1573
+ dragged_decor.offset0 = $(this).offset();
1574
+ dragged_decor.width = parseInt(currentTheme.decor[this.id].width);
1575
+ dragged_decor.height = parseInt(currentTheme.decor[this.id].height);
1576
+ dragged_decor.parent = this.parentElement;
1577
+
1578
+ const clone = $(this).clone(true);
1579
+
1580
+ function handleDraggableDecorDragging(e){
1581
+ const left = dragged_decor.offset0.left + (e.pageX - dragged_decor.pageX0);
1582
+ const top = dragged_decor.offset0.top + (e.pageY - dragged_decor.pageY0);
1583
+ $(dragged_decor.elem).offset({top: top, left: left});
1584
+ }
1585
+
1586
+ function handleDraggableDecorMouseUp(e){
1587
+ if (dragged_decor.elem.id !== null) {
1588
+ if (draggedObjectOnGrid(e, dragged_decor)) {
1589
+ let decorObject = new InternalDecor(dragged_decor.elem.id);
1590
+ decorObject.setPosition(e.pageX + paper.scrollLeft() - TAB_PANE_WIDTH - dragged_decor.width / 2, e.pageY + paper.scrollTop() - dragged_decor.height / 2);
1591
+ }
1592
+ }
1593
+
1594
+ $(document)
1595
+ .off('mousemove', handleDraggableDecorDragging)
1596
+ .off('mouseup mouseleave', handleDraggableDecorMouseUp);
1597
+
1598
+ $(dragged_decor.elem).remove();
1599
+ $(clone).appendTo(dragged_decor.parent);
1600
+ }
1601
+
1602
+ $(document)
1603
+ .on('mouseup mouseleave', handleDraggableDecorMouseUp)
1604
+ .on('mousemove', handleDraggableDecorDragging);
1605
+ }
1606
+
1518
1607
  function setupCowListeners(cow) {
1519
1608
  var image = cow.image;
1520
1609
 
@@ -1568,50 +1657,14 @@ ocargo.LevelEditor = function(levelId) {
1568
1657
  image.transform('t' + paperX + ',' + paperY );
1569
1658
 
1570
1659
  //Unmark the squares the cow previously occupied
1571
- if (controlledCoord) {
1572
- markAsBackground(controlledCoord);
1573
- }
1574
- if(cows) {
1575
- for( var i = 0; i < cows.length; i++){
1576
- var internalCow = cows[i];
1577
- if(internalCow !== cow && internalCow.controlledNode) {
1578
- mark(internalCow.controlledNode.coordinate, internalCow.data.group.color, 0.3, true);
1579
- }
1580
- }
1581
- }
1582
- if (originNode) {
1583
- markAsOrigin(originNode.coordinate);
1584
- }
1585
- if (houseNodes.length > 0) {
1586
- for (let i = 0; i < houseNodes.length; i++){
1587
- markAsHouse(houseNodes[i].coordinate);
1588
- }
1589
- }
1660
+ unmarkOldCowSquare(controlledCoord, cow);
1590
1661
 
1591
1662
  // Now calculate the source coordinate
1592
1663
  var box = image.getBBox();
1593
1664
  var absX = (box.x + box.width/2) / GRID_SPACE_SIZE;
1594
1665
  var absY = (box.y + box.height/2) / GRID_SPACE_SIZE;
1595
1666
 
1596
- var x = Math.min(Math.max(0, Math.floor(absX)), GRID_WIDTH - 1);
1597
- var y = GRID_HEIGHT - Math.min(Math.max(0, Math.floor(absY)), GRID_HEIGHT - 1) - 1;
1598
- controlledCoord = new ocargo.Coordinate(x,y);
1599
-
1600
- // If source node is not on grid remove it
1601
- if (!isCoordinateOnGrid(controlledCoord)) {
1602
- controlledCoord = null;
1603
- }
1604
-
1605
- if (controlledCoord) {
1606
- var colour;
1607
- if(isValidPlacement(controlledCoord)) {
1608
- colour = VALID_LIGHT_COLOUR;
1609
- } else {
1610
- colour = INVALID_LIGHT_COLOUR;
1611
- }
1612
-
1613
- mark(controlledCoord, colour, 0.7, false);
1614
- }
1667
+ controlledCoord = markNewCowSquare(absX, absY, controlledCoord, cow);
1615
1668
 
1616
1669
  // Deal with trashcan
1617
1670
  var paperAbsX = paperX - paper.scrollLeft() + imageWidth/2;
@@ -1628,6 +1681,8 @@ ocargo.LevelEditor = function(levelId) {
1628
1681
  }
1629
1682
 
1630
1683
  function onDragStart(x, y) {
1684
+ // cow shouldn't be in the cow group during dragging
1685
+ removeCowFromCowList(cow);
1631
1686
  var bBox = image.getBBox();
1632
1687
  imageWidth = bBox.width;
1633
1688
  imageHeight = bBox.height;
@@ -1643,71 +1698,62 @@ ocargo.LevelEditor = function(levelId) {
1643
1698
  }
1644
1699
 
1645
1700
  function onDragEnd() {
1646
- //Unmark previously occupied square
1647
- if(cow.controlledNode) {
1648
- markAsBackground(cow.controlledNode.coordinate);
1649
- }
1650
-
1651
- // Mark squares currently occupied
1652
- if (controlledCoord) {
1653
- mark(controlledCoord, cow.data.group.color, 0.3, true);
1654
- }
1655
- if (originNode) {
1656
- markAsOrigin(originNode.coordinate);
1657
- }
1658
- if (houseNodes.length > 0) {
1659
- for (let i = 0; i < houseNodes.length; i++) {
1660
- markAsHouse(houseNodes[i].coordinate);
1661
- }
1662
- }
1663
1701
 
1664
1702
  if (trashcanOpen) {
1665
1703
  cow.destroy();
1666
- } else if (isValidPlacement(controlledCoord)) {
1667
- // Add back to the list of cows if on valid nodes
1668
- var controlledNode = ocargo.Node.findNodeByCoordinate(controlledCoord, nodes);
1669
- cow.controlledNode = controlledNode;
1670
- cow.valid = true;
1671
- drawing.setCowImagePosition(controlledCoord, image, controlledNode);
1704
+ unmarkOldCowSquare(controlledCoord, cow);
1705
+ closeTrashcan();
1672
1706
  } else {
1673
- cow.controlledNode = null;
1674
- cow.valid = false;
1707
+ setCowMarkingsOnMouseUp(controlledCoord, cow);
1708
+ cows.push(cow);
1709
+ cow.coordinate = controlledCoord;
1710
+ cow.valid = isValidDraggedCowPlacement(controlledCoord, cow);
1711
+ if (cow.isOnRoad()) {
1712
+ const controlledNode = ocargo.Node.findNodeByCoordinate(controlledCoord, nodes);
1713
+ drawing.setCowImagePosition(controlledCoord, image, controlledNode);
1714
+ }
1715
+ else {
1716
+ var cowX = paperX;
1717
+ var cowY = paperY;
1675
1718
 
1676
- var cowX = paperX;
1677
- var cowY = paperY;
1719
+ if (paperWidth < paperX + imageWidth) {
1720
+ cowX = paperWidth - imageWidth
1721
+ }
1678
1722
 
1679
- if (paperWidth < paperX + imageWidth) {
1680
- cowX = paperWidth - imageWidth
1681
- }
1723
+ if (paperHeight < paperY + imageHeight) {
1724
+ cowY = paperHeight - imageHeight
1725
+ }
1682
1726
 
1683
- if (paperHeight < paperY + imageHeight) {
1684
- cowY = paperHeight - imageHeight
1727
+ image.transform('t' + cowX + ',' + cowY);
1685
1728
  }
1729
+ }
1686
1730
 
1687
- image.transform('t' + cowX + ',' + cowY);
1688
- }
1689
1731
  adjustCowGroupMinMaxFields(cow);
1690
-
1691
1732
  image.attr({'cursor':'pointer'});
1692
- closeTrashcan();
1693
- }
1694
-
1695
- function isValidPlacement(controlledCoord){
1696
- var controlledNode = ocargo.Node.findNodeByCoordinate(controlledCoord, nodes);
1697
- if (!controlledNode)
1698
- return false;
1699
- for (var i=0; i < cows.length; i++) {
1700
- var otherCow = cows[i];
1701
- if (otherCow.controlledNode == controlledNode && cow != otherCow)
1702
- return false;
1703
- }
1704
- return true;
1705
1733
  }
1706
1734
 
1707
1735
  image.drag(onDragMove, onDragStart, onDragEnd);
1708
1736
  addReleaseListeners(image.node);
1709
1737
  }
1710
1738
 
1739
+ function removeCowFromCowList(cow) {
1740
+ var index = cows.indexOf(cow);
1741
+ if (index > -1) {
1742
+ cows.splice(index, 1);
1743
+ }
1744
+ }
1745
+
1746
+ function isValidDraggedCowPlacement(controlledCoord, cow){
1747
+ if (isOriginCoordinate(controlledCoord) || isHouseCoordinate(controlledCoord))
1748
+ return false;
1749
+ for (var i=0; i < cows.length; i++) {
1750
+ var otherCow = cows[i];
1751
+ if (cow != otherCow && otherCow.coordinate && otherCow.coordinate.equals(controlledCoord))
1752
+ return false;
1753
+ }
1754
+ return true;
1755
+ }
1756
+
1711
1757
  function adjustCowGroupMinMaxFields(draggedCow) {
1712
1758
  var draggedCowGroupId = draggedCow.data.group.id;
1713
1759
 
@@ -1724,6 +1770,131 @@ ocargo.LevelEditor = function(levelId) {
1724
1770
  $('#cow_group_select').val(draggedCowGroupId).change();
1725
1771
  }
1726
1772
 
1773
+ function unmarkOldCowSquare(controlledCoord, cow = "undefined") {
1774
+ if (controlledCoord) {
1775
+ markAsBackground(controlledCoord);
1776
+ }
1777
+ if (originNode) {
1778
+ markAsOrigin(originNode.coordinate);
1779
+ }
1780
+ if (houseNodes.length > 0) {
1781
+ for (let i = 0; i < houseNodes.length; i++){
1782
+ markAsHouse(houseNodes[i].coordinate);
1783
+ }
1784
+ }
1785
+ }
1786
+
1787
+ function setCowMarkingsOnMouseUp(controlledCoord, cow) {
1788
+ if (cow.isOnRoad()) {
1789
+ markAsBackground(cow.coordinate);
1790
+ }
1791
+ if (controlledCoord) {
1792
+ mark(controlledCoord, cow.data.group.color, 0.3, true);
1793
+ }
1794
+ if (originNode) {
1795
+ markAsOrigin(originNode.coordinate);
1796
+ }
1797
+ if (houseNodes.length > 0) {
1798
+ for (let i = 0; i < houseNodes.length; i++) {
1799
+ markAsHouse(houseNodes[i].coordinate);
1800
+ }
1801
+ }
1802
+ }
1803
+
1804
+ function markNewCowSquare(absX, absY, controlledCoord, cow = "undefined") {
1805
+ const x = Math.min(Math.max(0, Math.floor(absX)), GRID_WIDTH - 1);
1806
+ const y = GRID_HEIGHT - Math.min(Math.max(0, Math.floor(absY)), GRID_HEIGHT - 1) - 1;
1807
+ controlledCoord = new ocargo.Coordinate(x,y);
1808
+
1809
+ // If source node is not on grid remove it
1810
+ if (!isCoordinateOnGrid(controlledCoord)) {
1811
+ controlledCoord = null;
1812
+ }
1813
+
1814
+ // mark square valid or invalid
1815
+ if (controlledCoord) {
1816
+ let colour;
1817
+ if(isValidDraggedCowPlacement(controlledCoord, cow)) {
1818
+ colour = VALID_LIGHT_COLOUR;
1819
+ } else {
1820
+ colour = INVALID_LIGHT_COLOUR;
1821
+ }
1822
+
1823
+ mark(controlledCoord, colour, 0.7, false);
1824
+ }
1825
+
1826
+ return controlledCoord;
1827
+ }
1828
+
1829
+ function handleDraggableCowMouseDown(e, cowGroup){
1830
+ e.preventDefault();
1831
+
1832
+ window.dragged_cow = {};
1833
+ dragged_cow.pageX0 = e.pageX;
1834
+ dragged_cow.pageY0 = e.pageY;
1835
+ dragged_cow.elem = e.target;
1836
+ dragged_cow.offset0 = $(e.target).offset();
1837
+ dragged_cow.parent = e.target.parentElement;
1838
+ dragged_cow.group = cowGroups[cowGroup];
1839
+ dragged_cow.width = COW_WIDTH;
1840
+ dragged_cow.height = COW_HEIGHT;
1841
+
1842
+ const clone = $(e.target).clone(true);
1843
+ let controlledCoord;
1844
+
1845
+ function handleDraggableCowDragging(e){
1846
+ e.target.style.cursor = "pointer";
1847
+
1848
+ const left = dragged_cow.offset0.left + (e.pageX - dragged_cow.pageX0);
1849
+ const top = dragged_cow.offset0.top + (e.pageY - dragged_cow.pageY0);
1850
+ $(dragged_cow.elem).offset({top: top, left: left});
1851
+
1852
+ unmarkOldCowSquare(controlledCoord);
1853
+
1854
+ const [absX, absY] = getAbsCoordinates(e);
1855
+ if (draggedCursorOverGrid(absX, absY)) {
1856
+ controlledCoord = markNewCowSquare(absX, absY, controlledCoord);
1857
+ }
1858
+ }
1859
+
1860
+ function handleDraggableCowMouseUp(e){
1861
+ let internalCow = new InternalCow({group: cowGroups["group1"]});
1862
+ let image = internalCow.image;
1863
+ internalCow.coordinate = controlledCoord;
1864
+ internalCow.valid = isValidDraggedCowPlacement(controlledCoord, internalCow);
1865
+
1866
+ if (internalCow.isOnRoad()) {
1867
+ const controlledNode = ocargo.Node.findNodeByCoordinate(controlledCoord, nodes);
1868
+ drawing.setCowImagePosition(controlledCoord, image, controlledNode);
1869
+ } else {
1870
+ const cowX = e.pageX + paper.scrollLeft() - TAB_PANE_WIDTH - dragged_cow.width / 2;
1871
+ const cowY = e.pageY + paper.scrollTop() - dragged_cow.height / 2;
1872
+
1873
+ if (draggedObjectOnGrid(e, dragged_cow)) {
1874
+ image.transform('t' + cowX + ',' + cowY);
1875
+ } else {
1876
+ internalCow.destroy();
1877
+ }
1878
+ }
1879
+
1880
+ if (!trashcanOpen) {
1881
+ setCowMarkingsOnMouseUp(controlledCoord, internalCow);
1882
+ adjustCowGroupMinMaxFields(internalCow);
1883
+ }
1884
+
1885
+ $(document)
1886
+ .off('mousemove', handleDraggableCowDragging)
1887
+ .off('mouseup mouseleave', handleDraggableCowMouseUp);
1888
+
1889
+ $(dragged_cow.elem).remove();
1890
+ $(clone).appendTo(dragged_cow.parent);
1891
+ }
1892
+
1893
+ $(document)
1894
+ .on('mouseup mouseleave', handleDraggableCowMouseUp)
1895
+ .on('mousemove', handleDraggableCowDragging);
1896
+ }
1897
+
1727
1898
 
1728
1899
  function setupTrafficLightListeners(trafficLight) {
1729
1900
  var image = trafficLight.image;
@@ -1788,23 +1959,7 @@ ocargo.LevelEditor = function(levelId) {
1788
1959
  image.transform('t' + paperX + ',' + paperY + 'r' + rotation + 's' + scaling);
1789
1960
 
1790
1961
  // Unmark the squares the light previously occupied
1791
- if (sourceCoord) {
1792
- markAsBackground(sourceCoord);
1793
- }
1794
- if (controlledCoord) {
1795
- markAsBackground(controlledCoord);
1796
- }
1797
-
1798
- markCowNodes();
1799
-
1800
- if (originNode) {
1801
- markAsOrigin(originNode.coordinate);
1802
- }
1803
- if (houseNodes.length > 0) {
1804
- for (let i = 0; i < houseNodes.length; i++) {
1805
- markAsHouse(houseNodes[i].coordinate);
1806
- }
1807
- }
1962
+ unmarkOldTrafficLightSquare(sourceCoord, controlledCoord);
1808
1963
 
1809
1964
  // Now calculate the source coordinate
1810
1965
  var box = image.getBBox();
@@ -1826,48 +1981,7 @@ ocargo.LevelEditor = function(levelId) {
1826
1981
  break;
1827
1982
  }
1828
1983
 
1829
- var x = Math.min(Math.max(0, Math.floor(absX)), GRID_WIDTH - 1);
1830
- var y = GRID_HEIGHT - Math.min(Math.max(0, Math.floor(absY)), GRID_HEIGHT - 1) - 1;
1831
- sourceCoord = new ocargo.Coordinate(x,y);
1832
-
1833
- // Find controlled position in map coordinates
1834
- switch(rotation) {
1835
- case 0:
1836
- controlledCoord = new ocargo.Coordinate(sourceCoord.x, sourceCoord.y + 1);
1837
- break;
1838
- case 90:
1839
- controlledCoord = new ocargo.Coordinate(sourceCoord.x + 1, sourceCoord.y);
1840
- break;
1841
- case 180:
1842
- controlledCoord = new ocargo.Coordinate(sourceCoord.x, sourceCoord.y - 1);
1843
- break;
1844
- case 270:
1845
- controlledCoord = new ocargo.Coordinate(sourceCoord.x - 1, sourceCoord.y);
1846
- break;
1847
- }
1848
-
1849
- // If controlled node is not on grid, remove it
1850
- if (!isCoordinateOnGrid(controlledCoord)) {
1851
- controlledCoord = null;
1852
- }
1853
-
1854
- // If source node is not on grid remove it
1855
- if (!isCoordinateOnGrid(sourceCoord)) {
1856
- sourceCoord = null;
1857
- }
1858
-
1859
- if (sourceCoord && controlledCoord) {
1860
- var colour;
1861
- if(isValidPlacement(sourceCoord, controlledCoord)) {
1862
- colour = VALID_LIGHT_COLOUR;
1863
- drawing.setTrafficLightImagePosition(sourceCoord, controlledCoord, image);
1864
- } else {
1865
- colour = INVALID_LIGHT_COLOUR;
1866
- }
1867
-
1868
- mark(controlledCoord, colour, 0.7, false);
1869
- mark(sourceCoord, colour, 0.7, false);
1870
- }
1984
+ [sourceCoord, controlledCoord] = markNewTrafficLightSquare(absX, absY, isValidTrafficLightPlacement, sourceCoord, controlledCoord, rotation, image);
1871
1985
 
1872
1986
  // Deal with trashcan
1873
1987
  var paperAbsX = paperX - paper.scrollLeft() + imageWidth/2;
@@ -1907,27 +2021,11 @@ ocargo.LevelEditor = function(levelId) {
1907
2021
 
1908
2022
  function onDragEnd() {
1909
2023
  // Unmark squares currently occupied
1910
- if (sourceCoord) {
1911
- markAsBackground(sourceCoord);
1912
- }
1913
- if (controlledCoord) {
1914
- markAsBackground(controlledCoord);
1915
- }
1916
-
1917
- markCowNodes();
1918
-
1919
- if (originNode) {
1920
- markAsOrigin(originNode.coordinate);
1921
- }
1922
- if (houseNodes.length > 0) {
1923
- for (let i = 0; i < houseNodes.length; i++) {
1924
- markAsHouse(houseNodes[i].coordinate);
1925
- }
1926
- }
2024
+ unmarkOldTrafficLightSquare(sourceCoord, controlledCoord);
1927
2025
 
1928
2026
  if(trashcanOpen) {
1929
2027
  trafficLight.destroy();
1930
- } else if(isValidPlacement(sourceCoord, controlledCoord)) {
2028
+ } else if(isValidTrafficLightPlacement(sourceCoord, controlledCoord)) {
1931
2029
  // Add back to the list of traffic lights if on valid nodes
1932
2030
  trafficLight.sourceNode = ocargo.Node.findNodeByCoordinate(sourceCoord, nodes);
1933
2031
  trafficLight.controlledNode = ocargo.Node.findNodeByCoordinate(controlledCoord, nodes);
@@ -1976,41 +2074,177 @@ ocargo.LevelEditor = function(levelId) {
1976
2074
  }
1977
2075
  return "0,0";
1978
2076
  }
2077
+ }
1979
2078
 
1980
- function isValidPlacement(sourceCoord, controlledCoord) {
1981
- var sourceNode = ocargo.Node.findNodeByCoordinate(sourceCoord, nodes);
1982
- var controlledNode = ocargo.Node.findNodeByCoordinate(controlledCoord, nodes);
2079
+ function isValidTrafficLightPlacement(sourceCoord, controlledCoord) {
2080
+ var sourceNode = ocargo.Node.findNodeByCoordinate(sourceCoord, nodes);
2081
+ var controlledNode = ocargo.Node.findNodeByCoordinate(controlledCoord, nodes);
1983
2082
 
1984
- // Test if two connected nodes exist
1985
- var connected = false;
1986
- if (sourceNode && controlledNode) {
1987
- for (var i = 0; i < sourceNode.connectedNodes.length; i++) {
1988
- if (sourceNode.connectedNodes[i] === controlledNode) {
1989
- connected = true;
1990
- break;
1991
- }
2083
+ // Test if two connected nodes exist
2084
+ var connected = false;
2085
+ if (sourceNode && controlledNode) {
2086
+ for (var i = 0; i < sourceNode.connectedNodes.length; i++) {
2087
+ if (sourceNode.connectedNodes[i] === controlledNode) {
2088
+ connected = true;
2089
+ break;
1992
2090
  }
1993
2091
  }
2092
+ }
1994
2093
 
1995
- if(!connected) {
2094
+ if(!connected) {
2095
+ return false;
2096
+ }
2097
+
2098
+ // Test it's not already occupied
2099
+ for(var i = 0; i < trafficLights.length; i++) {
2100
+ var tl = trafficLights[i];
2101
+ if(tl.valid &&
2102
+ ((tl.sourceNode === sourceNode && tl.controlledNode === controlledNode) ||
2103
+ (tl.sourceNode === controlledNode && tl.controlledNode === sourceNode))) {
1996
2104
  return false;
1997
2105
  }
2106
+ }
2107
+ return true;
2108
+ }
2109
+
2110
+ function unmarkOldTrafficLightSquare(sourceCoord, controlledCoord) {
2111
+ // Unmark the squares the light previously occupied
2112
+ if (sourceCoord) {
2113
+ markAsBackground(sourceCoord);
2114
+ }
2115
+ if (controlledCoord) {
2116
+ markAsBackground(controlledCoord);
2117
+ }
2118
+
2119
+ markCowNodes();
2120
+
2121
+ if (originNode) {
2122
+ markAsOrigin(originNode.coordinate);
2123
+ }
2124
+ if (houseNodes.length > 0) {
2125
+ for (let i = 0; i < houseNodes.length; i++) {
2126
+ markAsHouse(houseNodes[i].coordinate);
2127
+ }
2128
+ }
2129
+ }
2130
+
2131
+ function markNewTrafficLightSquare(absX, absY, validityCheckFunction, sourceCoord, controlledCoord, rotation, image = "undefined") {
2132
+ var x = Math.min(Math.max(0, Math.floor(absX)), GRID_WIDTH - 1);
2133
+ var y = GRID_HEIGHT - Math.min(Math.max(0, Math.floor(absY)), GRID_HEIGHT - 1) - 1;
2134
+ sourceCoord = new ocargo.Coordinate(x,y);
1998
2135
 
1999
- // Test it's not already occupied
2000
- for(var i = 0; i < trafficLights.length; i++) {
2001
- var tl = trafficLights[i];
2002
- if(tl.valid &&
2003
- ((tl.sourceNode === sourceNode && tl.controlledNode === controlledNode) ||
2004
- (tl.sourceNode === controlledNode && tl.controlledNode === sourceNode))) {
2005
- return false;
2136
+ // Find controlled position in map coordinates
2137
+ switch(rotation) {
2138
+ case 0:
2139
+ controlledCoord = new ocargo.Coordinate(sourceCoord.x, sourceCoord.y + 1);
2140
+ break;
2141
+ case 90:
2142
+ controlledCoord = new ocargo.Coordinate(sourceCoord.x + 1, sourceCoord.y);
2143
+ break;
2144
+ case 180:
2145
+ controlledCoord = new ocargo.Coordinate(sourceCoord.x, sourceCoord.y - 1);
2146
+ break;
2147
+ case 270:
2148
+ controlledCoord = new ocargo.Coordinate(sourceCoord.x - 1, sourceCoord.y);
2149
+ break;
2150
+ }
2151
+
2152
+ // If controlled node is not on grid, remove it
2153
+ if (!isCoordinateOnGrid(controlledCoord)) {
2154
+ controlledCoord = null;
2155
+ }
2156
+
2157
+ // If source node is not on grid remove it
2158
+ if (!isCoordinateOnGrid(sourceCoord)) {
2159
+ sourceCoord = null;
2160
+ }
2161
+
2162
+ if (sourceCoord && controlledCoord) {
2163
+ var colour;
2164
+ if(validityCheckFunction(sourceCoord, controlledCoord)) {
2165
+ colour = VALID_LIGHT_COLOUR;
2166
+ if (image !== "undefined") {
2167
+ drawing.setTrafficLightImagePosition(sourceCoord, controlledCoord, image);
2006
2168
  }
2169
+ } else {
2170
+ colour = INVALID_LIGHT_COLOUR;
2171
+ }
2172
+
2173
+ mark(controlledCoord, colour, 0.7, false);
2174
+ mark(sourceCoord, colour, 0.7, false);
2175
+ }
2176
+
2177
+ return [sourceCoord, controlledCoord];
2178
+ }
2179
+
2180
+ function handleDraggableTrafficLightsMouseDown(e, startingState){
2181
+ e.preventDefault();
2182
+
2183
+ window.dragged_light = {};
2184
+ dragged_light.pageX0 = e.pageX;
2185
+ dragged_light.pageY0 = e.pageY;
2186
+ dragged_light.elem = e.target;
2187
+ dragged_light.offset0 = $(e.target).offset();
2188
+ dragged_light.width = TRAFFIC_LIGHT_WIDTH;
2189
+ dragged_light.height = TRAFFIC_LIGHT_HEIGHT;
2190
+ dragged_light.parent = e.target.parentElement;
2191
+
2192
+ const clone = $(e.target).clone(true);
2193
+
2194
+ let sourceCoord;
2195
+ let controlledCoord;
2196
+
2197
+ function handleDraggableTrafficLightsDragging(e){
2198
+ const left = dragged_light.offset0.left + (e.pageX - dragged_light.pageX0);
2199
+ const top = dragged_light.offset0.top + (e.pageY - dragged_light.pageY0);
2200
+ $(dragged_light.elem).offset({top: top, left: left});
2201
+
2202
+ unmarkOldTrafficLightSquare(sourceCoord, controlledCoord);
2203
+
2204
+ const [absX, absY] = getAbsCoordinates(e);
2205
+ if (draggedCursorOverGrid(absX, absY)) {
2206
+ [sourceCoord, controlledCoord] = markNewTrafficLightSquare(absX, absY, isValidTrafficLightPlacement, sourceCoord, controlledCoord, 0);
2007
2207
  }
2008
- return true;
2009
2208
  }
2010
2209
 
2011
- function occupied(sourceCoord, controlledCoord) {
2210
+ function handleDraggableTrafficLightsMouseUp(e){
2211
+ let internalTrafficLight = new InternalTrafficLight({"redDuration": 3, "greenDuration": 3, "startTime": 0, "startingState": startingState, "sourceCoordinate": null, "direction": null});
2212
+ let image = internalTrafficLight.image;
2213
+
2214
+ const lightX = e.pageX + paper.scrollLeft() - TAB_PANE_WIDTH - dragged_light.width;
2215
+ const lightY = e.pageY + paper.scrollTop() - dragged_light.width / 2;
2216
+
2217
+ unmarkOldTrafficLightSquare(sourceCoord, controlledCoord);
2012
2218
 
2219
+ if (isValidTrafficLightPlacement(sourceCoord, controlledCoord)) {
2220
+ internalTrafficLight.sourceNode = ocargo.Node.findNodeByCoordinate(sourceCoord, nodes);
2221
+ internalTrafficLight.controlledNode = ocargo.Node.findNodeByCoordinate(controlledCoord, nodes);
2222
+ internalTrafficLight.valid = true;
2223
+
2224
+ drawing.setTrafficLightImagePosition(sourceCoord, controlledCoord, image);
2225
+ } else {
2226
+ internalTrafficLight.sourceCoord = null;
2227
+ internalTrafficLight.controlledCoord = null;
2228
+ internalTrafficLight.valid = false;
2229
+
2230
+ if (draggedObjectOnGrid(e, dragged_light)) {
2231
+ image.transform('t' + lightX + ',' + lightY + ' s-1,1');
2232
+ } else {
2233
+ internalTrafficLight.destroy();
2234
+ }
2235
+ }
2236
+
2237
+ $(document)
2238
+ .off('mousemove', handleDraggableTrafficLightsDragging)
2239
+ .off('mouseup mouseleave', handleDraggableTrafficLightsMouseUp);
2240
+
2241
+ $(dragged_light.elem).remove();
2242
+ $(clone).appendTo(dragged_light.parent);
2013
2243
  }
2244
+
2245
+ $(document)
2246
+ .on('mouseup mouseleave', handleDraggableTrafficLightsMouseUp)
2247
+ .on('mousemove', handleDraggableTrafficLightsDragging);
2014
2248
  }
2015
2249
 
2016
2250
  /********************************/
@@ -2143,6 +2377,7 @@ ocargo.LevelEditor = function(levelId) {
2143
2377
 
2144
2378
  function setTheme(theme) {
2145
2379
  currentTheme = theme;
2380
+ let newType = currentTheme == THEMES.city ? ocargo.Cow.PIGEON : ocargo.Cow.WHITE;
2146
2381
 
2147
2382
  for (var x = 0; x < GRID_WIDTH; x++) {
2148
2383
  for (var y = 0; y < GRID_HEIGHT; y++) {
@@ -2159,6 +2394,36 @@ ocargo.LevelEditor = function(levelId) {
2159
2394
  });
2160
2395
 
2161
2396
  $('#paper').css({'background-color': theme.background});
2397
+
2398
+ const animalSource = theme == THEMES.city ? "/static/game/image/pigeon.svg" : "/static/game/image/Clarice.svg";
2399
+
2400
+ $('#cow').each(function(index, element) {
2401
+ element.src = animalSource;
2402
+ })
2403
+
2404
+ $('#animals_label').each(function(index, element) {
2405
+ element.innerHTML = theme == THEMES.city ? "Pigeons" : "Cows";
2406
+ })
2407
+
2408
+ for (let [key, value] of Object.entries(cowGroups)) {
2409
+ value["type"] = theme == THEMES.city ? ocargo.Cow.PIGEON : ocargo.Cow.WHITE;
2410
+ }
2411
+
2412
+ for (let i = 0; i < cows.length; i++) {
2413
+ cows[i].updateTheme();
2414
+ }
2415
+
2416
+ const pigeonHTML = `<svg class="block_image"><g transform="translate(10,0)" <path="" class="blocklyPathDark" fill="#496684" d="m 0,0 H 111.34375 v 30 H 0 V 20 c 0,-10 -8,8 -8,-7.5 s 8,2.5 8,-7.5 z
2417
+ "><path class="blocklyPath" stroke="none" fill="#5b80a5" d="m 0,0 H 111.34375 v 30 H 0 V 20 c 0,-10 -8,8 -8,-7.5 s 8,2.5 8,-7.5 z
2418
+ "></path><path class="blocklyPathLight" stroke="#8ca6c0" d="m 0.5,0.5 H 110.84375 M 110.84375,0.5 M 0.5,29.5 V 18.5 m -7.36,-0.5 q -1.52,-5.5 0,-11 m 7.36,1 V 0.5 H 1
2419
+ "></path><text class="blocklyText" y="12.5" transform="translate(10,5)">pigeons</text><g transform="translate(71.34375,5)"><image height="20px" width="30px" xlink:href="/static/game/image/pigeon.svg" alt=""></image></g></g></svg>`;
2420
+
2421
+ const cowHTML = `<svg class="block_image"><g transform="translate(10,0)" <path="" class="blocklyPathDark" fill="#496684" d="m 0,0 H 93.40625 v 30 H 0 V 20 c 0,-10 -8,8 -8,-7.5 s 8,2.5 8,-7.5 z
2422
+ "><path class="blocklyPath" stroke="none" fill="#5b80a5" d="m 0,0 H 93.40625 v 30 H 0 V 20 c 0,-10 -8,8 -8,-7.5 s 8,2.5 8,-7.5 z
2423
+ "></path><path class="blocklyPathLight" stroke="#8ca6c0" d="m 0.5,0.5 H 92.90625 M 92.90625,0.5 M 0.5,29.5 V 18.5 m -7.36,-0.5 q -1.52,-5.5 0,-11 m 7.36,1 V 0.5 H 1
2424
+ "></path><text class="blocklyText" y="12.5" transform="translate(10,5)">cows</text><g transform="translate(53.40625,5)"><image height="20px" width="30px" xlink:href="/static/game/image/Clarice.svg" alt=""></image></g></g></svg>`;
2425
+
2426
+ $("#cow_crossing_image").html(newType == ocargo.Cow.PIGEON ? pigeonHTML : cowHTML);
2162
2427
  }
2163
2428
 
2164
2429
  function sortNodes(nodes) {
@@ -2225,7 +2490,7 @@ ocargo.LevelEditor = function(levelId) {
2225
2490
  type: cowGroups[groupId].type}; //editor can only add white cow for now
2226
2491
  }
2227
2492
 
2228
- var coordinates = cows[i].controlledNode.coordinate;
2493
+ var coordinates = cows[i].coordinate;
2229
2494
  var strCoordinates = {'x':coordinates.x, 'y':coordinates.y};
2230
2495
  cowGroupData[groupId].potentialCoordinates.push(strCoordinates);
2231
2496
  }
@@ -2277,13 +2542,65 @@ ocargo.LevelEditor = function(levelId) {
2277
2542
 
2278
2543
  // Language data
2279
2544
  var language = $('#language_select').val();
2280
- state.blocklyEnabled = language === 'blockly' || language === 'both' || language === 'blocklyWithPythonView';
2281
- state.pythonViewEnabled = language === 'blocklyWithPythonView';
2282
- state.pythonEnabled = language === 'python' || language === 'both';
2545
+ state.blockly_enabled = language === 'blockly' || language === 'both' || language === 'blocklyWithPythonView';
2546
+ state.python_view_enabled = language === 'blocklyWithPythonView';
2547
+ state.python_enabled = language === 'python' || language === 'both';
2548
+
2549
+ const regex = /^[\w.?!', ]*$/;
2550
+ const subtitleValue = $('#subtitle').val();
2551
+ const descriptionValue = $('#description').val();
2552
+ const hintValue = $('#hint').val();
2553
+
2554
+ // Description and hint data
2555
+ if (subtitleValue.length > 0) {
2556
+ if (regex.exec(subtitleValue)) {
2557
+ state.subtitle = subtitleValue;
2558
+ }
2559
+ else {
2560
+ ocargo.Drawing.startPopup(
2561
+ "Oh no!",
2562
+ "You used some invalid characters for your level subtitle.",
2563
+ "Try saving your level again using only" +
2564
+ " letters, numbers and standard punctuation."
2565
+ );
2566
+ return
2567
+ }
2568
+ }
2569
+
2570
+ if (descriptionValue.length > 0) {
2571
+ if (regex.exec(descriptionValue)) {
2572
+ state.lesson = descriptionValue;
2573
+ }
2574
+ else {
2575
+ ocargo.Drawing.startPopup(
2576
+ "Oh no!",
2577
+ "You used some invalid characters for your level description.",
2578
+ "Try saving your level again using only" +
2579
+ " letters and numbers and standard punctuation."
2580
+ );
2581
+ return
2582
+ }
2583
+ }
2584
+
2585
+ if (hintValue.length > 0) {
2586
+ if (regex.exec(hintValue)) {
2587
+ state.hint = hintValue;
2588
+ }
2589
+ else {
2590
+ ocargo.Drawing.startPopup(
2591
+ "Oh no!",
2592
+ "You used some invalid characters for your level hint.",
2593
+ "Try saving your level again using only" +
2594
+ " letters and numbers and standard punctuation."
2595
+ );
2596
+ return
2597
+ }
2598
+ }
2283
2599
 
2284
2600
  // Other data
2285
2601
  state.theme = currentTheme.id;
2286
2602
  state.character = $('#character_select').val();
2603
+ state.disable_algorithm_score = true;
2287
2604
 
2288
2605
  return state;
2289
2606
  }
@@ -2291,6 +2608,17 @@ ocargo.LevelEditor = function(levelId) {
2291
2608
  function restoreState(state) {
2292
2609
  console.log("restoring state");
2293
2610
 
2611
+ // Get character id from saved character name
2612
+ var characterName = state.character_name;
2613
+ if (characterName) {
2614
+ var characterId = null;
2615
+ for (var id in CHARACTERS) {
2616
+ if (characterName == CHARACTERS[id].name) {
2617
+ characterId = id;
2618
+ break;
2619
+ }
2620
+ }
2621
+ }
2294
2622
  clear();
2295
2623
 
2296
2624
  // Load node data
@@ -2302,35 +2630,6 @@ ocargo.LevelEditor = function(levelId) {
2302
2630
  new InternalTrafficLight(trafficLightData[i]);
2303
2631
  }
2304
2632
 
2305
- if(COW_LEVELS_ENABLED) {
2306
- var cowGroupData = JSON.parse(state.cows);
2307
- for (var i = 0; i < cowGroupData.length; i++) {
2308
- // Add new group to group select element
2309
- if (i >= Object.keys(cowGroups).length) {
2310
- addCowGroup();
2311
- }
2312
- var cowGroupId = Object.keys(cowGroups)[i];
2313
- cowGroups[cowGroupId].minCows = cowGroupData[i].minCows;
2314
- cowGroups[cowGroupId].maxCows = cowGroupData[i].maxCows;
2315
- cowGroups[cowGroupId].type = cowGroupData[i].type;
2316
-
2317
- if (cowGroupData[i].potentialCoordinates != null) {
2318
- for (var j = 0; j < cowGroupData[i].potentialCoordinates.length; j++) {
2319
- var cowData = {
2320
- coordinates: [cowGroupData[i].potentialCoordinates[j]],
2321
- group: cowGroups[cowGroupId]
2322
- };
2323
- new InternalCow(cowData);
2324
- }
2325
- }
2326
- }
2327
-
2328
- // Trigger change listener on cow group select box to set initial min/max values
2329
- $('#cow_group_select').change();
2330
-
2331
- markCowNodes();
2332
- }
2333
-
2334
2633
  // Load in destination and origin nodes
2335
2634
  if (state.destinations) {
2336
2635
  var houses = JSON.parse(state.destinations);
@@ -2349,7 +2648,7 @@ ocargo.LevelEditor = function(levelId) {
2349
2648
  }
2350
2649
 
2351
2650
  // Load in character
2352
- $('#character_select').val(state.character);
2651
+ $('#character_select').val(characterId);
2353
2652
  $('#character_select').change();
2354
2653
 
2355
2654
  drawAll();
@@ -2378,6 +2677,36 @@ ocargo.LevelEditor = function(levelId) {
2378
2677
  PAPER_HEIGHT - currentTheme.decor[decor[i].decorName].height - decor[i].y + PAPER_PADDING);
2379
2678
  }
2380
2679
 
2680
+ //Load in cow data
2681
+ if(COW_LEVELS_ENABLED) {
2682
+ var cowGroupData = JSON.parse(state.cows);
2683
+ for (var i = 0; i < cowGroupData.length; i++) {
2684
+ // Add new group to group select element
2685
+ if (i >= Object.keys(cowGroups).length) {
2686
+ addCowGroup();
2687
+ }
2688
+ var cowGroupId = Object.keys(cowGroups)[i];
2689
+ cowGroups[cowGroupId].minCows = cowGroupData[i].minCows;
2690
+ cowGroups[cowGroupId].maxCows = cowGroupData[i].maxCows;
2691
+ cowGroups[cowGroupId].type = cowGroupData[i].type;
2692
+
2693
+ if (cowGroupData[i].potentialCoordinates != null) {
2694
+ for (var j = 0; j < cowGroupData[i].potentialCoordinates.length; j++) {
2695
+ var cowData = {
2696
+ coordinates: [cowGroupData[i].potentialCoordinates[j]],
2697
+ group: cowGroups[cowGroupId]
2698
+ };
2699
+ new InternalCow(cowData);
2700
+ }
2701
+ }
2702
+ }
2703
+
2704
+ // Trigger change listener on cow group select box to set initial min/max values
2705
+ $('#cow_group_select').change();
2706
+
2707
+ markCowNodes();
2708
+ }
2709
+
2381
2710
  // Load in block data
2382
2711
  if(state.blocks) {
2383
2712
  for(var i = 0; i < BLOCKS.length; i++) {
@@ -2397,21 +2726,28 @@ ocargo.LevelEditor = function(levelId) {
2397
2726
 
2398
2727
  // Load in language data
2399
2728
  var languageSelect = $('#language_select');
2400
- if (state.blocklyEnabled && state.pythonViewEnabled){
2729
+ if (state.blockly_enabled && state.python_view_enabled){
2401
2730
  languageSelect.val('blocklyWithPythonView');
2402
- } else if(state.blocklyEnabled && state.pythonEnabled) {
2731
+ } else if(state.blockly_enabled && state.python_enabled) {
2403
2732
  languageSelect.val('both');
2404
- } else if(state.pythonEnabled) {
2733
+ } else if(state.python_enabled) {
2405
2734
  languageSelect.val('python');
2406
2735
  } else {
2407
2736
  languageSelect.val('blockly');
2408
2737
  }
2409
2738
  languageSelect.change();
2410
2739
 
2740
+ // Load in description and hint data
2741
+ $('#subtitle').val(state.subtitle);
2742
+ $('#description').val(state.lesson);
2743
+ $('#hint').val(state.hint);
2744
+
2411
2745
  // Other data
2412
2746
  if(state.max_fuel) {
2413
2747
  $('#max_fuel').val(state.max_fuel);
2414
2748
  }
2749
+
2750
+ needsApproval = state.needs_approval;
2415
2751
  }
2416
2752
 
2417
2753
  function loadLevel(levelID) {
@@ -2520,10 +2856,13 @@ ocargo.LevelEditor = function(levelId) {
2520
2856
 
2521
2857
  function canShare() {
2522
2858
  if (!saveState.isSaved()) {
2523
- ocargo.Drawing.startPopup(gettext('Sharing'), '', gettext('Please save your level before continuing!'));
2859
+ ocargo.Drawing.startPopup("Sharing", "", "Please save your level before continuing!");
2524
2860
  return false;
2525
2861
  } else if (hasLevelChangedSinceSave()) {
2526
- ocargo.Drawing.startPopup(gettext('Sharing'), '', gettext('Please save your latest changes!'));
2862
+ ocargo.Drawing.startPopup("Sharing", "", "Please save your latest changes!");
2863
+ return false;
2864
+ } else if (needsApproval) {
2865
+ ocargo.Drawing.startPopup("Sharing", "", "Your teacher hasn't approved your level so you can't share it yet. Please let your teacher know they need to approve it first.")
2527
2866
  return false;
2528
2867
  }
2529
2868
  return true;
@@ -2586,7 +2925,7 @@ ocargo.LevelEditor = function(levelId) {
2586
2925
  // you can copy and paste into a Django migration file
2587
2926
  var state = extractState();
2588
2927
 
2589
- var boolFields = ["pythonEnabled", "blocklyEnabled", 'fuel_gauge', 'direct_drive'];
2928
+ var boolFields = ["python_enabled", "blockly_enabled", 'fuel_gauge'];
2590
2929
  var stringFields = ['path', 'traffic_lights', 'cows', 'origin', 'destinations'];
2591
2930
  var otherFields = ['max_fuel'];
2592
2931
 
@@ -2637,9 +2976,8 @@ ocargo.LevelEditor = function(levelId) {
2637
2976
  if (!this.valid) {
2638
2977
  throw "Error: cannot create actual cow from invalid internal cow!";
2639
2978
  }
2640
-
2641
2979
  // Where the cow is placed.
2642
- var coordinates = this.controlledNode.coordinate;
2980
+ var coordinates = this.coordinate;
2643
2981
  var strCoordinates= {'x':coordinates.x, 'y':coordinates.y};
2644
2982
 
2645
2983
  return { "coordinates": [strCoordinates],
@@ -2661,17 +2999,50 @@ ocargo.LevelEditor = function(levelId) {
2661
2999
 
2662
3000
  };
2663
3001
 
3002
+ this.isOnRoad = function() {
3003
+ return this.coordinate && ocargo.Node.findNodeByCoordinate(this.coordinate, nodes);
3004
+ }
3005
+
3006
+ this.updateTheme = function() {
3007
+ let newType = currentTheme == THEMES.city ? ocargo.Cow.PIGEON : ocargo.Cow.WHITE;
3008
+ let transformDimensions = this["image"]["_"]["transform"][0]
3009
+ let rotateDimensions = this["image"]["_"]["transform"][1]
3010
+ let x = transformDimensions[1]
3011
+ let y = transformDimensions[2]
3012
+ let r = 0;
3013
+ if (rotateDimensions) {
3014
+ r = rotateDimensions[1];
3015
+ }
3016
+
3017
+ this.image.remove();
3018
+
3019
+ this.image = drawing.createCowImage(newType);
3020
+ if (this.isOnRoad()) {
3021
+ let controlledNode = ocargo.Node.findNodeByCoordinate(coordinates, nodes);
3022
+ drawing.setCowImagePosition(this.coordinate, this.image, controlledNode);
3023
+ } else {
3024
+ this.image.transform("t" + x + "," + y + " r" + r);
3025
+ }
3026
+
3027
+ setupCowListeners(this);
3028
+ }
3029
+
2664
3030
  this.image = drawing.createCowImage(data.group.type);
2665
3031
  this.valid = false;
2666
3032
 
2667
-
2668
3033
  if ( data.coordinates && data.coordinates.length > 0 ) {
2669
- var coordinates = new ocargo.Coordinate(data.coordinates[0].x, data.coordinates[0].y);
2670
- this.controlledNode = ocargo.Node.findNodeByCoordinate(coordinates, nodes);
3034
+ this.coordinate = new ocargo.Coordinate(data.coordinates[0].x, data.coordinates[0].y);
3035
+ this.valid = isValidDraggedCowPlacement(this.coordinate, this);
2671
3036
 
2672
- if (this.controlledNode) {
2673
- this.valid = true;
2674
- drawing.setCowImagePosition(coordinates, this.image, this.controlledNode);
3037
+ if (this.isOnRoad()) {
3038
+ const controlledNode = ocargo.Node.findNodeByCoordinate(this.coordinate, nodes);
3039
+ drawing.setCowImagePosition(this.coordinate, this.image, controlledNode);
3040
+ } else {
3041
+ const box = this.image.getBBox();
3042
+ // calculate position of the image
3043
+ const paperX = (this.coordinate.x + 1) * GRID_SPACE_SIZE - box.width/2;
3044
+ const paperY = (GRID_HEIGHT - this.coordinate.y) * GRID_SPACE_SIZE - box.height/2;
3045
+ this.image.transform('t' + paperX + ',' + paperY );
2675
3046
  }
2676
3047
  } else {
2677
3048
  this.image.transform('...t' + (-paper.scrollLeft()) + ',' + paper.scrollTop());
@@ -2679,6 +3050,7 @@ ocargo.LevelEditor = function(levelId) {
2679
3050
 
2680
3051
  setupCowListeners(this);
2681
3052
  this.image.attr({'cursor':'pointer'});
3053
+ this.image.attr({'position': 'absolute'});
2682
3054
  cows.push(this);
2683
3055
 
2684
3056
  }
@@ -2808,7 +3180,7 @@ ocargo.LevelEditor = function(levelId) {
2808
3180
  /******************/
2809
3181
 
2810
3182
  $(function() {
2811
- var editor = new ocargo.LevelEditor(LEVEL); // This seems unused but removing it breaks the editor page.
3183
+ new ocargo.LevelEditor(LEVEL);
2812
3184
  var subtitle = interpolate(
2813
3185
  gettext('Click %(help_icon)s%(help_label)s for clues on getting started.'), {
2814
3186
  help_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/help.svg', 'popupHelp'),
@@ -2821,7 +3193,7 @@ $(function() {
2821
3193
  } else {
2822
3194
  let buttons = '';
2823
3195
  buttons += ocargo.button.dismissButtonHtml("edit_button", "Edit");
2824
- buttons += ocargo.button.redirectButtonHtml("play_button", Urls.levels() + "custom/" + LEVEL,"Play");
3196
+ buttons += ocargo.button.redirectButtonHtml("play_button", Urls.levels() + "custom/" + LEVEL, "Play");
2825
3197
 
2826
3198
  ocargo.Drawing.startPopup(
2827
3199
  gettext('Welcome back!'),