From c8920c42193759f6d04974cfaf73adae2cec8978 Mon Sep 17 00:00:00 2001 From: Eve Martin Date: Wed, 24 Apr 2024 09:24:20 +0000 Subject: [PATCH 01/62] feat: add language dropdown --- game/static/game/css/game_screen.css | 11 + game/static/game/js/blockly/msg/js/en-gb.js | 5 +- game/static/game/js/blockly/msg/js/en.js | 5 +- game/static/game/js/blockly/msg/js/fr.js | 5 +- game/static/game/js/blocklyControl.js | 9 +- game/static/game/js/blocklyCustomBlocks.js | 4 +- game/static/game/js/game.js | 227 ++++++++++---------- game/static/game/js/loadLanguages.js | 18 ++ game/templates/game/game.html | 21 +- game/views/language_code_conversions.py | 87 ++++++++ game/views/level.py | 3 + 11 files changed, 280 insertions(+), 115 deletions(-) create mode 100644 game/static/game/js/loadLanguages.js create mode 100644 game/views/language_code_conversions.py diff --git a/game/static/game/css/game_screen.css b/game/static/game/css/game_screen.css index 549667581..33cc000ed 100644 --- a/game/static/game/css/game_screen.css +++ b/game/static/game/css/game_screen.css @@ -430,6 +430,17 @@ div.no-print div.tab label { background: rgba(255, 255, 255, 0.25); } +.tab select:hover { + background: rgba(255, 255, 255, 0.25); +} + +.tab select { + color: black; + background-color: #86ae18; + border: none; + font-weight: bold; +} + #console { z-index: 100; display: flex; diff --git a/game/static/game/js/blockly/msg/js/en-gb.js b/game/static/game/js/blockly/msg/js/en-gb.js index e22384b06..cc58bcd47 100755 --- a/game/static/game/js/blockly/msg/js/en-gb.js +++ b/game/static/game/js/blockly/msg/js/en-gb.js @@ -433,4 +433,7 @@ Blockly.Msg["VARIABLES_HUE"] = "330"; Blockly.Msg["TEXTS_HUE"] = "160"; Blockly.Msg["PROCEDURES_HUE"] = "290"; Blockly.Msg["COLOUR_HUE"] = "20"; -Blockly.Msg["VARIABLES_DYNAMIC_HUE"] = "310"; \ No newline at end of file +Blockly.Msg["VARIABLES_DYNAMIC_HUE"] = "310"; + +Blockly.Msg["MOVE_FORWARDS_TITLE"] = "move forwards but British"; +Blockly.Msg["MOVE_FORWARD_TOOLTIP"] = "Move the van forwards but British"; \ No newline at end of file diff --git a/game/static/game/js/blockly/msg/js/en.js b/game/static/game/js/blockly/msg/js/en.js index 24afa59b7..103d52aa4 100755 --- a/game/static/game/js/blockly/msg/js/en.js +++ b/game/static/game/js/blockly/msg/js/en.js @@ -433,4 +433,7 @@ Blockly.Msg["VARIABLES_HUE"] = "330"; Blockly.Msg["TEXTS_HUE"] = "160"; Blockly.Msg["PROCEDURES_HUE"] = "290"; Blockly.Msg["COLOUR_HUE"] = "20"; -Blockly.Msg["VARIABLES_DYNAMIC_HUE"] = "310"; \ No newline at end of file +Blockly.Msg["VARIABLES_DYNAMIC_HUE"] = "310"; + +Blockly.Msg["MOVE_FORWARDS_TITLE"] = "move forwards"; +Blockly.Msg["MOVE_FORWARD_TOOLTIP"] = "Move the van forwards"; \ No newline at end of file diff --git a/game/static/game/js/blockly/msg/js/fr.js b/game/static/game/js/blockly/msg/js/fr.js index dce0fe18a..b254a199a 100755 --- a/game/static/game/js/blockly/msg/js/fr.js +++ b/game/static/game/js/blockly/msg/js/fr.js @@ -433,4 +433,7 @@ Blockly.Msg["VARIABLES_HUE"] = "330"; Blockly.Msg["TEXTS_HUE"] = "160"; Blockly.Msg["PROCEDURES_HUE"] = "290"; Blockly.Msg["COLOUR_HUE"] = "20"; -Blockly.Msg["VARIABLES_DYNAMIC_HUE"] = "310"; \ No newline at end of file +Blockly.Msg["VARIABLES_DYNAMIC_HUE"] = "310"; + +Blockly.Msg["MOVE_FORWARDS_TITLE"] = "move forwards but French"; +Blockly.Msg["MOVE_FORWARD_TOOLTIP"] = "Move the van forwards but French"; \ No newline at end of file diff --git a/game/static/game/js/blocklyControl.js b/game/static/game/js/blocklyControl.js index f3ade14ba..111dfcc59 100644 --- a/game/static/game/js/blocklyControl.js +++ b/game/static/game/js/blocklyControl.js @@ -8,6 +8,7 @@ ocargo.BlocklyControl = function () { this.blocklyCustomisations.setupLimitedBlocks(); this.blocklyDiv = document.getElementById("blockly_holder"); this.toolbox = document.getElementById("blockly_toolbox"); + Blockly.inject(this.blocklyDiv, { path: "/static/game/js/blockly/", toolbox: BLOCKLY_XML, @@ -52,6 +53,12 @@ ocargo.BlocklyControl.prototype.prepare = function (blocks) { } }; +ocargo.BlocklyControl.prototype.updateBlockLanguage = function (language_code) { + loadLanguage("/static/game/js/blockly/msg/js/", language_code, function() { + reloadWorkspace(Blockly.mainWorkspace); + }); +}; + ocargo.BlocklyControl.prototype.redrawBlockly = function () { Blockly.svgResize(Blockly.mainWorkspace); }; @@ -434,4 +441,4 @@ ocargo.BlockHandler.prototype.deselectCurrent = function () { this.setBlockSelected(this.selectedBlock, false); this.selectedBlock = null; } -}; +}; \ No newline at end of file diff --git a/game/static/game/js/blocklyCustomBlocks.js b/game/static/game/js/blocklyCustomBlocks.js index 7818a4e89..d59a27b95 100644 --- a/game/static/game/js/blocklyCustomBlocks.js +++ b/game/static/game/js/blocklyCustomBlocks.js @@ -39,7 +39,7 @@ function initCustomBlocksDescription() { init: function () { this.setColour(160); this.appendDummyInput() - .appendField(gettext("move forwards")) + .appendField(Blockly.Msg.MOVE_FORWARDS_TITLE) .appendField( new Blockly.FieldImage( ocargo.Drawing.imageDir + "actions/forward.svg", @@ -49,7 +49,7 @@ function initCustomBlocksDescription() { ); this.setPreviousStatement(true, "Action"); this.setNextStatement(true, "Action"); - this.setTooltip(gettext("Move the van forwards")); + this.setTooltip(Blockly.Msg.MOVE_FORWARD_TOOLTIP); }, }; diff --git a/game/static/game/js/game.js b/game/static/game/js/game.js index be1fae43e..c6be54efd 100644 --- a/game/static/game/js/game.js +++ b/game/static/game/js/game.js @@ -10,133 +10,138 @@ ocargo.Game = function () { } ocargo.Game.prototype.setup = function () { - if(new Date().getMonth() === 11) { - $("#paper").css('background-color', '#eef7ff') - } + let _this = this; - if (NIGHT_MODE_FEATURE_ENABLED) { - if (NIGHT_MODE) { - $('#paper').css('background-color', 'black') + loadLanguage("/static/game/js/blockly/msg/js/", navigator.language.toLowerCase(), function () { + if(new Date().getMonth() === 11) { + $("#paper").css('background-color', '#eef7ff') } - if (!ANONYMOUS) { - $('#nightmode_tab').show() + if (NIGHT_MODE_FEATURE_ENABLED) { + if (NIGHT_MODE) { + $('#paper').css('background-color', 'black') + } + + if (!ANONYMOUS) { + $('#nightmode_tab').show() + } } - } -// Function being called when there is a change in game level. -ocargo.Game.prototype.onLevelChange = function() { - const currentLevelId = LEVEL_ID; - localStorage.setItem('currentEpisode', EPISODE); -} + // Function being called when there is a change in game level. + ocargo.Game.prototype.onLevelChange = function() { + const currentLevelId = LEVEL_ID; + localStorage.setItem('currentEpisode', EPISODE); + } - restoreCmsLogin() - initCustomBlocks() - ocargo.solutionLoaded = false - ocargo.blocklyControl = new ocargo.BlocklyControl() - ocargo.pythonControl = new ocargo.PythonControl() - ocargo.blocklyCompiler = new ocargo.BlocklyCompiler() - ocargo.model = new ocargo.Model( - PATH, - ORIGIN, - DESTINATIONS, - TRAFFIC_LIGHTS, - COWS, - MAX_FUEL - ) - this.drawing = new ocargo.Drawing(ocargo.model.startingPosition()) - this.drawing.preloadRoadTiles() - ocargo.animation = new ocargo.Animation(ocargo.model, DECOR, this.drawing) - this.saving = new ocargo.Saving() - this.sharing = new ocargo.Sharing( - () => parseInt(LEVEL_ID), - () => true - ) + restoreCmsLogin() + initCustomBlocks() + ocargo.solutionLoaded = false + ocargo.blocklyControl = new ocargo.BlocklyControl() + ocargo.pythonControl = new ocargo.PythonControl() + ocargo.blocklyCompiler = new ocargo.BlocklyCompiler() + ocargo.model = new ocargo.Model( + PATH, + ORIGIN, + DESTINATIONS, + TRAFFIC_LIGHTS, + COWS, + MAX_FUEL + ) - // Setup the blockly workspace - ocargo.blocklyControl.reset() - ocargo.blocklyControl.loadPreviousAttempt() - ocargo.pythonControl.loadPreviousAttempt() + _this.drawing = new ocargo.Drawing(ocargo.model.startingPosition()) + _this.drawing.preloadRoadTiles() + ocargo.animation = new ocargo.Animation(ocargo.model, DECOR, _this.drawing) + _this.saving = new ocargo.Saving() + _this.sharing = new ocargo.Sharing( + () => parseInt(LEVEL_ID), + () => true + ) - // Setup the ui - this._setupConsoleSliderListeners() - this._setupPythonViewSliderListeners() - this._setupConsoleLogViewSliderListeners() - this._setupDirectDriveListeners() - this._setupFuelGauge(ocargo.model.map.nodes, BLOCKS) - this._setupTabs() + // Setup the blockly workspace + ocargo.blocklyControl.reset() + ocargo.blocklyControl.loadPreviousAttempt() + ocargo.pythonControl.loadPreviousAttempt() - this.onStopControls() + // Setup the ui + _this._setupConsoleSliderListeners() + _this._setupPythonViewSliderListeners() + _this._setupConsoleLogViewSliderListeners() + _this._setupDirectDriveListeners() + _this._setupFuelGauge(ocargo.model.map.nodes, BLOCKS) + _this._setupTabs() - // default controller - if (BLOCKLY_ENABLED) { - ocargo.controller = ocargo.blocklyControl - } else { - ocargo.controller = ocargo.pythonControl - } + _this.onStopControls() - // Setup blockly to python - Blockly.Python.init(Blockly.getMainWorkspace()) - window.addEventListener( - 'unload', - function (event) { - ocargo.pythonControl.teardown() - ocargo.blocklyControl.teardown() - }.bind(this) - ) + // default controller + if (BLOCKLY_ENABLED) { + ocargo.controller = ocargo.blocklyControl + } else { + ocargo.controller = ocargo.pythonControl + } - var loggedOutWarning = '' - // Check if logged on - if (USER_STATUS == 'UNTRACKED') { - loggedOutWarning = - '
' + - gettext("You are not logged in. Your progress won't be saved.") + - '' - } - // Start the popup - var title = gettext('Try solving this one...') - if (LEVEL_ID) { - var titlePrefix = '' - if (NIGHT_MODE) { - titlePrefix = gettext('Night Level %(level_name)s') - } else if (DEFAULT_LEVEL) { - titlePrefix = gettext('Level %(level_name)s') + // Setup blockly to python + Blockly.Python.init(Blockly.getMainWorkspace()) + window.addEventListener( + 'unload', + function (event) { + ocargo.pythonControl.teardown() + ocargo.blocklyControl.teardown() + }.bind(this) + ) + + var loggedOutWarning = '' + // Check if logged on + if (USER_STATUS == 'UNTRACKED') { + loggedOutWarning = + '
' + + gettext("You are not logged in. Your progress won't be saved.") + + '' } - if (titlePrefix) { - title = interpolate(titlePrefix, { level_name: LEVEL_NAME }, true) - } else { - title = LEVEL_NAME + // Start the popup + var title = gettext('Try solving this one...') + if (LEVEL_ID) { + var titlePrefix = '' + if (NIGHT_MODE) { + titlePrefix = gettext('Night Level %(level_name)s') + } else if (DEFAULT_LEVEL) { + titlePrefix = gettext('Level %(level_name)s') + } + if (titlePrefix) { + title = interpolate(titlePrefix, { level_name: LEVEL_NAME }, true) + } else { + title = LEVEL_NAME + } } - } - var message - if (NIGHT_MODE) { - message = - '
' + - gettext( - 'In Night Mode you can only see a very short distance. ' + - "We've given you more blocks to help you find your way!" - ) - } else { - message = loggedOutWarning - } + var message + if (NIGHT_MODE) { + message = + '
' + + gettext( + 'In Night Mode you can only see a very short distance. ' + + "We've given you more blocks to help you find your way!" + ) + } else { + message = loggedOutWarning + } - this.drawing.enablePanning() + _this.drawing.enablePanning() - const showMascot = BLOCKLY_ENABLED && !PYTHON_VIEW_ENABLED && LEVEL_NAME <= 80; // show mascot on Blockly-only levels that are not above 80 + const showMascot = BLOCKLY_ENABLED && !PYTHON_VIEW_ENABLED && LEVEL_NAME <= 80; // show mascot on Blockly-only levels that are not above 80 - ocargo.Drawing.startPopup( - title, - LESSON, - message, - showMascot, - [ - ocargo.button.dismissButtonHtml("prev_button", gettext("Previous level")), - ocargo.button.dismissButtonHtml('play_button', gettext('Play')), - ocargo.button.dismissButtonHtml("next_button", gettext("Next level")) - ] - ) + ocargo.Drawing.startPopup( + title, + LESSON, + message, + showMascot, + [ + ocargo.button.dismissButtonHtml("prev_button", gettext("Previous level")), + ocargo.button.dismissButtonHtml('play_button', gettext('Play')), + ocargo.button.dismissButtonHtml("next_button", gettext("Next level")) + ] + ) + }) } // Script used to save and check for episode upon loading of the webpage @@ -1387,8 +1392,14 @@ function setMutedCookie(mute) { } } +function gameUpdateBlockLanguage(language_code) { + ocargo.blocklyControl.updateBlockLanguage(language_code); +} + $(document).ready(function () { ocargo.game = new ocargo.Game() ocargo.game.setup() ocargo.game.mute(ocargo.game.isMuted) }) + + diff --git a/game/static/game/js/loadLanguages.js b/game/static/game/js/loadLanguages.js new file mode 100644 index 000000000..c0890f0be --- /dev/null +++ b/game/static/game/js/loadLanguages.js @@ -0,0 +1,18 @@ +function loadLanguage(path, langStr, callback) { + var xobj = new XMLHttpRequest(); + xobj.overrideMimeType("application/javascript"); + xobj.open('GET', path + langStr + '.js', true); + xobj.onreadystatechange = function () { + if (xobj.readyState == 4 && xobj.status == "200") { + eval(xobj.responseText); + callback(); + } + }; + xobj.send(null); +}; + +function reloadWorkspace(workspace) { + var blocklyDom = Blockly.Xml.workspaceToDom(workspace); + workspace.clear(); + Blockly.Xml.domToWorkspace(blocklyDom, workspace); +}; \ No newline at end of file diff --git a/game/templates/game/game.html b/game/templates/game/game.html index 72c6f2a5f..f1044bdeb 100644 --- a/game/templates/game/game.html +++ b/game/templates/game/game.html @@ -88,7 +88,6 @@ - @@ -104,6 +103,7 @@ + @@ -143,6 +143,7 @@ {% block content %} {{ block.super }} +
@@ -158,6 +159,16 @@ {% trans "Blockly" %} +
+ +
+ +
+
- {% for language_code, language in available_language_dict.items %} {% endfor %} From 1c3a1246ef43c38d9dd9beb764225b45898b88fb Mon Sep 17 00:00:00 2001 From: Eve Martin Date: Thu, 25 Apr 2024 15:01:02 +0000 Subject: [PATCH 14/62] debug tests --- game/end_to_end_tests/base_game_test.py | 3 --- game/end_to_end_tests/game_page.py | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/game/end_to_end_tests/base_game_test.py b/game/end_to_end_tests/base_game_test.py index e03398826..d555196f5 100644 --- a/game/end_to_end_tests/base_game_test.py +++ b/game/end_to_end_tests/base_game_test.py @@ -87,14 +87,12 @@ def go_to_homepage(self): def go_to_level(self, level_name): path = reverse("play_default_level", kwargs={"levelName": str(level_name)}) self._go_to_path(path) - self.selenium.execute_script("ocargo.animation.FAST_ANIMATION_DURATION = 1;") return GamePage(self.selenium) def go_to_custom_level(self, level): path = reverse("play_custom_level", kwargs={"levelId": str(level.id)}) self._go_to_path(path) - self.selenium.execute_script("ocargo.animation.FAST_ANIMATION_DURATION = 1;") return GamePage(self.selenium) @@ -106,7 +104,6 @@ def go_to_level_editor(self): def go_to_episode(self, episodeId): path = reverse("start_episode", kwargs={"episodeId": str(episodeId)}) self._go_to_path(path) - self.selenium.execute_script("ocargo.animation.FAST_ANIMATION_DURATION = 1;") return GamePage(self.selenium) diff --git a/game/end_to_end_tests/game_page.py b/game/end_to_end_tests/game_page.py index 9cb32f3ce..1c723ff50 100644 --- a/game/end_to_end_tests/game_page.py +++ b/game/end_to_end_tests/game_page.py @@ -21,6 +21,7 @@ def __init__(self, browser): assert self.on_correct_page("game_page") self._dismiss_initial_dialog() + self.browser.execute_script("ocargo.animation.FAST_ANIMATION_DURATION = 1;") def _dismiss_initial_dialog(self): self.dismiss_dialog("play_button") From 0a0ce2789c6e9c7ae741c9515ebd239bf6948da0 Mon Sep 17 00:00:00 2001 From: Eve Martin Date: Fri, 26 Apr 2024 08:55:38 +0000 Subject: [PATCH 15/62] testing --- game/end_to_end_tests/base_game_test.py | 3 --- game/end_to_end_tests/game_page.py | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/game/end_to_end_tests/base_game_test.py b/game/end_to_end_tests/base_game_test.py index e03398826..d555196f5 100644 --- a/game/end_to_end_tests/base_game_test.py +++ b/game/end_to_end_tests/base_game_test.py @@ -87,14 +87,12 @@ def go_to_homepage(self): def go_to_level(self, level_name): path = reverse("play_default_level", kwargs={"levelName": str(level_name)}) self._go_to_path(path) - self.selenium.execute_script("ocargo.animation.FAST_ANIMATION_DURATION = 1;") return GamePage(self.selenium) def go_to_custom_level(self, level): path = reverse("play_custom_level", kwargs={"levelId": str(level.id)}) self._go_to_path(path) - self.selenium.execute_script("ocargo.animation.FAST_ANIMATION_DURATION = 1;") return GamePage(self.selenium) @@ -106,7 +104,6 @@ def go_to_level_editor(self): def go_to_episode(self, episodeId): path = reverse("start_episode", kwargs={"episodeId": str(episodeId)}) self._go_to_path(path) - self.selenium.execute_script("ocargo.animation.FAST_ANIMATION_DURATION = 1;") return GamePage(self.selenium) diff --git a/game/end_to_end_tests/game_page.py b/game/end_to_end_tests/game_page.py index 804c37ed0..187da9dc5 100644 --- a/game/end_to_end_tests/game_page.py +++ b/game/end_to_end_tests/game_page.py @@ -22,6 +22,7 @@ def __init__(self, browser): assert self.on_correct_page("game_page") self._dismiss_initial_dialog() + self.browser.execute_script("ocargo.animation.FAST_ANIMATION_DURATION = 1;") def _dismiss_initial_dialog(self): self.dismiss_dialog("play_button") From 1295f1fb32c291778f5d4cc54e9df4d0b1f374ca Mon Sep 17 00:00:00 2001 From: Eve Martin Date: Fri, 26 Apr 2024 09:03:05 +0000 Subject: [PATCH 16/62] testing --- game/end_to_end_tests/game_page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/end_to_end_tests/game_page.py b/game/end_to_end_tests/game_page.py index 187da9dc5..5179b4814 100644 --- a/game/end_to_end_tests/game_page.py +++ b/game/end_to_end_tests/game_page.py @@ -18,11 +18,11 @@ class GamePage(BasePage): def __init__(self, browser): super(GamePage, self).__init__(browser) + self.browser.execute_script("ocargo.animation.FAST_ANIMATION_DURATION = 1;") assert self.on_correct_page("game_page") self._dismiss_initial_dialog() - self.browser.execute_script("ocargo.animation.FAST_ANIMATION_DURATION = 1;") def _dismiss_initial_dialog(self): self.dismiss_dialog("play_button") From 87c74d6c08bd0b383e8e1701a1feabfc85385a51 Mon Sep 17 00:00:00 2001 From: Eve Martin Date: Fri, 26 Apr 2024 10:19:44 +0000 Subject: [PATCH 17/62] debug tests --- game/end_to_end_tests/game_page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/end_to_end_tests/game_page.py b/game/end_to_end_tests/game_page.py index 1c723ff50..a85f752fd 100644 --- a/game/end_to_end_tests/game_page.py +++ b/game/end_to_end_tests/game_page.py @@ -18,10 +18,10 @@ class GamePage(BasePage): def __init__(self, browser): super(GamePage, self).__init__(browser) + self.browser.execute_script("ocargo.animation.FAST_ANIMATION_DURATION = 1;") assert self.on_correct_page("game_page") self._dismiss_initial_dialog() - self.browser.execute_script("ocargo.animation.FAST_ANIMATION_DURATION = 1;") def _dismiss_initial_dialog(self): self.dismiss_dialog("play_button") From f85daba66f5f347748220c0e98d0aaa892b8023f Mon Sep 17 00:00:00 2001 From: Eve Martin Date: Fri, 26 Apr 2024 10:26:14 +0000 Subject: [PATCH 18/62] debug tests --- game/end_to_end_tests/game_page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/end_to_end_tests/game_page.py b/game/end_to_end_tests/game_page.py index a85f752fd..5488380e4 100644 --- a/game/end_to_end_tests/game_page.py +++ b/game/end_to_end_tests/game_page.py @@ -18,9 +18,9 @@ class GamePage(BasePage): def __init__(self, browser): super(GamePage, self).__init__(browser) - self.browser.execute_script("ocargo.animation.FAST_ANIMATION_DURATION = 1;") assert self.on_correct_page("game_page") + self.browser.execute_script("ocargo.animation.FAST_ANIMATION_DURATION = 1;") self._dismiss_initial_dialog() def _dismiss_initial_dialog(self): From c6026cf4353c6fb50ecd1dd41dec67d2581512ba Mon Sep 17 00:00:00 2001 From: Eve Martin Date: Fri, 26 Apr 2024 10:55:07 +0000 Subject: [PATCH 19/62] add dropdown with dummy onchange function --- game/static/game/js/game.js | 4 ++ game/templates/game/game.html | 19 ++++++ game/views/language_code_conversions.py | 87 +++++++++++++++++++++++++ game/views/level.py | 2 + 4 files changed, 112 insertions(+) create mode 100644 game/views/language_code_conversions.py diff --git a/game/static/game/js/game.js b/game/static/game/js/game.js index be1fae43e..b0bcc0f21 100644 --- a/game/static/game/js/game.js +++ b/game/static/game/js/game.js @@ -1387,6 +1387,10 @@ function setMutedCookie(mute) { } } +function gameUpdateBlockLanguage(language_code) { + console.log(language_code); +} + $(document).ready(function () { ocargo.game = new ocargo.Game() ocargo.game.setup() diff --git a/game/templates/game/game.html b/game/templates/game/game.html index 72c6f2a5f..e45552d4a 100644 --- a/game/templates/game/game.html +++ b/game/templates/game/game.html @@ -158,6 +158,17 @@ {% trans "Blockly" %}
+ +
+ +
+ +
+
-
+