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 }