diff --git a/src/activities/railroad/Loco.qml b/src/activities/railroad/Loco.qml index 7eb24a31a..ee34d1b4c 100644 --- a/src/activities/railroad/Loco.qml +++ b/src/activities/railroad/Loco.qml @@ -1,43 +1,43 @@ /* GCompris - Loco.qml * * Copyright (C) 2017 Utkarsh Tiwari * * Authors: * Pascal Georges (GTK+ version) * Utkarsh Tiwari (Qt Quick port) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ import QtQuick 2.6 import GCompris 1.0 import "railroad.js" as Activity Item { id: draggedItem - property int imageIndex + property string imageURL Image { id: img - source: Activity.resourceURL + "loco" + imageIndex + ".svg" + source: imageURL height: background.height / 8.0 width: ((background.width > background.height) ? background.width : background.height) / 5.66 Drag.hotSpot.x: width / 2 Drag.hotSpot.y: height / 2 } function destroy() { // Destroy this copy object on drop draggedItem.destroy(); } } diff --git a/src/activities/railroad/Railroad.qml b/src/activities/railroad/Railroad.qml index 06ecaf553..e3283568d 100644 --- a/src/activities/railroad/Railroad.qml +++ b/src/activities/railroad/Railroad.qml @@ -1,592 +1,592 @@ /* GCompris - railroad.qml * * Copyright (C) 2016 Utkarsh Tiwari * * Authors: * Pascal Georges (GTK+ version) * Utkarsh Tiwari (Qt Quick port) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ import QtQuick 2.6 import GCompris 1.0 import "../../core" import "railroad.js" as Activity ActivityBase { id: activity onStart: focus = true onStop: {} pageComponent: Image { id: background source: Activity.resourceURL + "railroad-bg.svg" height: activity.height / 2 width: activity.width anchors.fill: parent signal start signal stop Component.onCompleted: { activity.start.connect(start) activity.stop.connect(stop) } // Add here the QML items you need to access in javascript QtObject { id: items property Item main: activity.main property alias background: background property alias bar: bar property alias bonus: bonus property alias score: score property alias timer: timer property alias sampleList: sampleList property alias listModel: listModel property alias answerZone: answerZone property alias animateFlow: animateFlow property alias introMessage: introMessage property bool memoryMode: false property bool mouseEnabled: true property var currentKeyZone: sampleList property bool keyNavigationMode: false property int sampleImageHeight: 0 //Stores height of sampleGrid images to set rail bar support position. } onStart: { Activity.start(items) } onStop: { Activity.stop() } Keys.enabled: !timer.running && !animateFlow.running && !introMessage.visible Keys.onPressed: { items.keyNavigationMode = true; items.currentKeyZone.handleKeys(event); activity.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/smudge.wav'); } // Countdown timer Timer { id: timer repeat: false interval: 4000 onTriggered: { items.animateFlow.start() activity.audioEffects.play(Activity.resourceURL + 'sounds/train.wav') } } // Intro message IntroMessage { id: introMessage y: background.height / 4.7 anchors { right: parent.right rightMargin: 5 left: parent.left leftMargin: 5 } z: score.z + 1 onIntroDone: { timer.start() } intro: [ qsTr("Observe and remember the train before the timer ends and then drag the items to set up a similar train."), qsTr("If you forget the positions, you can click on the Hint button to view them again.") ] } // Top Display Area Rectangle { id: topDisplayArea width: background.width height: background.height / 4.5 y: background.height / 12.7 color: 'transparent' z: 1 GridView { id: answerZone readonly property int levelCellWidth: (background.width > background.height) ? background.width / (listModel.count > 5 ? 7.2 : 5.66) : background.width / ((listModel.count > 5) ? 7.1 : 5) readonly property int levelCellHeight: background.width > background.height ? topDisplayArea.height / 2 : topDisplayArea.height / 3 width: parent.width height: background.height / 8 cellWidth: levelCellWidth cellHeight: levelCellHeight x: parent.x y: background.width > background.height ? sampleList.y - height * 1.55 : sampleList.y - height * 1.29 interactive: false model: listModel delegate: Image { id: wagon source: Activity.resourceURL + modelData + ".svg" height: answerZone.levelCellHeight width: answerZone.levelCellWidth sourceSize.width: width function checkDrop(dragItem) { // Checks the drop location of this wagon var globalCoordinates = dragItem.mapToItem(answerZone, 0, 0) if(globalCoordinates.y <= ((background.height / 12.5) + (background.height / 8))) { var dropIndex = Activity.getDropIndex(globalCoordinates.x) if(dropIndex > (listModel.count - 1)) { // Handles index overflow dropIndex = listModel.count - 1 } listModel.move(listModel.count - 1, dropIndex, 1) opacity = 1 } if(globalCoordinates.y > (background.height / 8)) { // Remove it if dropped in the lower section activity.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/smudge.wav') listModel.remove(listModel.count - 1) } } function createNewItem() { var component = Qt.createComponent("Loco.qml"); if(component.status === Component.Ready) { - var newItem = component.createObject(parent, {"x": x, "y": y, "z": 10, "imageIndex": listModel.get(index).id}); + var newItem = component.createObject(parent, {"x": x, "y": y, "z": 10, "imageURL": source}); } return newItem } MouseArea { id: displayWagonMouseArea hoverEnabled: true enabled: (introMessage.visible ? false : true) && items.mouseEnabled anchors.fill: parent onPressed: { if(items.memoryMode == true) { drag.target = parent.createNewItem(); parent.opacity = 0 listModel.move(index, listModel.count - 1, 1) } answerZone.selectedSwapIndex = -1; } onReleased: { if(items.memoryMode == true) { var dragItem = drag.target parent.checkDrop(dragItem) dragItem.destroy(); parent.Drag.cancel() } } onClicked: { // skips memorization time. if(!items.memoryMode) { bar.hintClicked() } else { items.currentKeyZone = answerZone if(items.keyNavigationMode) { answerZone.currentIndex = index } } answerZone.selectedSwapIndex = -1; } } states: State { name: "wagonHover" when: displayWagonMouseArea.containsMouse && (items.memoryMode === true) PropertyChanges { target: wagon scale: 1.1 } } } onXChanged: { if(answerZone.x >= background.width) { timer.stop() animateFlow.stop(); answerZone.x = 2; listModel.clear(); items.memoryMode = true; } } PropertyAnimation { id: animateFlow target: answerZone properties: "x" from: answerZone.x to: background.width duration: 4000 easing.type: Easing.InExpo loops: 1 onStopped: answerZone.x = 2; } function handleKeys(event) { // Switch zones via tab key. if(event.key === Qt.Key_Tab) { items.currentKeyZone = sampleList sampleList.currentIndex = 0 answerZone.currentIndex = -1 } if(event.key === Qt.Key_Down) { items.currentKeyZone = sampleList answerZone.currentIndex = -1 sampleList.currentIndex = 0 } if(event.key === Qt.Key_Up) { items.currentKeyZone = sampleList answerZone.currentIndex = -1 sampleList.currentIndex = 0 } if(event.key === Qt.Key_Left) { items.currentKeyZone = answerZone answerZone.moveCurrentIndexLeft() } if(event.key === Qt.Key_Right) { items.currentKeyZone = answerZone answerZone.moveCurrentIndexRight() } // Remove a wagon via Delete/Return key. if(event.key === Qt.Key_Delete && listModel.count > 0) { activity.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/smudge.wav') listModel.remove(answerZone.currentIndex) if(listModel.count < 2) { answerZone.selectedSwapIndex = -1; } } // Checks answer. if(event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { items.currentKeyZone = answerZone Activity.isAnswer(); } // Swaps two wagons with help of Space/Enter keys. if(event.key === Qt.Key_Space) { if(selectedSwapIndex === -1 && listModel.count > 1) { answerZone.selectedSwapIndex = answerZone.currentIndex; swapHighlight.x = answerZone.currentItem.x; swapHighlight.anchors.top = answerZone.top; } else if(answerZone.currentIndex != selectedSwapIndex && listModel.count > 1){ var min = Math.min(selectedSwapIndex, answerZone.currentIndex); var max = Math.max(selectedSwapIndex, answerZone.currentIndex); listModel.move(min, max, 1); listModel.move(max-1, min, 1); answerZone.selectedSwapIndex = -1; } } } // variable for storing the index of wagons to be swaped via key navigations. property int selectedSwapIndex: -1 Keys.enabled: true focus: true keyNavigationWraps: true highlightRangeMode: GridView.ApplyRange highlight: Rectangle { width: answerZone.cellWidth height: answerZone.cellHeight color: "blue" opacity: 0.3 radius: 5 visible: (items.currentKeyZone === answerZone) && (!timer.running && !animateFlow.running) && items.keyNavigationMode x: visible ? answerZone.currentItem.x : 0 y: visible ? answerZone.currentItem.y : 0 Behavior on x { SpringAnimation { spring: 3 damping: 0.2 } } Behavior on y { SpringAnimation { spring: 3 damping: 0.2 } } } highlightFollowsCurrentItem: false } // Used to highlight a wagon selected for swaping via key navigations. Rectangle { id: swapHighlight width: answerZone.cellWidth height: answerZone.cellHeight visible: answerZone.selectedSwapIndex != -1 ? true : false color: "#AA41AAC4" opacity: 0.8 radius: 5 } ListModel { id: listModel } } // Lower Sample Wagon Display Area GridView { id: sampleList visible: items.memoryMode y: background.height / 4.7 z: 5 width: background.width height: background.height - topDisplayArea.height anchors.margins: 20 cellWidth: width / columnCount cellHeight: (background.width > background.height) ? background.height / 7 : background.height / 7.5 model: Activity.dataset["noOfLocos"][bar.level - 1] + Activity.dataset["noOfWagons"][bar.level - 1] interactive: false // No. of wagons in a row readonly property int columnCount: (background.width > background.height) ? Activity.dataset["columnsInHorizontalMode"][bar.level - 1] : Activity.dataset["columsInVerticalMode"][bar.level - 1] readonly property int rowCount: columnCount > 0 ? model / columnCount : 0 delegate: Image { id: loco readonly property string uniqueID: Activity.uniqueId[index] property real originX property real originY source: Activity.resourceURL + uniqueID + ".svg" width: ((background.width > background.height) ? background.width / 5.66 : background.width / 4.2) sourceSize.width: width fillMode: Image.PreserveAspectFit visible: true onHeightChanged: items.sampleImageHeight = height onVisibleChanged: items.sampleImageHeight = height function initDrag() { originX = x originY = y } function replace() { x = originX y = originY } function checkDrop() { // Checks the drop location of this wagon var globalCoordinates = loco.mapToItem(answerZone, 0, 0) // checks if the wagon is dropped in correct zone and no. of wagons in answer row are less than // total no. of wagons in correct answer + 2, before dropping the wagon. if(globalCoordinates.y <= (background.height / 12.5) && listModel.count < Activity.dataset["WagonsInCorrectAnswers"][bar.level - 1] + 2) { activity.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/smudge.wav') var dropIndex = Activity.getDropIndex(globalCoordinates.x) Activity.addWagon(uniqueID, dropIndex); } } MouseArea { id: mouseArea hoverEnabled: true anchors.fill: parent drag.target: parent drag.axis: (parent.y >= 0 && parent.y <= background.height / 7.5) ? Drag.YAxis : Drag.XAndYAxis enabled: items.mouseEnabled onClicked: { items.currentKeyZone = sampleList if(items.keyNavigationMode) { sampleList.currentIndex = index } } onPressed: { parent.initDrag() } onReleased: { parent.Drag.cancel() parent.checkDrop() parent.replace() } } Component.onCompleted: initDrag(); states: State { name: "carHover" when: mouseArea.containsMouse PropertyChanges { target: loco scale: 1.1 } } } function handleKeys(event) { if(event.key === Qt.Key_Tab) { if(listModel.count > 0) { items.currentKeyZone = answerZone sampleList.currentIndex = -1 answerZone.currentIndex = 0 } } if(event.key === Qt.Key_Up) { items.currentKeyZone = sampleList // Checks if current highlighted element is in first row of the grid. if(sampleList.currentIndex < columnCount && listModel.count > 0) { items.currentKeyZone = answerZone answerZone.currentIndex = 0 sampleList.currentIndex = -1 } else { sampleList.moveCurrentIndexUp() } } if(event.key === Qt.Key_Down) { items.currentKeyZone = sampleList sampleList.moveCurrentIndexDown() } if(event.key === Qt.Key_Left) { items.currentKeyZone = sampleList sampleList.moveCurrentIndexLeft() } if(event.key === Qt.Key_Right) { items.currentKeyZone = sampleList sampleList.moveCurrentIndexRight() } if(event.key === Qt.Key_Space) { var imageId = Activity.uniqueId[sampleList.currentIndex] // At most (current level + 2) wagons are allowed in answer row at a time. if(listModel.count < Activity.dataset["WagonsInCorrectAnswers"][bar.level - 1] + 2) { activity.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/smudge.wav') Activity.addWagon(imageId, listModel.count); } } if((event.key === Qt.Key_Enter || event.key === Qt.Key_Return) && listModel.count > 0) { items.currentKeyZone = sampleList Activity.isAnswer(); } } Keys.enabled: true focus: true keyNavigationWraps: true highlightRangeMode: GridView.ApplyRange highlight: Rectangle { width: (background.width > background.height) ? background.width / 5.66 : background.width / 4.2 height: background.width > background.height ? sampleList.cellHeight : sampleList.cellHeight / 1.65 color: "#AA41AAC4" opacity: 0.8 radius: 5 visible: items.currentKeyZone === sampleList && items.keyNavigationMode x: (sampleList.currentIndex >= 0) ? sampleList.currentItem.x : 0 y: (sampleList.currentIndex >= 0) ? sampleList.currentItem.y : 0 Behavior on x { SpringAnimation { spring: 3 damping: 0.2 } } Behavior on y { SpringAnimation { spring: 3 damping: 0.2 } } } highlightFollowsCurrentItem: false } // Lower level wagons shelves Repeater { id: railSupporter model: sampleList.rowCount Rectangle { x: 0 y: sampleList.y + (sampleList.cellHeight * (index + 1)) - (sampleList.cellHeight - items.sampleImageHeight) z: 1 width: background.width height: (background.width > background.height) ? 6 : 3 border.color: "#808180" color: "transparent" border.width: 4 } } // Answer Submission button. BarButton { id: okButton source: "qrc:/gcompris/src/core/resource/bar_ok.svg" width: height height: score.height sourceSize.width: width sourceSize.height: height anchors.top: score.top z: score.z anchors { right: score.left rightMargin: 10 } ParticleSystemStarLoader { id: okButtonParticles clip: false } MouseArea { id: okButtonMouseArea anchors.fill: parent onClicked: { if((!timer.running && !animateFlow.running) && listModel.count > 0) { Activity.isAnswer() } } } } DialogHelp { id: dialogHelp onClose: home() } Score { id: score fontSize: background.width > background.height ? internalTextComponent.smallSize : internalTextComponent.tinySize anchors.top: parent.top anchors.topMargin: 10 * ApplicationInfo.ratio anchors.right: parent.right anchors.leftMargin: 10 * ApplicationInfo.ratio anchors.bottom: undefined anchors.left: undefined } Bar { id: bar content: BarEnumContent { value: help | home | level | hint } onHelpClicked: { displayDialog(dialogHelp) } onPreviousLevelClicked: Activity.previousLevel() onNextLevelClicked: Activity.nextLevel() onHomeClicked: activity.home() onHintClicked: { if(!introMessage.visible && items.mouseEnabled) { if(items.memoryMode == false) { timer.stop() animateFlow.stop(); listModel.clear(); for(var index = 0; index < Activity.backupListModel.length; index++) { Activity.addWagon(Activity.backupListModel[index], index); } items.memoryMode = true; } else { Activity.restoreLevel(); } } } } Bonus { id: bonus Component.onCompleted: win.connect(Activity.nextSubLevel) } } } diff --git a/src/activities/railroad/railroad.js b/src/activities/railroad/railroad.js index 77a9373e3..640724d2e 100644 --- a/src/activities/railroad/railroad.js +++ b/src/activities/railroad/railroad.js @@ -1,201 +1,202 @@ /* GCompris - railroad.js * * Copyright (C) 2016 Utkarsh Tiwari * Copyright (C) 2018 Amit Sagtani * * Authors: * (GTK+ version) * Utkarsh Tiwari (Qt Quick port) * Amit Sagtani (Qt Quick port) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ .pragma library .import QtQuick 2.6 as Quick .import GCompris 1.0 as GCompris var currentLevel = 0 var numberOfLevel = 10 var solutionArray = [] var backupListModel = [] var isNewLevel = true var resourceURL = "qrc:/gcompris/src/activities/railroad/resource/" var items var uniqueId = [] /** * Stores configuration for each level. * 'WagonsInCorrectAnswers' contains no. of wagons in correct answer. * 'memoryTime' contains time(in seconds) for memorizing the wagons. * 'numberOfSubLevels' contains no. of sublevels in each level. * 'columnsInHorizontalMode' contains no. of columns in a row of sampleZone in horizontal mode. * 'columnsInVerticalMode' contains no. of columns in a row of sampleZone in vertical mode. * 'noOfLocos' stores no. of locos to be displayed in sampleZone. * 'noOfWagons' stores no. of wagons to be displayed in sampleZone. */ var dataset = { "WagonsInCorrectAnswers": [1, 1, 2, 2, 3, 3, 4, 4, 5, 5], "memoryTime": [4, 4, 6, 6, 7, 7, 8, 8, 10, 10], "numberOfSubLevels": 3, "columnsInHorizontalMode": [3, 5, 3, 5, 3, 5, 3, 5, 3, 5], "columsInVerticalMode": [3, 4, 3, 4, 3, 4, 3, 4, 3, 4], "noOfLocos": [4, 9, 4, 9, 4, 9, 4, 9, 4, 9], "noOfWagons": [8, 11, 8, 11, 8, 11, 8, 11, 8, 11] } function start(items_) { items = items_ currentLevel = 0 items.score.numberOfSubLevels = dataset["numberOfSubLevels"]; items.score.currentSubLevel = 1; initLevel() } function stop() { } function initLevel() { generateUniqueId(); var uniqueId; items.mouseEnabled = true; items.memoryMode = false; items.timer.stop(); items.animateFlow.stop(); // Stops any previous animations items.listModel.clear(); items.answerZone.currentIndex = 0; items.sampleList.currentIndex = 0; items.answerZone.selectedSwapIndex = -1; if(isNewLevel) { // Initiates a new level backupListModel = []; solutionArray = []; //Adds wagons to display in answerZone. for(var i = 0; i < dataset["WagonsInCorrectAnswers"][currentLevel] - 1; i++) { do { uniqueId = "wagon" + Math.floor(Math.random() * dataset["noOfWagons"][currentLevel]) } while (solutionArray.indexOf(uniqueId) != -1) solutionArray.push(uniqueId); addWagon(uniqueId, i); } // Adds a loco at the beginning. uniqueId = "loco" + Math.floor(Math.random() * dataset["noOfLocos"][currentLevel]) solutionArray.push(uniqueId); addWagon(uniqueId, items.listModel.length); + solutionArray.reverse(); } else { // Re-setup the same level for(var i = 0; i < solutionArray.length; i++) { addWagon(solutionArray[i], i); } } if(items.introMessage.visible === false && isNewLevel) { items.timer.start() } items.bar.level = currentLevel + 1; items.timer.interval = dataset["memoryTime"][currentLevel] * 1000 } function nextLevel() { if(numberOfLevel <= ++currentLevel) { currentLevel = 0 } items.score.currentSubLevel = 1; isNewLevel = true; initLevel(); } function previousLevel() { if(--currentLevel < 0) { currentLevel = numberOfLevel - 1 } items.score.currentSubLevel = 1; isNewLevel = true; initLevel(); } function restoreLevel() { backupListModel = []; for (var index = 0; index < items.listModel.count; index++) { backupListModel.push(items.listModel.get(index).id); } isNewLevel = false; initLevel(); } function nextSubLevel() { /* Sets up the next sublevel */ items.score.currentSubLevel ++; if(items.score.currentSubLevel > dataset["numberOfSubLevels"]) { nextLevel(); } else { isNewLevel = true; initLevel(); } } function isAnswer() { /* Checks if the top level setup equals the solutions */ if(items.listModel.count === solutionArray.length) { var isSolution = true; for (var index = 0; index < items.listModel.count; index++) { if(items.listModel.get(index).id !== solutionArray[index]) { isSolution = false; break; } } if(isSolution == true) { items.mouseEnabled = false; // Disables the touch items.bonus.good("flower"); } else { items.bonus.bad("flower"); } } else { items.bonus.bad("flower"); } } function addWagon(uniqueID, dropIndex) { /* Appends wagons to the display area */ items.listModel.insert(dropIndex, {"id": uniqueID}); } function getDropIndex(x) { var count = items.listModel.count; for (var index = 0; index < count; index++) { var xVal = items.answerZone.cellWidth * index var itemWidth = items.answerZone.cellWidth if(x < xVal && index == 0) { return 0; } else if((xVal + itemWidth + items.background.width * 0.0025) <= x && index == (count - 1)) { return count; } else if(xVal <= x && x < (xVal + itemWidth + items.background.width * 0.0025)) { return index + 1; } } return 0; } function generateUniqueId() { uniqueId = []; var index; for(index = 0; index < dataset["noOfLocos"][currentLevel]; index++) { uniqueId.push("loco" + index) } for(index = 0; index < dataset["noOfWagons"][currentLevel]; index++) { uniqueId.push("wagon" + index) } }