diff --git a/src/activities/balancebox/Balancebox.qml b/src/activities/balancebox/Balancebox.qml index d08cce393..be6fe9a5e 100644 --- a/src/activities/balancebox/Balancebox.qml +++ b/src/activities/balancebox/Balancebox.qml @@ -1,573 +1,574 @@ /* GCompris - balance.qml * * Copyright (C) 2014-2015 Holger Kaelberer * * Authors: * Holger Kaelberer * * 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.1 import QtQuick.Window 2.1 import QtSensors 5.0 import QtGraphicalEffects 1.0 import GCompris 1.0 import Box2D 2.0 import QtQuick.Controls 1.0 import "../../core" import "editor/" import "balancebox.js" as Activity import "qrc:/gcompris/src/core/core.js" as Core ActivityBase { id: activity property string mode: "play" // "play" or "test" property string levelSet: "builtin" // "builtin" or "user" property var testLevel property bool inForeground: false // to avoid unneeded reconfigurations property bool alwaysStart: true // enforce start signal for editor-to-testing- and returning from config-transition property bool needRestart: true onWidthChanged: if (inForeground) { Activity.reconfigureScene(); Activity.initLevel(); } onHeightChanged: if (inForeground) { Activity.reconfigureScene(); Activity.initLevel(); } onStart: { inForeground = true; focus = true; } onStop: inForeground = false; Keys.onPressed: Activity.processKeyPress(event.key) Keys.onReleased: Activity.processKeyRelease(event.key) pageComponent: Image { id: background source: Activity.baseUrl + "/maze_bg.svg" sourceSize.width: parent.width anchors.fill: parent signal start signal stop function startEditor() { editorLoader.active = true; if (activity.mode == "test") displayDialogs([dialogActivityConfig, editorLoader.item]); else displayDialog(editorLoader.item); } function handleBackEvent() { if (activity.mode == "test") { startEditor(); return true; } else return false; } Keys.onEscapePressed: event.accepted = handleBackEvent(); Keys.onReleased: { if (event.key === Qt.Key_Back) event.accepted = handleBackEvent(); } Component.onCompleted: { dialogActivityConfig.getInitialConfiguration() activity.start.connect(start) activity.stop.connect(stop) items.dpi = Math.round(Screen.pixelDensity*25.4); } onStart: if (activity.needRestart) { Activity.start(items); activity.needRestart = false; } else Activity.initLevel(); onStop: { Activity.stop(); activity.needRestart = true; } QtObject { id: items property string mode: activity.mode property string levelSet: activity.levelSet property var testLevel: activity.testLevel property Item main: activity.main property alias background: background property alias bar: bar property alias bonus: bonus property alias tilt: tilt property alias timer: timer property alias ball: ball property int ballSize: cellSize - 2*wallSize property alias mapWrapper: mapWrapper property int cellSize: mapWrapper.length / Math.min(mapWrapper.rows, mapWrapper.columns) property int wallSize: cellSize / 5 property var world: physicsWorld property alias keyboardTimer: keyboardTimer property var ballType: Fixture.Category1 property var wallType: Fixture.Category2 property var holeType: Fixture.Category3 property var goalType: Fixture.Category4 property var buttonType: Fixture.Category5 property alias parser: parser property double dpi property GCAudio audioEffects: activity.audioEffects + property Loading loading: activity.loading } Loader { id: editorLoader active: false sourceComponent: BalanceboxEditor { id: editor visible: true testBox: activity onClose: activity.home() } } JsonParser { id: parser onError: console.error("Balancebox: Error parsing JSON: " + msg); } Rectangle { id: mapWrapper property double margin: 20 property int columns: 0 property int rows: 0 property double length: Math.min(background.height - 2*mapWrapper.margin, background.width - 2*mapWrapper.margin); color: "#E3DEDB" width: length height: length anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter transform: [ Rotation { origin.x: mapWrapper.width / 2 origin.y: mapWrapper.height / 2 axis { x: 1; y: 0; z: 0 } angle: ApplicationInfo.isMobile ? 0 : -items.tilt.xRotation }, Rotation { origin.x: mapWrapper.width / 2 origin.y: mapWrapper.height / 2 axis { x: 0; y: 1; z: 0 } angle: ApplicationInfo.isMobile ? 0 : items.tilt.yRotation } ] // right: Wall { id: rightWall width: items.wallSize height: parent.height + items.wallSize anchors.left: mapWrapper.right anchors.leftMargin: - items.wallSize/2 anchors.top: parent.top anchors.topMargin: -items.wallSize/2 shadow: false shadowHorizontalOffset: Math.min(items.tilt.yRotation, items.wallSize) shadowVerticalOffset: Math.min(items.tilt.xRotation, items.wallSize) } // bottom: Wall { id: bottomWall width: parent.width + items.wallSize height: items.wallSize anchors.left: mapWrapper.left anchors.leftMargin: - items.wallSize/2 anchors.top: parent.bottom anchors.topMargin: -items.wallSize/2 shadow: false shadowHorizontalOffset: Math.min(items.tilt.yRotation, items.wallSize) shadowVerticalOffset: Math.min(items.tilt.xRotation, items.wallSize) } // top: Wall { id: topWall width: parent.width + items.wallSize height: items.wallSize anchors.left: mapWrapper.left anchors.leftMargin: - items.wallSize/2 anchors.top: parent.top anchors.topMargin: -items.wallSize/2 shadow: false shadowHorizontalOffset: Math.min(items.tilt.yRotation, items.wallSize) shadowVerticalOffset: Math.min(items.tilt.xRotation, items.wallSize) } // left: Wall { id: leftWall width: items.wallSize height: parent.height + items.wallSize anchors.left: mapWrapper.left anchors.leftMargin: - items.wallSize/2 anchors.top: parent.top anchors.topMargin: -items.wallSize/2 shadow: false shadowHorizontalOffset: Math.min(items.tilt.yRotation, items.wallSize) shadowVerticalOffset: Math.min(items.tilt.xRotation, items.wallSize) } BalanceItem { id: ball world: physicsWorld imageSource: Activity.baseUrl + "/ball.svg" visible: false scale: 1.0 width: items.ballSize height: items.ballSize z: 1 categories: items.ballType collidesWith: items.wallType | items.holeType | items.goalType | items.buttonType density: 1 friction: Activity.friction linearDamping: Activity.friction restitution: Activity.restitution bodyType: Body.Dynamic shadow: true shadowHorizontalOffset: (items.tilt.yRotation > 0) ? Math.min(items.tilt.yRotation, items.wallSize) : Math.max(items.tilt.yRotation, -items.wallSize) shadowVerticalOffset: (items.tilt.xRotation > 0) ? Math.min(items.tilt.xRotation, items.wallSize) : Math.max(items.tilt.xRotation, -items.wallSize) Behavior on scale { NumberAnimation { id: fallAnimation duration: 1000 } } onBeginContact: { if (other.categories !== items.wallType) Activity.addBallContact(other); else { // sound-effect on each contact with a wall might be too annoying: //items.audioEffects.stop(); //items.audioEffects.play("qrc:/gcompris/src/core/resource/sounds/brick.wav"); } } onEndContact: { if (other.categories !== items.wallType) Activity.removeBallContact(other); } } World { id: physicsWorld gravity: Qt.point(0, 0) // we calculate acceleration ourselves pixelsPerMeter: Activity.box2dPpm // default: 32 timeStep: Activity.step/1000 // default: 1/60 } DebugDraw { id: debugDraw world: physicsWorld visible: Activity.debugDraw z: 1 } } Timer { id: timer interval: Activity.step; running: false; repeat: true onTriggered: Activity.moveBall() } Item { id: tilt property double xRotation: 0 property double yRotation: 0 property bool swapAxes: false property bool invertX: false property bool invertY: false onXRotationChanged: { if (xRotation > 90) xRotation = 90; else if (xRotation < -90) xRotation = -90; } onYRotationChanged: { if (yRotation > 90) yRotation = 90; else if (yRotation < -90) yRotation = -90; } TiltSensor { id: tiltSensor active: ApplicationInfo.isMobile ? true : false onReadingChanged: { if (!tilt.swapAxes) { tilt.xRotation = tilt.invertX ? -reading.xRotation : reading.xRotation; tilt.yRotation = tilt.invertY ? -reading.yRotation : reading.yRotation; } else { tilt.xRotation = tilt.invertX ? -reading.yRotation : reading.yRotation; tilt.yRotation = tilt.invertY ? -reading.xRotation : reading.xRotation; } tiltText.text = "X/Y Rotation: " + tiltSensor.reading.xRotation + "/" + tiltSensor.reading.yRotation } } } Item { id: textWrapper anchors.left: parent.left anchors.top: parent.top width: parent.width height: parent.height / 3 visible: Activity.debugDraw Text { id: tiltText anchors.left: parent.left anchors.top: parent.top text: "X/Y Rotation: " + tilt.xRotation + "/" + tilt.yRotation font.pointSize: 12 } Text { id: posText anchors.left: parent.left anchors.top: tiltText.bottom text: "X/Y = " + ball.x + "/" + ball.y font.pointSize: 12 } } MultiPointTouchArea { anchors.fill: parent touchPoints: [ TouchPoint { id: point1 } ] property real startX property real startY property int offset: 30 function reset() { startX = point1.x startY = point1.y } onPressed: { reset() } onUpdated: { var moveX = point1.x - startX var moveY = point1.y - startY // Find the direction with the most move if(Math.abs(moveX) * ApplicationInfo.ratio > offset && Math.abs(moveX) > Math.abs(moveY)) { if(moveX > offset * ApplicationInfo.ratio) { Activity.processKeyPress(Qt.Key_Right) reset() } else if(moveX < -offset * ApplicationInfo.ratio) { Activity.processKeyPress(Qt.Key_Left) reset() } } else if(Math.abs(moveY) * ApplicationInfo.ratio > offset && Math.abs(moveX) < Math.abs(moveY)) { if(moveY > offset * ApplicationInfo.ratio) { Activity.processKeyPress(Qt.Key_Down) reset() } else if(moveY < -offset * ApplicationInfo.ratio) { Activity.processKeyPress(Qt.Key_Up) reset() } } } onReleased: { Activity.keyboardIsTilting = false } } DialogHelp { id: dialogHelp onClose: home() } Bar { id: bar content: BarEnumContent { value: activity.mode == "play" ? (help | home | level | config ) : ( help | home ) } onHelpClicked: { // stop everything or the ball keeps moving while we're away: items.timer.stop(); displayDialog(dialogHelp); } onPreviousLevelClicked: Activity.previousLevel() onNextLevelClicked: Activity.nextLevel() onHomeClicked: { if (activity.mode == "test") background.startEditor(); else activity.home() } onConfigClicked: { items.timer.stop(); dialogActivityConfig.active = true // Set default values dialogActivityConfig.setDefaultValues(); displayDialog(dialogActivityConfig) } } Bonus { id: bonus looseSound: "qrc:/gcompris/src/core/resource/sounds/crash.wav" Component.onCompleted: { win.connect(Activity.nextLevel); loose.connect(Activity.initLevel); } } Timer { id: keyboardTimer interval: Activity.keyboardTimeStep; running: false repeat: false onTriggered: Activity.keyboardHandler() } DialogActivityConfig { id: dialogActivityConfig currentActivity: activity content: Component { Item { property alias levelsBox: levelsBox property var availableLevels: [ { "text": qsTr("Built-in"), "value": "builtin" }, { "text": qsTr("User"), "value": "user" }, ] Flow { id: flow spacing: 5 width: dialogActivityConfig.width GCComboBox { id: levelsBox model: availableLevels background: dialogActivityConfig label: qsTr("Select your level set") } Button { id: editorButton style: GCButtonStyle {} height: levelsBox.height text: qsTr("Start Editor") visible: levelsBox.currentIndex == 1 onClicked: background.startEditor() } } } } onClose: home(); onLoadData: { if(dataToSave && dataToSave["levels"]) { activity.levelSet = dataToSave["levels"]; } } onSaveData: { var newLevels = dialogActivityConfig.configItem .availableLevels[dialogActivityConfig.configItem.levelsBox.currentIndex].value; if (newLevels !== activity.levelSet) { activity.levelSet = newLevels; dataToSave = {"levels": activity.levelSet}; activity.needRestart = true; } } dataValidationFunc: function() { var newLevels = dialogActivityConfig.configItem .availableLevels[dialogActivityConfig.configItem.levelsBox.currentIndex].value; if (newLevels === "user" && !parser.jsonFile.exists(Activity.userFile)) { Core.showMessageDialog(dialogActivityConfig, qsTr("You selected the user-defined level set, but you have not yet defined any user levels!
" + "Either create your user levels by starting the level editor or choose the 'built-in' level set."), qsTr("Ok"), null, "", null, null); return false; } return true; } function setDefaultValues() { for(var i = 0 ; i < dialogActivityConfig.configItem.availableLevels.length; i ++) { if(dialogActivityConfig.configItem.availableLevels[i].value === activity.levelSet) { dialogActivityConfig.configItem.levelsBox.currentIndex = i; break; } } } } } } diff --git a/src/activities/balancebox/balancebox.js b/src/activities/balancebox/balancebox.js index 131e22197..5cd170391 100644 --- a/src/activities/balancebox/balancebox.js +++ b/src/activities/balancebox/balancebox.js @@ -1,457 +1,511 @@ /* GCompris - balancebox.js * * Copyright (C) 2014-2015 Holger Kaelberer * * Authors: * Holger Kaelberer * * 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 . */ /* ToDo: - make sensitivity configurable - add rectangular fixture for goal - editor: add 'clear' button - editor: allow going back: level 1 -> last level - add sound effects - add new item: unordered contact, that has to be collected but in an arbitrary order */ .pragma library .import QtQuick 2.0 as Quick .import GCompris 1.0 as GCompris .import Box2D 2.0 as Box2D +.import "qrc:/gcompris/src/core/core.js" as Core +.import QtQml 2.2 as Qml Qt.include("balancebox_common.js") var dataset = null; // Parameters that control the ball's dynamics var m = 0.2; // without ppm-correction: 10 var g = 9.81; // without ppm-correction: 50.8 var box2dPpm = 32; // pixelsPerMeter used in Box2D's world var boardSizeM = 0.9; // board's real edge length, fixed to 90 cm var boardSizePix = 500; // board's current size in pix (acquired dynamically) var dpiBase=139; var boardSizeBase = 760; var curDpi = null; var pixelsPerMeter = null; var vFactor = pixelsPerMeter / box2dPpm; // FIXME: calculate! var step = 20; // time step (in ms) var friction = 0.15; var restitution = 0.3; // rebounce factor // stuff for keyboard based tilting var keyboardTiltStep = 0.5; // degrees var keyboardTimeStep = 20; // ms var lastKey; var keyboardIsTilting = false; // tilting or resetting to horizontal var debugDraw = false; var currentLevel = 0; var numberOfLevel = 0; var items; var level; var map; // current map var goal = null; var holes = new Array(); var walls = new Array(); var contacts = new Array(); var ballContacts = new Array(); var goalUnlocked; var lastContact; var ballContacts; var wallComponent = Qt.createComponent("qrc:/gcompris/src/activities/balancebox/Wall.qml"); var contactComponent = Qt.createComponent("qrc:/gcompris/src/activities/balancebox/BalanceContact.qml"); var balanceItemComponent = Qt.createComponent("qrc:/gcompris/src/activities/balancebox/BalanceItem.qml"); var contactIndex = -1; +var pendingObjects = 0; function start(items_) { items = items_; currentLevel = 0; reconfigureScene(); if (items.mode === "play") { if (GCompris.ApplicationInfo.isMobile) { // lock screen orientation GCompris.ApplicationInfo.setRequestedOrientation(0); if (GCompris.ApplicationInfo.getNativeOrientation() === Qt.PortraitOrientation) { /* * Adjust tilting if native orientation != landscape. * * Note: As of Qt 5.4.1 QTiltSensor as well as QRotationSensor * report on Android * isFeatureSupported(AxesOrientation) == false. * Therefore we honour rotation manually. */ items.tilt.swapAxes = true; items.tilt.invertX = true; } } var levelsFile = builtinFile; if (items.levelSet === "user") levelsFile = userFile; dataset = items.parser.parseFromUrl(levelsFile, validateLevels); if (dataset == null) { console.error("Balancebox: Error loading levels from " + levelsFile + ", can't continue!"); return; } } else { // testmode: dataset = [items.testLevel]; } numberOfLevel = dataset.length; initLevel(); } function reconfigureScene() { if (items === undefined || items.mapWrapper === undefined) return; // set up dynamic variables for movement: pixelsPerMeter = (items.mapWrapper.length / boardSizeBase) * boardSizePix / boardSizeM; vFactor = pixelsPerMeter / box2dPpm; console.log("Starting: mode=" + items.mode + " pixelsPerM=" + items.world.pixelsPerMeter + " timeStep=" + items.world.timeStep + " posIterations=" + items.world.positionIterations + " velIterations=" + items.world.velocityIterations + " boardSizePix" + boardSizePix + " (real " + items.mapWrapper.length + ")" + " pixelsPerMeter=" + pixelsPerMeter + " vFactor=" + vFactor + " dpi=" + items.dpi + " nativeOrientation=" + GCompris.ApplicationInfo.getNativeOrientation()); } function sinDeg(num) { return Math.sin(num/180*Math.PI); } function moveBall() { var dt = step / 1000; var dvx = ((m*g*dt) * sinDeg(items.tilt.yRotation)) / m; var dvy = ((m*g*dt) * sinDeg(items.tilt.xRotation)) / m; /* console.log("moving ball: dv: " + items.ball.body.linearVelocity.x + "/" + items.ball.body.linearVelocity.y + " -> " + (items.ball.body.linearVelocity.x+dvx) + "/" + (items.ball.body.linearVelocity.y+dvy)); */ items.ball.body.linearVelocity.x += dvx * vFactor; items.ball.body.linearVelocity.y += dvy * vFactor; checkBallContacts(); } function checkBallContacts() { for (var k = 0; k < ballContacts.length; k++) { if (items.ball.x > ballContacts[k].x - items.ballSize/2 && items.ball.x < ballContacts[k].x + items.ballSize/2 && items.ball.y > ballContacts[k].y - items.ballSize/2 && items.ball.y < ballContacts[k].y + items.ballSize/2) { // collision if (ballContacts[k].categories == items.holeType) finishBall(false, ballContacts[k].x, ballContacts[k].y); else if (ballContacts[k].categories == items.goalType && goalUnlocked) finishBall(true, ballContacts[k].x, ballContacts[k].y); else if (ballContacts[k].categories == items.buttonType) { if (!ballContacts[k].pressed && ballContacts[k].orderNum == lastContact + 1) { ballContacts[k].pressed = true; lastContact = ballContacts[k].orderNum; if (lastContact == contacts.length) { items.audioEffects.play("qrc:/gcompris/src/core/resource/sounds/win.wav"); goalUnlocked = true; goal.imageSource = baseUrl + "/door.svg"; } else items.audioEffects.play("qrc:/gcompris/src/core/resource/sounds/scroll.wav"); // bleep } } } } } function finishBall(won, x, y) { items.timer.stop(); items.keyboardTimer.stop(); items.ball.x = x; items.ball.y = y; items.ball.scale = 0.4; items.ball.body.linearVelocity = Qt.point(0, 0); if (won) items.bonus.good("flower"); else items.bonus.bad("flower"); } function stop() { // reset everything tearDown(); // unlock screen orientation if (GCompris.ApplicationInfo.isMobile) GCompris.ApplicationInfo.setRequestedOrientation(-1); } function createObject(component, properties) { var p = properties; p.world = items.world; var object = component.createObject(items.mapWrapper, p); return object; } +var incubators; // need to reference all returned incubators in global scope + // or things don't work +function incubateObject(targetArr, component, properties) +{ + var p = properties; + p.world = items.world; + var incubator = component.incubateObject(items.mapWrapper, p); + if (incubator === null) { + console.error("Error during object incubation!"); + items.loading.stop(); + return; + } + incubators.push(incubator); + if (incubator.status === Qml.Component.Ready) + targetAttr.push(incubator.object); + else if (incubator.status === Qml.Component.Loading) { + pendingObjects++; + incubator.onStatusChanged = function(status) { + if (status === Qml.Component.Ready) + targetArr.push(incubator.object); + else + console.error("Error during object creation!"); + if (--pendingObjects === 0) { + // initMap completed + items.timer.start(); + items.loading.stop(); + } + } + } else + console.error("Error during object creation!"); +} + function initMap() { var modelMap = new Array(); + incubators = new Array(); goalUnlocked = true; items.mapWrapper.rows = map.length; items.mapWrapper.columns = map[0].length; + pendingObjects = 0; for (var row = 0; row < map.length; row++) { for (var col = 0; col < map[row].length; col++) { var x = col * items.cellSize; var y = row * items.cellSize; var orderNum = (map[row][col] & 0xFF00) >> 8; // debugging: if (debugDraw) { try { var rect = Qt.createQmlObject( - "import QtQuick 2.0;Rectangle{" - +"width:" + items.cellSize +";" - +"height:" + items.cellSize+";" - +"x:" + x + ";" - +"y:" + y +";" - +"color: \"transparent\";" - +"border.color: \"blue\";" - +"border.width: 1;" - +"}", items.mapWrapper); + "import QtQuick 2.0;Rectangle{" + +"width:" + items.cellSize +";" + +"height:" + items.cellSize+";" + +"x:" + x + ";" + +"y:" + y +";" + +"color: \"transparent\";" + +"border.color: \"blue\";" + +"border.width: 1;" + +"}", items.mapWrapper); } catch (e) { console.error("Error creating object: " + e); } } if (map[row][col] & NORTH) { - walls.push(createObject(wallComponent, {x: x-items.wallSize/2, - y: y-items.wallSize/2, width: items.cellSize + items.wallSize, - height: items.wallSize, - shadow: false})); + incubateObject(walls, wallComponent, { + x: x-items.wallSize/2, + y: y-items.wallSize/2, + width: items.cellSize + items.wallSize, + height: items.wallSize, + shadow: false}); } if (map[row][col] & SOUTH) { - walls.push(createObject(wallComponent, {x: x-items.wallSize/2, - y: y+items.cellSize-items.wallSize/2, - width: items.cellSize+items.wallSize, height: items.wallSize, - shadow: false})); + incubateObject(walls, wallComponent, { + x: x-items.wallSize/2, + y: y+items.cellSize-items.wallSize/2, + width: items.cellSize+items.wallSize, + height: items.wallSize, + shadow: false}); } if (map[row][col] & EAST) { - walls.push(createObject(wallComponent, {x: x+items.cellSize-items.wallSize/2, - y: y-items.wallSize/2, width: items.wallSize, - height: items.cellSize+items.wallSize, shadow: false})); + incubateObject(walls, wallComponent, { + x: x+items.cellSize-items.wallSize/2, + y: y-items.wallSize/2, + width: items.wallSize, + height: items.cellSize+items.wallSize, + shadow: false}); } if (map[row][col] & WEST) { - walls.push(createObject(wallComponent, {x: x-items.wallSize/2, - y: y-items.wallSize/2, width: items.wallSize, - height: items.cellSize+items.wallSize, shadow: false})); + incubateObject(walls, wallComponent, { + x: x-items.wallSize/2, + y: y-items.wallSize/2, + width: items.wallSize, + height: items.cellSize+items.wallSize, + shadow: false}); } if (map[row][col] & START) { items.ball.x = col * items.cellSize + items.wallSize; items.ball.y = row * items.cellSize + items.wallSize; items.ball.visible = true; } if (map[row][col] & GOAL) { var goalX = col * items.cellSize + items.wallSize/2; var goalY = row * items.cellSize + items.wallSize/2; goal = createObject(balanceItemComponent, { - x: goalX, y: goalY, - width: items.cellSize - items.wallSize, - height: items.cellSize - items.wallSize, - imageSource: baseUrl + "/door_closed.svg", - categories: items.goalType, - sensor: true}); + x: goalX, + y: goalY, + width: items.cellSize - items.wallSize, + height: items.cellSize - items.wallSize, + imageSource: baseUrl + "/door_closed.svg", + categories: items.goalType, + sensor: true}); } if (map[row][col] & HOLE) { var holeX = col * items.cellSize + items.wallSize; var holeY = row * items.cellSize + items.wallSize; - holes.push(createObject(balanceItemComponent, { - x: holeX, y: holeY, - width: items.ballSize, height: items.ballSize, - imageSource: baseUrl + "/hole.svg", - density: 0, friction: 0, restitution: 0, - categories: items.holeType, - sensor: true})); + incubateObject(holes, balanceItemComponent, { + x: holeX, + y: holeY, + width: items.ballSize, + height: items.ballSize, + imageSource: baseUrl + "/hole.svg", + density: 0, + friction: 0, + restitution: 0, + categories: items.holeType, + sensor: true}); } if (orderNum > 0) { var contactX = col * items.cellSize + items.wallSize/2; var contactY = row * items.cellSize + items.wallSize/2; goalUnlocked = false; - contacts.push(createObject(contactComponent, { - x: contactX, y: contactY, - width: items.cellSize - items.wallSize, - height: items.cellSize - items.wallSize, - pressed: false, - density: 0, friction: 0, restitution: 0, - categories: items.buttonType, - sensor: true, - orderNum: orderNum, - text: level.targets[orderNum-1]})); + incubateObject(contacts, contactComponent, { + x: contactX, + y: contactY, + width: items.cellSize - items.wallSize, + height: items.cellSize - items.wallSize, + pressed: false, + density: 0, + friction: 0, + restitution: 0, + categories: items.buttonType, + sensor: true, + orderNum: orderNum, + text: level.targets[orderNum-1]}); } } } if (goalUnlocked && goal) // if we have no contacts at all goal.imageSource = baseUrl + "/door.svg"; - } function addBallContact(item) { if (ballContacts.indexOf(item) !== -1) return; ballContacts.push(item); } function removeBallContact(item) { var index = ballContacts.indexOf(item); if (index > -1) ballContacts.splice(index, 1); } function tearDown() { items.ball.body.linearVelocity = Qt.point(0, 0); items.ball.scale = 1; items.ball.visible = false; items.timer.stop(); items.keyboardTimer.stop(); if (holes.length > 0) { for (var i = 0; i< holes.length; i++) holes[i].destroy(); holes.length = 0; } if (walls.length > 0) { for (var i = 0; i< walls.length; i++) walls[i].destroy(); walls.length = 0; } if (contacts.length > 0) { for (var i = 0; i< contacts.length; i++) contacts[i].destroy(); contacts.length = 0; } lastContact = 0; if (goal) goal.destroy(); goal = null; items.tilt.xRotation = 0; items.tilt.yRotation = 0; ballContacts = new Array(); } function initLevel(testLevel) { + items.loading.start(); items.bar.level = currentLevel + 1; // reset everything tearDown(); level = dataset[currentLevel]; map = level.map initMap(); - items.timer.start(); } // keyboard tilting stuff: function keyboardHandler() { var MAX_TILT = 5 if (keyboardIsTilting) { if (lastKey == Qt.Key_Left && items.tilt.yRotation > -MAX_TILT) items.tilt.yRotation -= keyboardTiltStep; else if (lastKey == Qt.Key_Right && items.tilt.yRotation < MAX_TILT) items.tilt.yRotation += keyboardTiltStep; else if (lastKey == Qt.Key_Up && items.tilt.xRotation > -MAX_TILT) items.tilt.xRotation -= keyboardTiltStep; else if (lastKey == Qt.Key_Down && items.tilt.xRotation < MAX_TILT) items.tilt.xRotation += keyboardTiltStep; items.keyboardTimer.start(); } else {// is resetting // yRotation: if (items.tilt.yRotation < 0) items.tilt.yRotation = Math.min(items.tilt.yRotation + keyboardTiltStep, 0); else if (items.tilt.yRotation > 0) items.tilt.yRotation = Math.max(items.tilt.yRotation - keyboardTiltStep, 0); // xRotation: if (items.tilt.xRotation < 0) items.tilt.xRotation = Math.min(items.tilt.xRotation + keyboardTiltStep, 0); else if (items.tilt.xRotation > 0) items.tilt.xRotation = Math.max(items.tilt.xRotation - keyboardTiltStep, 0); // resetting done? if (items.tilt.yRotation != 0 || items.tilt.xRotation != 0) items.keyboardTimer.start(); } } function processKeyPress(key) { if (key == Qt.Key_Left || key == Qt.Key_Right || key == Qt.Key_Up || key == Qt.Key_Down) { lastKey = key; keyboardIsTilting = true; items.keyboardTimer.stop(); keyboardHandler(); } } function processKeyRelease(key) { if (key == Qt.Key_Left || key == Qt.Key_Right || key == Qt.Key_Up || key == Qt.Key_Down) { lastKey = key; keyboardIsTilting = false; items.keyboardTimer.stop(); keyboardHandler(); } } function nextLevel() { if(numberOfLevel <= ++currentLevel ) { currentLevel = 0 } initLevel(); } function previousLevel() { if(--currentLevel < 0) { currentLevel = numberOfLevel - 1 } initLevel(); } diff --git a/src/activities/balancebox/editor/BalanceboxEditor.qml b/src/activities/balancebox/editor/BalanceboxEditor.qml index 63957eb64..0992ab89d 100644 --- a/src/activities/balancebox/editor/BalanceboxEditor.qml +++ b/src/activities/balancebox/editor/BalanceboxEditor.qml @@ -1,652 +1,668 @@ /* GCompris - BalanceboxEditor.qml * * Copyright (C) 2015 Holger Kaelberer * * Authors: * Holger Kaelberer * * 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.1 import QtGraphicalEffects 1.0 import GCompris 1.0 import QtQuick.Controls 1.0 import "../../../core" import ".." import "balanceboxeditor.js" as Activity Item { id: editor property string filename: Activity.userFile property ActivityBase currentActivity property var testBox property bool isTesting: false // props needed for stackView integration: signal close signal start signal stop property bool isDialog: true property bool alwaysStart: true // enforce start signal for configDialog-to-editor-transition function handleBackEvent() { if (!isTesting) { if (Activity.levelChanged) Activity.warnUnsavedChanges(function() {stop(); home();}, function() {}); else { stop(); home(); } return true; } else return false; } Keys.onEscapePressed: event.accepted = handleBackEvent(); Keys.onReleased: { if (event.key === Qt.Key_Back) { event.accepted = handleBackEvent(); } else event.accepted = false; } onStart: { focus = true; if (!isTesting) Activity.initEditor(props); else stopTesting(); } onStop: testBox.focus = true; QtObject { id: props property int columns: 10 property int rows: 10 property int currentTool property alias editor: editor property alias mapModel: mapModel property alias mapWrapper: mapWrapper property int cellSize: mapWrapper.length / Math.min(mapWrapper.rows, mapWrapper.columns) property int wallSize: cellSize / 5 property int ballSize: cellSize - 2*wallSize property alias toolBox: toolBox property string contactValue: "1" property int lastOrderNum: 0 property alias file: file property alias parser: parser property alias bar: bar property int lastGoalIndex: -1 property int lastBallIndex: -1 + property alias editorWorker: editorWorker } function startTesting() { editor.isTesting = true; testBox.mode = "test"; testBox.testLevel = Activity.modelToLevel(); testBox.needRestart = true; back(testBox); } function stopTesting() { editor.isTesting = false; testBox.mode = "play"; testBox.testLevel = null; testBox.needRestart = true; } Rectangle { id: background anchors.fill: parent File { id: file onError: console.error("File error: " + msg); } JsonParser { id: parser onError: console.error("Balanceboxeditor: Error parsing JSON: " + msg); } Column { id: toolBox2 anchors.top: mapWrapper.top anchors.left: mapWrapper.right anchors.leftMargin: 10 * ApplicationInfo.ratio anchors.topMargin: 20 * ApplicationInfo.ratio spacing: 5 * ApplicationInfo.ratio width: (background.width - mapWrapper.width - props.wallSize - 20 * ApplicationInfo.ratio) / 2 height: parent.height // anchors.topMargin: 20 Button { id: saveButton width: parent.width height: props.cellSize style: GCButtonStyle {} text: "Save" onClicked: Activity.saveModel(); } Button { id: testButton width: parent.width height: props.cellSize style: GCButtonStyle {} text: "Test" onClicked: editor.startTesting(); } } Column { id: toolBox anchors.top: mapWrapper.top anchors.topMargin: 20 * ApplicationInfo.ratio anchors.left: parent.left anchors.leftMargin: 10 width: (mapWrapper.x - 20) spacing: 5 * ApplicationInfo.ratio Component.onCompleted: clearTool.selected = true; function setCurrentTool(item) { props.currentTool = item.type; if (clearTool !== item) clearTool.selected = false; if (hWallTool !== item) hWallTool.selected = false; if (vWallTool !== item) vWallTool.selected = false; if (holeTool !== item) holeTool.selected = false; if (ballTool !== item) ballTool.selected = false; if (contactTool !== item) contactTool.selected = false; if (goalTool !== item) goalTool.selected = false; } EditorTool { id: clearTool type: Activity.TOOL_CLEAR anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: props.cellSize - 2 onSelectedChanged: { if (selected) { toolBox.setCurrentTool(clearTool); } } Image { id: clear source: "qrc:/gcompris/src/core/resource/cancel.svg" width: props.cellSize - 4 height: props.cellSize - 4 anchors.centerIn: parent anchors.margins: 3 } } EditorTool { id: hWallTool type: Activity.TOOL_H_WALL anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: props.cellSize onSelectedChanged: { if (selected) { toolBox.setCurrentTool(hWallTool); } } Wall { id: hWall width: props.cellSize height: props.wallSize anchors.centerIn: parent anchors.margins: 3 } } EditorTool { id: vWallTool anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: props.cellSize type: Activity.TOOL_V_WALL onSelectedChanged: { if (selected) { toolBox.setCurrentTool(vWallTool); } } Wall { id: vWall width: props.wallSize height: props.cellSize - 4 anchors.centerIn: parent anchors.margins: 3 } } EditorTool { id: holeTool anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: props.cellSize type: Activity.TOOL_HOLE onSelectedChanged: { if (selected) { toolBox.setCurrentTool(holeTool); } } BalanceItem { id: hole width: props.cellSize - props.wallSize / 2 height: props.cellSize - props.wallSize / 2 anchors.centerIn: parent anchors.margins: props.wallSize / 2 visible: true imageSource: Activity.baseUrl + "/hole.svg" } } EditorTool { id: ballTool anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: props.cellSize type: Activity.TOOL_BALL onSelectedChanged: { if (selected) { toolBox.setCurrentTool(ballTool); } } BalanceItem { id: ball width: props.cellSize - props.wallSize / 2 height: parent.height - props.wallSize / 2 anchors.centerIn: parent anchors.margins: props.wallSize / 2 visible: true imageSource: Activity.baseUrl + "/ball.svg" } } EditorTool { id: goalTool anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: props.cellSize type: Activity.TOOL_GOAL onSelectedChanged: { if (selected) { toolBox.setCurrentTool(goalTool); } } BalanceItem { id: goal width: props.cellSize - props.wallSize height: props.cellSize - props.wallSize anchors.centerIn: parent anchors.margins: props.wallSize / 2 z: 1 imageSource: Activity.baseUrl + "/door.svg" } } EditorTool { id: contactTool anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: props.cellSize type: Activity.TOOL_CONTACT onSelectedChanged: { if (selected) { toolBox.setCurrentTool(contactTool); } } Row { id: contactToolRow spacing: 5 width: contact.width + contactTextInput.width + spacing anchors.centerIn: parent BalanceContact { id: contact width: props.cellSize - props.wallSize height: props.cellSize - props.wallSize anchors.margins: props.wallSize / 2 pressed: false orderNum: 99 text: props.contactValue z: 1 } SpinBox { id: contactTextInput width: contact.width * 2 height: contact.height value: props.contactValue maximumValue: 99 minimumValue: 1 decimals: 0 horizontalAlignment: Qt.AlignHCenter font.family: GCSingletonFontLoader.fontLoader.name font.pixelSize: height / 2 onValueChanged: if (value != props.contactValue) props.contactValue = value; } } } } + WorkerScript { + id: editorWorker + + source: "editor_worker.js" + onMessage: { + // worker finished, update all changed values (except the model): + props.contactValue = messageObject.maxContactValue; + props.lastBallIndex = messageObject.lastBallIndex; + props.lastGoalIndex = messageObject.lastGoalIndex; + props.lastOrderNum = messageObject.lastOrderNum; + Activity.targetList = messageObject.targetList; + testBox.loading.stop(); + } + } + ListModel { id: mapModel } Rectangle { id: mapWrapper property double margin: 20 property int columns: props.columns property int rows: props.rows property double length: Math.min(background.height - 2*mapWrapper.margin, background.width - 2*mapWrapper.margin); color: "#E3DEDB" width: length height: length anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter Grid { id: mapGrid columns: mapWrapper.columns rows: mapWrapper.rows anchors.fill: parent width: parent.width height: parent.height spacing: 0 Repeater { id: mapGridRepeater model: mapModel//mapGrid.columns * mapGrid.rows delegate: Item { // cell wrapper id: cell width: props.cellSize height: props.cellSize property bool highlighted: false Loader { id: northWallLoader active: value & Activity.NORTH width: props.cellSize + props.wallSize height: props.wallSize anchors.top: parent.top anchors.left: parent.left anchors.topMargin: -props.wallSize / 2 anchors.leftMargin: -props.wallSize / 2 sourceComponent: Wall { id: northWall shadow: false anchors.centerIn: parent z: 1 } } Loader { id: eastWallLoader active: value & Activity.EAST || (cell.highlighted && props.currentTool === Activity.TOOL_V_WALL) width: props.wallSize height: props.cellSize + props.wallSize anchors.bottom: parent.bottom anchors.right: parent.right anchors.bottomMargin: -props.wallSize / 2 anchors.rightMargin: -props.wallSize / 2 sourceComponent: Wall { id: eastWall anchors.centerIn: parent shadow: false z: 1 } } Loader { id: southWallLoader active: value & Activity.SOUTH || (cell.highlighted && props.currentTool === Activity.SOUTH) width: props.cellSize + props.wallSize height: props.wallSize anchors.bottom: parent.bottom anchors.left: parent.left anchors.bottomMargin: -props.wallSize / 2 anchors.leftMargin: -props.wallSize / 2 sourceComponent: Wall { id: southWall anchors.centerIn: parent shadow: false z: 1 } } Loader { id: westWallLoader active: value & Activity.WEST width: props.wallSize height: props.cellSize + props.wallSize anchors.bottom: parent.bottom anchors.left: parent.left anchors.bottomMargin: -props.wallSize / 2 anchors.leftMargin: -props.wallSize / 2 sourceComponent: Wall { id: westWall anchors.centerIn: parent shadow: false z: 1 } } Loader { id: doorLoader active: value & Activity.GOAL || (cell.highlighted && props.currentTool === Activity.TOOL_GOAL) anchors.centerIn: parent width: props.cellSize - props.wallSize height: props.cellSize - props.wallSize sourceComponent: BalanceItem { id: goal anchors.centerIn: parent z: 1 imageSource: Activity.baseUrl + "/door.svg" } } Loader { id: holeLoader active: value & Activity.HOLE || (cell.highlighted && props.currentTool === Activity.TOOL_HOLE) anchors.centerIn: parent sourceComponent: BalanceItem { id: hole width: props.ballSize height:props.ballSize anchors.centerIn: parent z: 1 imageSource: Activity.baseUrl + "/hole.svg" } } Loader { id: ballLoader active: value & Activity.START || (cell.highlighted && props.currentTool === Activity.TOOL_BALL) anchors.centerIn: parent sourceComponent: BalanceItem { id: ball width: props.ballSize height:props.ballSize anchors.centerIn: parent visible: true imageSource: Activity.baseUrl + "/ball.svg" z: 1 } } Loader { id: contactLoader active: (value & Activity.CONTACT) || (cell.highlighted && props.currentTool === Activity.TOOL_CONTACT) width: props.cellSize - props.wallSize height: props.cellSize - props.wallSize anchors.centerIn: parent sourceComponent: BalanceContact { id: contact anchors.centerIn: parent visible: true pressed: false text: contactValue z: 1 } } Rectangle { // bounding rect id: cellRect width: props.cellSize height: props.cellSize color: "transparent" border.width: 1 border.color: cell.highlighted ? "yellow": "lightgray" z: 10 MouseArea { id: cellMouse anchors.fill: parent hoverEnabled: ApplicationInfo.isMobile ? false : true onEntered: cell.highlighted = true onExited: cell.highlighted = false onClicked: { editor.focus = true; Activity.modifyMap(props, row, col); } } } } } } // right: Wall { id: rightWall width: props.wallSize height: parent.height + props.wallSize anchors.left: mapWrapper.right anchors.leftMargin: - props.wallSize/2 anchors.top: parent.top anchors.topMargin: -props.wallSize/2 shadow: false } // bottom: Wall { id: bottomWall width: parent.width + props.wallSize height: props.wallSize anchors.left: mapWrapper.left anchors.leftMargin: - props.wallSize/2 anchors.top: parent.bottom anchors.topMargin: -props.wallSize/2 shadow: false } // top: Wall { id: topWall width: parent.width + props.wallSize height: props.wallSize anchors.left: mapWrapper.left anchors.leftMargin: - props.wallSize/2 anchors.top: parent.top anchors.topMargin: -props.wallSize/2 shadow: false } // left: Wall { id: leftWall width: props.wallSize height: parent.height + props.wallSize anchors.left: mapWrapper.left anchors.leftMargin: - props.wallSize/2 anchors.top: parent.top anchors.topMargin: -props.wallSize/2 shadow: false } } } Bar { id: bar content: BarEnumContent { value: home | level } // FIXME: add dedicated editor help? onPreviousLevelClicked: { if (Activity.currentLevel > 0) { if (Activity.levelChanged) Activity.warnUnsavedChanges(Activity.previousLevel, function() {}); else Activity.previousLevel(); } } onNextLevelClicked: { if (Activity.levelChanged) Activity.warnUnsavedChanges(Activity.nextLevel, function() {}); else Activity.nextLevel(); } onHomeClicked: { if (Activity.levelChanged) Activity.warnUnsavedChanges(activity.home, function() {}); else activity.home() } } } diff --git a/src/activities/balancebox/editor/balanceboxeditor.js b/src/activities/balancebox/editor/balanceboxeditor.js index 8dde913b2..8120848de 100644 --- a/src/activities/balancebox/editor/balanceboxeditor.js +++ b/src/activities/balancebox/editor/balanceboxeditor.js @@ -1,354 +1,315 @@ /* GCompris - balanceboxeditor.js * * Copyright (C) 2015 Holger Kaelberer * * Authors: * Holger Kaelberer * * 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.0 as Quick .import GCompris 1.0 as GCompris .import "qrc:/gcompris/src/core/core.js" as Core Qt.include("../balancebox_common.js") var TOOL_CLEAR = EMPTY var TOOL_H_WALL = SOUTH var TOOL_V_WALL = EAST var TOOL_HOLE = HOLE var TOOL_CONTACT = CONTACT var TOOL_GOAL = GOAL var TOOL_BALL = START var levels; var level; var currentLevel; var numberOfLevel; var levelChanged = false; // whether current level has unsaved changes var props; var currentIsNewLevel; var targetList = []; function initEditor(_props) { props = _props; console.log("init editor"); currentLevel = 0; numberOfLevel = 0; props.lastBallIndex = -1; props.lastGoalIndex = -1; levels = []; if (props.file.exists(props.editor.filename)) { levels = props.parser.parseFromUrl(props.editor.filename, validateLevels); if (levels == null) { console.error("BalanceboxEditor: Error loading levels from " + props.editor.filename); levels = []; // restart with an empty level-set } } numberOfLevel = levels.length; initLevel(); } function createEmptyLevel() { var map = []; var num = currentLevel + 1; for (var row = 0; row < props.rows; row++) for (var col = 0; col < props.columns; col++) { if (col === 0) map[row] = []; map[row][col] = 0; } return { level: currentLevel + 1, map: map, targets: [] }; } function initLevel() { + props.editor.testBox.loading.start(); if (currentLevel >= numberOfLevel) { levels.push(createEmptyLevel()); levelChanged = false; numberOfLevel++; currentIsNewLevel = true; } else currentIsNewLevel = false; level = levels[currentLevel]; - props.bar.level = currentLevel + 1 - props.lastBallIndex = -1; - props.lastGoalIndex = -1; - props.lastOrderNum = 0; - var maxContactValue = 1; - - props.mapModel.clear(); - targetList = []; - for (var row = 0; row < level.map.length; row++) { - for (var col = 0; col < level.map[row].length; col++) { - var contactValue = ""; - var value = parseInt(level.map[row][col]); // always enforce number - var orderNum = (value & 0xFF00) >> 8; - if (orderNum > 0 && level.targets[orderNum - 1] === undefined) { - console.error("Invalid level: orderNum " + orderNum - + " without target value!"); - } else if (orderNum > 0) { - if (orderNum > props.lastOrderNum) - props.lastOrderNum = orderNum; - var target = level.targets[orderNum-1] - targetList.push(parseInt(target)); - contactValue = Number(target).toString(); - if (target >= maxContactValue) - maxContactValue = target + 1; - } - props.mapModel.append({ - "row": row, - "col": col, - "value": value, - "contactValue": (orderNum > 0) ? contactValue : "" - }); - if (value & GOAL) { - if (props.lastGoalIndex > -1) { - console.error("Invalid level: multiple goal locations: row/col=" - + row + "/" + col); - return; - } - props.lastGoalIndex = row * level.map.length + col; - } - if (value & START) { - if (props.lastBallIndex > -1) { - console.error("Invalid level: multiple start locations: row/col=" - + row + "/" + col); - return; - } - props.lastBallIndex = row * level.map.length + col; - } - } - } - props.contactValue = maxContactValue; + props.bar.level = currentLevel + 1; + // populate model in the worker-thread: + props.editorWorker.sendMessage({ + lastBallIndex: props.lastBallIndex, + lastGoalIndex: props.lastGoalIndex, + lastOrderNum: props.lastOrderNum, + mapModel: props.mapModel, + targetList: targetList, + level: level + }); } function dec2hex(i) { return (i+0x10000).toString(16).substr(-4).toUpperCase(); } function modelToLevel() { var map = new Array(); var targets = new Array(); targetList.sort(function(a,b) { return a - b;}) for (var i = 0; i < props.mapModel.count; i++) { var row = Math.floor(i / props.columns); var col = i % props.columns; if (col === 0) { map[row] = new Array(); } var obj = props.mapModel.get(i); var value = obj.value; value &= ~(0xff00); // always clear order-number bits if (obj.value & CONTACT) { value |= ((targetList.indexOf(parseInt(obj.contactValue)) + 1) << 8); } map[row][col] = "0x" + dec2hex(value); } var level = { level: currentLevel + 1, map: map, targets: targetList } return level; } function saveModel() { var l = modelToLevel(); var path = userFile.substring(0, userFile.lastIndexOf("/")); if (!props.file.exists(path)) { if (!props.file.mkpath(path)) console.error("Could not create directory " + path); else console.debug("Created directory " + path); } levels[currentLevel] = l; // renumber levels before saving: for(var i = 0; i < levels.length; i++) levels[i].level = i + 1; if (!props.file.write(JSON.stringify(levels), userFile)) Core.showMessageDialog(props.editor, qsTr("Error saving %1 levels to your levels file (%2)") .arg(numberOfLevel).arg(userFile), "", null, "", null, null); else { Core.showMessageDialog(props.editor, qsTr("Saved %1 levels to your levels file (%2)") .arg(numberOfLevel).arg(userFile), "", null, "", null, null); levelChanged = false; currentIsNewLevel = false; } } function modifyMap(props, row, col) { var modelIndex = row * level.map.length + col; var obj = props.mapModel.get(modelIndex); var oldValue = obj.value; var newValue = oldValue; // contact-tool: check for already existing value early if (props.currentTool === TOOL_CONTACT // have contact tool and ... && targetList.indexOf(parseInt(props.contactValue)) !== -1 // already have this contact value ... && !(obj.value & TOOL_CONTACT // which is not set at the same cell && obj.contactValue === props.contactValue)) { console.debug("Avoiding to set duplicate contact value " + props.contactValue + " current targets=" + JSON.stringify(targetList)); return; } if (props.currentTool === TOOL_CLEAR) { newValue = 0; // remove contact stuff: if (obj.value & TOOL_CONTACT) { if (targetList.indexOf(parseInt(obj.contactValue)) !== -1) targetList.splice(targetList.indexOf(parseInt(obj.contactValue)), 1); props.mapModel.setProperty(row * level.map.length + col, "contactValue", ""); } } else { // all other tools // special treatment for mutually exclusive ones: if (props.currentTool === TOOL_HOLE || props.currentTool === TOOL_GOAL || props.currentTool === TOOL_CONTACT || props.currentTool === TOOL_BALL) { // helper: var MUTEX_MASK = (START | GOAL | HOLE | CONTACT) ^ props.currentTool; newValue &= ~MUTEX_MASK; } // special treatment for singletons: if (props.currentTool === TOOL_GOAL) { if ((obj.value & TOOL_GOAL) === 0) { // setting a new one if (props.lastGoalIndex > -1) { // clear last one first: props.mapModel.setProperty(props.lastGoalIndex, "value", props.mapModel.get(props.lastGoalIndex).value & (~TOOL_GOAL)); } // now memorize the new one: props.lastGoalIndex = modelIndex; } } else if (props.currentTool === TOOL_BALL) { if ((obj.value & TOOL_BALL) === 0) { // setting a new one if (props.lastBallIndex > -1) // clear last one first: props.mapModel.setProperty(props.lastBallIndex, "value", props.mapModel.get(props.lastBallIndex).value & (~TOOL_BALL)); // now memorize the new one: props.lastBallIndex = modelIndex; } } // special treatment for contacts: if (props.currentTool === TOOL_CONTACT) { if (obj.value & TOOL_CONTACT && // have old contact value ... obj.contactValue === props.contactValue) { // ... which is == the new one // clear contact if (targetList.indexOf(parseInt(obj.contactValue)) !== -1) targetList.splice(targetList.indexOf(parseInt(obj.contactValue)), 1); props.mapModel.setProperty(row * level.map.length + col, "contactValue", ""); newValue &= ~(CONTACT); } else { if (obj.value & TOOL_CONTACT) { // have old contact that is different if (targetList.indexOf(parseInt(obj.contactValue)) !== -1) targetList.splice(targetList.indexOf(parseInt(obj.contactValue)), 1); // no change to newValue } // -> set new one: if (targetList.indexOf(parseInt(props.contactValue)) === -1) targetList.push(parseInt(props.contactValue)); props.mapModel.setProperty(row * level.map.length + col, "contactValue", props.contactValue); props.contactValue = Number(Number(props.contactValue) + 1).toString(); newValue |= CONTACT; } } else { // for other than contact-tool: update value by current tool bit: newValue ^= props.currentTool; } } if (oldValue !== newValue) levelChanged = true; props.mapModel.setProperty(modelIndex, "value", newValue); } var warningVisible = false; function warnUnsavedChanges(yesFunc, noFunc) { if (!warningVisible) { warningVisible = true; Core.showMessageDialog(props.editor, qsTr("You have unsaved changes!
" + "Really leave this level and lose changes?"), qsTr("Yes"), function() { warningVisible = false; if (yesFunc !== undefined) yesFunc(); }, qsTr("No"), function() { warningVisible = false; if (noFunc !== undefined) noFunc(); }, function() { warningVisible = false; if (noFunc !== undefined) noFunc(); }); } } function nextLevel() { if(numberOfLevel === currentLevel + 1 && !levelChanged && currentIsNewLevel ) { console.log("BalanceboxEditor: Current level is new and unchanged, nogo!"); return; } currentLevel++; levelChanged = false; initLevel(); } function previousLevel() { if (currentLevel === 0) return; currentLevel--; levelChanged = false; initLevel(); } diff --git a/src/activities/balancebox/editor/editor_worker.js b/src/activities/balancebox/editor/editor_worker.js new file mode 100644 index 000000000..c1ffd7a40 --- /dev/null +++ b/src/activities/balancebox/editor/editor_worker.js @@ -0,0 +1,77 @@ +/* GCompris - editor_worker.js + * + * Copyright (C) 2015 Holger Kaelberer + * + * Authors: + * Holger Kaelberer + * + * 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 . + */ + +Qt.include("../balancebox_common.js") + +WorkerScript.onMessage = function(msg) { + console.debug("XXX worker started"); + var maxContactValue = 1; + msg.lastBallIndex = -1; + msg.lastGoalIndex = -1; + msg.lastOrderNum = 0; + msg.mapModel.clear(); + msg.targetList = []; + for (var row = 0; row < msg.level.map.length; row++) { + for (var col = 0; col < msg.level.map[row].length; col++) { + var contactValue = ""; + var value = parseInt(msg.level.map[row][col]); // always enforce number + var orderNum = (value & 0xFF00) >> 8; + if (orderNum > 0 && msg.level.targets[orderNum - 1] === undefined) { + console.error("Invalid level: orderNum " + orderNum + + " without target value!"); + } else if (orderNum > 0) { + if (orderNum > msg.lastOrderNum) + msg.lastOrderNum = orderNum; + var target = msg.level.targets[orderNum-1] + msg.targetList.push(parseInt(target)); + contactValue = Number(target).toString(); + if (target >= maxContactValue) + maxContactValue = target + 1; + } + msg.mapModel.append({ + "row": row, + "col": col, + "value": value, + "contactValue": (orderNum > 0) ? contactValue : "" + }); + if (value & GOAL) { + if (msg.lastGoalIndex > -1) { + console.error("Invalid level: multiple goal locations: row/col=" + + row + "/" + col); + return; + } + msg.lastGoalIndex = row * msg.level.map.length + col; + } + if (value & START) { + if (msg.lastBallIndex > -1) { + console.error("Invalid level: multiple start locations: row/col=" + + row + "/" + col); + return; + } + msg.lastBallIndex = row * msg.level.map.length + col; + } + } + msg.mapModel.sync(); // sync per row + } + msg.maxContactValue = maxContactValue; + // send result back to the gui-thread + WorkerScript.sendMessage(msg); +}