File indexing completed on 2024-04-28 15:07:44
0001 /* GCompris - balancebox.js 0002 * 0003 * SPDX-FileCopyrightText: 2014-2016 Holger Kaelberer <holger.k@elberer.de> 0004 * 0005 * Authors: 0006 * Holger Kaelberer <holger.k@elberer.de> 0007 * 0008 * SPDX-License-Identifier: GPL-3.0-or-later 0009 */ 0010 0011 /* ToDo: 0012 - make sensitivity configurable 0013 - editor: add 'clear' button 0014 - editor: allow going back: level 1 -> last level 0015 - add new item: unordered contact, that has to be collected but in an 0016 arbitrary order 0017 */ 0018 .pragma library 0019 .import QtQuick 2.12 as Quick 0020 .import GCompris 1.0 as GCompris 0021 .import Box2D 2.0 as Box2D 0022 .import "qrc:/gcompris/src/core/core.js" as Core 0023 .import QtQml 2.12 as Qml 0024 0025 Qt.include("balancebox_common.js") 0026 0027 var dataset = null; 0028 0029 // Parameters that control the ball's dynamics 0030 var m = 0.2; // without ppm-correction: 10 0031 var g = 9.81; // without ppm-correction: 50.8 0032 var box2dPpm = 32; // pixelsPerMeter used in Box2D's world 0033 var boardSizeM = 0.9; // board's real edge length, fixed to 90 cm 0034 var boardSizePix = 500; // board's current size in pix (acquired dynamically) 0035 var dpiBase=139; 0036 var boardSizeBase = 760; 0037 var curDpi = null; 0038 var pixelsPerMeter = null; 0039 var vFactor = pixelsPerMeter / box2dPpm; // FIXME: calculate! 0040 0041 var step = 20; // time step (in ms) 0042 var friction = 0.15; 0043 var restitution = 0.3; // rebounce factor 0044 0045 // stuff for keyboard based tilting 0046 var keyboardTiltStep = 0.5; // degrees 0047 var keyboardTimeStep = 20; // ms 0048 var lastKey; 0049 var keyboardIsTilting = false; // tilting or resetting to horizontal 0050 0051 var debugDraw = false; 0052 var numberOfLevel = 0; 0053 var items; 0054 var level; 0055 var map; // current map 0056 0057 var goal = null; 0058 var holes = new Array(); 0059 var walls = new Array(); 0060 var contacts = new Array(); 0061 var ballContacts = new Array(); 0062 var goalUnlocked; 0063 var lastContact; 0064 var wallComponent = Qt.createComponent("qrc:/gcompris/src/activities/balancebox/Wall.qml"); 0065 var contactComponent = Qt.createComponent("qrc:/gcompris/src/activities/balancebox/BalanceContact.qml"); 0066 var balanceItemComponent = Qt.createComponent("qrc:/gcompris/src/activities/balancebox/BalanceItem.qml"); 0067 var goalComponent = Qt.createComponent("qrc:/gcompris/src/activities/balancebox/Goal.qml"); 0068 var contactIndex = -1; 0069 var pendingObjects = 0; 0070 var pendingReconfigure = false; 0071 var finishRunning = false; 0072 var createLevelsMsg = qsTr("Either create your levels by starting the level editor in the activity settings menu and then load your file, or choose the 'built-in' level set.") 0073 0074 function start(items_) { 0075 items = items_; 0076 0077 if (items.mode === "play") { 0078 if (GCompris.ApplicationInfo.isMobile) { 0079 // we don't have many touch events, therefore disable screensaver on android: 0080 GCompris.ApplicationInfo.setKeepScreenOn(true); 0081 // lock screen orientation to landscape: 0082 GCompris.ApplicationInfo.setRequestedOrientation(0); 0083 if (GCompris.ApplicationInfo.getNativeOrientation() === Qt.PortraitOrientation) { 0084 /* 0085 * Adjust tilting if native orientation != landscape. 0086 * 0087 * Note: As of Qt 5.4.1 QTiltSensor as well as QRotationSensor 0088 * report on Android 0089 * isFeatureSupported(AxesOrientation) == false. 0090 * Therefore we honour rotation manually. 0091 */ 0092 items.tilt.swapAxes = true; 0093 items.tilt.invertX = true; 0094 } 0095 } 0096 var levelsFile; 0097 if (items.levelSet === "user" && items.file.exists(items.filePath)) { 0098 levelsFile = items.filePath; 0099 items.currentLevel = 0 0100 } 0101 else { 0102 if(items.levelSet === "user") { 0103 Core.showMessageDialog(items.background, 0104 // The argument represents the file path name to be loaded. 0105 qsTr("The file '%1' is missing!<br>Falling back to builtin levels.").arg(items.filePath) + "<br>" + createLevelsMsg, 0106 "", null, 0107 "", null, 0108 null); 0109 } 0110 levelsFile = builtinFile; 0111 items.currentLevel = GCompris.ApplicationSettings.loadActivityProgress( 0112 "balancebox"); 0113 } 0114 0115 dataset = items.parser.parseFromUrl(levelsFile, validateLevels); 0116 if (dataset == null) { 0117 console.error("Balancebox: Error loading levels from " + levelsFile 0118 + ", can't continue!"); 0119 return; 0120 } 0121 } else { 0122 // testmode: 0123 dataset = [items.testLevel]; 0124 } 0125 numberOfLevel = dataset.length; 0126 0127 if(GCompris.ActivityInfoTree.startingLevel != -1) { 0128 items.currentLevel = Core.getInitialLevel(numberOfLevel); 0129 } 0130 0131 reconfigureScene(); 0132 } 0133 0134 function reconfigureScene() 0135 { 0136 if (items === undefined || items.mapWrapper === undefined) 0137 return; 0138 if (pendingObjects > 0) { 0139 pendingReconfigure = true; 0140 return; 0141 } 0142 0143 // set up dynamic variables for movement: 0144 pixelsPerMeter = (items.mapWrapper.length / boardSizeBase) * boardSizePix / boardSizeM; 0145 vFactor = pixelsPerMeter / box2dPpm; 0146 0147 // console.log("Starting: mode=" + items.mode 0148 // + " pixelsPerM=" + items.world.pixelsPerMeter 0149 // + " timeStep=" + items.world.timeStep 0150 // + " posIterations=" + items.world.positionIterations 0151 // + " velIterations=" + items.world.velocityIterations 0152 // + " boardSizePix" + boardSizePix + " (real " + items.mapWrapper.length + ")" 0153 // + " pixelsPerMeter=" + pixelsPerMeter 0154 // + " vFactor=" + vFactor 0155 // + " dpi=" + items.dpi 0156 // + " nativeOrientation=" + GCompris.ApplicationInfo.getNativeOrientation()); 0157 initLevel(); 0158 } 0159 0160 function sinDeg(num) 0161 { 0162 return Math.sin(num/180*Math.PI); 0163 } 0164 0165 function moveBall() 0166 { 0167 var dt = step / 1000; 0168 var dvx = g*dt * sinDeg(items.tilt.yRotation); 0169 var dvy = g*dt * sinDeg(items.tilt.xRotation); 0170 0171 // console.log("moving ball: dv: " + items.ball.body.linearVelocity.x 0172 // + "/" + items.ball.body.linearVelocity.y 0173 // + " -> " + (items.ball.body.linearVelocity.x+dvx) 0174 // + "/" + (items.ball.body.linearVelocity.y+dvy)); 0175 0176 items.ball.body.linearVelocity.x += dvx * vFactor; 0177 items.ball.body.linearVelocity.y += dvy * vFactor; 0178 0179 checkBallContacts(); 0180 } 0181 0182 function checkBallContacts() 0183 { 0184 for (var k = 0; k < ballContacts.length; k++) { 0185 if (items.ball.x > ballContacts[k].x - items.ballSize/2 && 0186 items.ball.x < ballContacts[k].x + items.ballSize/2 && 0187 items.ball.y > ballContacts[k].y - items.ballSize/2 && 0188 items.ball.y < ballContacts[k].y + items.ballSize/2) { 0189 // collision 0190 if (ballContacts[k].categories === items.holeType) 0191 finishBall(false, ballContacts[k].x, ballContacts[k].y); 0192 else if (ballContacts[k].categories === items.goalType && goalUnlocked) 0193 finishBall(true, 0194 ballContacts[k].x + (items.cellSize - items.wallSize - items.ballSize)/2, 0195 ballContacts[k].y + (items.cellSize - items.wallSize - items.ballSize)/2); 0196 else if (ballContacts[k].categories === items.buttonType) { 0197 if (!ballContacts[k].pressed 0198 && ballContacts[k].orderNum === lastContact + 1) 0199 { 0200 ballContacts[k].pressed = true; 0201 lastContact = ballContacts[k].orderNum; 0202 if (lastContact === contacts.length) { 0203 items.audioEffects.play("qrc:/gcompris/src/core/resource/sounds/win.wav"); 0204 goalUnlocked = true; 0205 goal.imageSource = baseUrl + "/door.svg"; 0206 } else 0207 items.audioEffects.play("qrc:/gcompris/src/core/resource/sounds/scroll.wav"); // bleep 0208 } 0209 } 0210 } 0211 } 0212 } 0213 0214 function finishBall(won, x, y) 0215 { 0216 finishRunning = true; 0217 items.timer.stop(); 0218 items.keyboardTimer.stop(); 0219 items.ball.x = x; 0220 items.ball.y = y; 0221 items.ball.scale = 0.4; 0222 items.ball.body.linearVelocity = Qt.point(0, 0); 0223 if (won) { 0224 items.bonus.good("flower"); 0225 if (items.levelSet === "builtin" && items.mode === "play") { 0226 GCompris.ApplicationSettings.saveActivityProgress("balancebox", 0227 items.currentLevel+1 >= numberOfLevel ? 0 : items.currentLevel+1); 0228 } 0229 } else 0230 items.bonus.bad("flower"); 0231 } 0232 0233 function stop() { 0234 // reset everything 0235 tearDown(); 0236 0237 if(goal) { 0238 goal.destroy() 0239 goal = null 0240 } 0241 // unlock screen orientation 0242 if (GCompris.ApplicationInfo.isMobile) { 0243 GCompris.ApplicationInfo.setKeepScreenOn(false); 0244 GCompris.ApplicationInfo.setRequestedOrientation(-1); 0245 } 0246 // make sure loading overlay is really stopped 0247 items.loading.stop(); 0248 } 0249 0250 function createObject(component, properties) 0251 { 0252 var p = properties; 0253 p.world = items.world; 0254 var object = component.createObject(items.mapWrapper, p); 0255 return object; 0256 } 0257 0258 var incubators; // need to reference all returned incubators in global scope 0259 // or things don't work 0260 function incubateObject(targetArr, component, properties) 0261 { 0262 var p = properties; 0263 p.world = items.world; 0264 var incubator = component.incubateObject(items.mapWrapper, p); 0265 if (incubator === null) { 0266 console.error("Error during object incubation!"); 0267 items.loading.stop(); 0268 return; 0269 } 0270 incubators.push(incubator); 0271 if (incubator.status === Qml.Component.Ready) 0272 targetArr.push(incubator.object); 0273 else if (incubator.status === Qml.Component.Loading) { 0274 pendingObjects++; 0275 incubator.onStatusChanged = function(status) { 0276 if (status === Qml.Component.Ready) 0277 targetArr.push(incubator.object); 0278 else 0279 console.error("Error during object creation!"); 0280 if (--pendingObjects === 0) { 0281 // initMap completed 0282 if (pendingReconfigure) { 0283 pendingReconfigure = false; 0284 reconfigureScene(); 0285 } else { 0286 items.timer.start(); 0287 items.loading.stop(); 0288 } 0289 } 0290 } 0291 } else 0292 console.error("Error during object creation!"); 0293 } 0294 0295 function initMap() 0296 { 0297 incubators = new Array(); 0298 goalUnlocked = true; 0299 finishRunning = false; 0300 items.mapWrapper.rows = map.length; 0301 items.mapWrapper.columns = map[0].length; 0302 pendingObjects = 0; 0303 for (var row = 0; row < map.length; row++) { 0304 for (var col = 0; col < map[row].length; col++) { 0305 var x = col * items.cellSize; 0306 var y = row * items.cellSize; 0307 var currentCase = map[row][col]; 0308 var orderNum = (currentCase & 0xFF00) >> 8; 0309 // debugging: 0310 if (debugDraw) { 0311 try { 0312 var rect = Qt.createQmlObject( 0313 "import QtQuick 2.12;Rectangle{" 0314 +"width:" + items.cellSize +";" 0315 +"height:" + items.cellSize+";" 0316 +"x:" + x + ";" 0317 +"y:" + y +";" 0318 +"color: \"transparent\";" 0319 +"border.color: \"blue\";" 0320 +"border.width: 1;" 0321 +"}", items.mapWrapper); 0322 } catch (e) { 0323 console.error("Error creating object: " + e); 0324 } 0325 } 0326 if (currentCase & NORTH) { 0327 incubateObject(walls, wallComponent, { 0328 x: x-items.wallSize/2, 0329 y: y-items.wallSize/2, 0330 width: items.cellSize + items.wallSize, 0331 height: items.wallSize, 0332 shadow: false}); 0333 } 0334 if (currentCase & SOUTH) { 0335 incubateObject(walls, wallComponent, { 0336 x: x-items.wallSize/2, 0337 y: y+items.cellSize-items.wallSize/2, 0338 width: items.cellSize+items.wallSize, 0339 height: items.wallSize, 0340 shadow: false}); 0341 } 0342 if (currentCase & EAST) { 0343 incubateObject(walls, wallComponent, { 0344 x: x+items.cellSize-items.wallSize/2, 0345 y: y-items.wallSize/2, 0346 width: items.wallSize, 0347 height: items.cellSize+items.wallSize, 0348 shadow: false}); 0349 } 0350 if (currentCase & WEST) { 0351 incubateObject(walls, wallComponent, { 0352 x: x-items.wallSize/2, 0353 y: y-items.wallSize/2, 0354 width: items.wallSize, 0355 height: items.cellSize+items.wallSize, 0356 shadow: false}); 0357 } 0358 0359 if (currentCase & START) { 0360 items.ball.x = col * items.cellSize + items.wallSize; 0361 items.ball.y = row * items.cellSize + items.wallSize; 0362 items.ball.visible = true; 0363 } 0364 0365 if (currentCase & GOAL) { 0366 var goalX = col * items.cellSize + items.wallSize/2; 0367 var goalY = row * items.cellSize + items.wallSize/2; 0368 if(goal === null) { 0369 goal = createObject(goalComponent, { 0370 x: goalX, 0371 y: goalY, 0372 width: items.cellSize - items.wallSize, 0373 height: items.cellSize - items.wallSize, 0374 imageSource: baseUrl + "/door_closed.svg", 0375 categories: items.goalType, 0376 sensor: true}); 0377 } 0378 else { 0379 goal.x = goalX; 0380 goal.y = goalY; 0381 goal.width = items.cellSize - items.wallSize; 0382 goal.height = goal.width; 0383 goal.imageSource = baseUrl + "/door_closed.svg"; 0384 } 0385 } 0386 0387 if (currentCase & HOLE) { 0388 var holeX = col * items.cellSize + items.wallSize; 0389 var holeY = row * items.cellSize + items.wallSize; 0390 incubateObject(holes, balanceItemComponent, { 0391 x: holeX, 0392 y: holeY, 0393 width: items.ballSize, 0394 height: items.ballSize, 0395 imageSource: baseUrl + "/hole.svg", 0396 density: 0, 0397 friction: 0, 0398 restitution: 0, 0399 categories: items.holeType, 0400 sensor: true}); 0401 } 0402 0403 if (orderNum > 0) { 0404 var contactX = col * items.cellSize + items.wallSize/2; 0405 var contactY = row * items.cellSize + items.wallSize/2; 0406 goalUnlocked = false; 0407 incubateObject(contacts, contactComponent, { 0408 x: contactX, 0409 y: contactY, 0410 width: items.cellSize - items.wallSize, 0411 height: items.cellSize - items.wallSize, 0412 pressed: false, 0413 density: 0, 0414 friction: 0, 0415 restitution: 0, 0416 categories: items.buttonType, 0417 sensor: true, 0418 orderNum: orderNum, 0419 text: level.targets[orderNum-1]}); 0420 } 0421 } 0422 } 0423 if (goalUnlocked && goal) // if we have no contacts at all 0424 goal.imageSource = baseUrl + "/door.svg"; 0425 0426 if (pendingObjects === 0) { 0427 // don't have any pending objects (e.g. empty map!): stop overlay 0428 items.timer.start(); 0429 items.loading.stop(); 0430 } 0431 } 0432 0433 function addBallContact(item) 0434 { 0435 if (ballContacts.indexOf(item) !== -1) 0436 return; 0437 ballContacts.push(item); 0438 } 0439 0440 function removeBallContact(item) 0441 { 0442 var index = ballContacts.indexOf(item); 0443 if (index > -1) 0444 ballContacts.splice(index, 1); 0445 } 0446 0447 function tearDown() 0448 { 0449 items.ball.body.linearVelocity = Qt.point(0, 0); 0450 items.ball.scale = 1; 0451 items.ball.visible = false; 0452 items.timer.stop(); 0453 items.keyboardTimer.stop(); 0454 if (holes.length > 0) { 0455 for (var i = 0; i< holes.length; i++) 0456 holes[i].destroy(); 0457 holes.length = 0; 0458 } 0459 if (walls.length > 0) { 0460 for (var i = 0; i< walls.length; i++) 0461 walls[i].destroy(); 0462 walls.length = 0; 0463 } 0464 if (contacts.length > 0) { 0465 for (var i = 0; i< contacts.length; i++) 0466 contacts[i].destroy(); 0467 contacts.length = 0; 0468 } 0469 lastContact = 0; 0470 items.tilt.xRotation = 0; 0471 items.tilt.yRotation = 0; 0472 ballContacts = new Array(); 0473 } 0474 0475 function initLevel(testLevel) { 0476 items.loading.start(); 0477 0478 // reset everything 0479 tearDown(); 0480 0481 level = dataset[items.currentLevel]; 0482 map = level.map 0483 initMap(); 0484 } 0485 0486 // keyboard tilting stuff: 0487 function keyboardHandler() 0488 { 0489 var MAX_TILT = 5 0490 0491 if (keyboardIsTilting) { 0492 if (lastKey == Qt.Key_Left && items.tilt.yRotation > -MAX_TILT) 0493 items.tilt.yRotation -= keyboardTiltStep; 0494 else if (lastKey == Qt.Key_Right && items.tilt.yRotation < MAX_TILT) 0495 items.tilt.yRotation += keyboardTiltStep; 0496 else if (lastKey == Qt.Key_Up && items.tilt.xRotation > -MAX_TILT) 0497 items.tilt.xRotation -= keyboardTiltStep; 0498 else if (lastKey == Qt.Key_Down && items.tilt.xRotation < MAX_TILT) 0499 items.tilt.xRotation += keyboardTiltStep; 0500 items.keyboardTimer.start(); 0501 } else {// is resetting 0502 // yRotation: 0503 if (items.tilt.yRotation < 0) 0504 items.tilt.yRotation = Math.min(items.tilt.yRotation + keyboardTiltStep, 0); 0505 else if (items.tilt.yRotation > 0) 0506 items.tilt.yRotation = Math.max(items.tilt.yRotation - keyboardTiltStep, 0); 0507 // xRotation: 0508 if (items.tilt.xRotation < 0) 0509 items.tilt.xRotation = Math.min(items.tilt.xRotation + keyboardTiltStep, 0); 0510 else if (items.tilt.xRotation > 0) 0511 items.tilt.xRotation = Math.max(items.tilt.xRotation - keyboardTiltStep, 0); 0512 // resetting done? 0513 if (items.tilt.yRotation != 0 || items.tilt.xRotation != 0) 0514 items.keyboardTimer.start(); 0515 } 0516 } 0517 0518 function processKeyPress(key) 0519 { 0520 if (key == Qt.Key_Left 0521 || key == Qt.Key_Right 0522 || key == Qt.Key_Up 0523 || key == Qt.Key_Down) { 0524 lastKey = key; 0525 keyboardIsTilting = true; 0526 items.keyboardTimer.stop(); 0527 keyboardHandler(); 0528 } 0529 } 0530 0531 function processKeyRelease(key) 0532 { 0533 if (key == Qt.Key_Left 0534 || key == Qt.Key_Right 0535 || key == Qt.Key_Up 0536 || key == Qt.Key_Down) { 0537 lastKey = key; 0538 keyboardIsTilting = false; 0539 items.keyboardTimer.stop(); 0540 keyboardHandler(); 0541 } 0542 } 0543 0544 function nextLevel() { 0545 items.currentLevel = Core.getNextLevel(items.currentLevel, numberOfLevel); 0546 initLevel(); 0547 } 0548 0549 function previousLevel() { 0550 items.currentLevel = Core.getPreviousLevel(items.currentLevel, numberOfLevel); 0551 initLevel(); 0552 }