Commit c61f77aa authored by Holger Kaelberer's avatar Holger Kaelberer
Browse files

balancebox: rework editor integration (1)

- Factor out some common code and move editor-specific code to a
dedicated js-file.
(Note!!! Can't factor out map-grid approach of editor, because the
cells containing Box2D bodies won't be on the same hierarchy level
(coordinate system) as the World, which is required by Box2D)

- Add a testing mode to allow for direct testing of an editor map.

- Load, save and maintain a whole level-set in the editor.

- Minor extensions to File API

- Add some dialogs.
parent a9de7e49
......@@ -34,13 +34,19 @@ import "balancebox.js" as Activity
ActivityBase {
id: activity
onStart: focus = true
onStop: {}
property string mode: "play" // play or test
property var testLevel
onStart: {
console.log("XXX BalanceBox onStart");
focus = true;
}
onStop: {console.log("XXX BalanceBox onStop");}
Keys.enabled: ApplicationInfo.isMobile ? false : true
Keys.onPressed: Activity.processKeyPress(event.key)
Keys.onReleased: Activity.processKeyRelease(event.key)
pageComponent: Image {
id: background
source: Activity.baseUrl + "/maze_bg.svg"
......@@ -55,10 +61,20 @@ ActivityBase {
}
}
Keys.onEscapePressed: {
console.log("XXX Balancebox onEscape");
if (activity.mode == "test") {
console.log("XXX Balancebox onEscape");
startEditor();
event.accepted = true;
} else
event.accepted = false;
}
function startEditor() {
console.log("XXX: launching editor");
editor.visible = true;
displayDialog(editor);
editorLoader.active = true;
displayDialog(editorLoader.item);
}
Button {
......@@ -69,6 +85,7 @@ ActivityBase {
height: 30
text: "Editor"
onClicked: {
//console.log("XXX mode=" + activity.mode);
startEditor();
}
}
......@@ -86,6 +103,8 @@ ActivityBase {
QtObject {
id: items
property string mode: activity.mode
property var testLevel: activity.testLevel
property Item main: activity.main
property alias background: background
property alias bar: bar
......@@ -107,15 +126,20 @@ ActivityBase {
property double dpi
}
BalanceboxEditor {
id: editor
visible: false
Loader {
id: editorLoader
active: false
sourceComponent: BalanceboxEditor {
id: editor
visible: true
testBox: activity
onClose: {
console.log("XXX editor.onClose");
activity.home()
}
onClose: {
console.log("XXX editor.onClose");
activity.home()
}
}
}
JsonParser {
......@@ -336,13 +360,20 @@ ActivityBase {
Bar {
id: bar
content: BarEnumContent { value: help | home | level }
content: BarEnumContent {
value: activity.mode == "play" ? (help | home | level) : ( help | home )
}
onHelpClicked: {
displayDialog(dialogHelp)
}
onPreviousLevelClicked: Activity.previousLevel()
onNextLevelClicked: Activity.nextLevel()
onHomeClicked: activity.home()
onHomeClicked: {
if (activity.mode == "test")
background.startEditor();
else
activity.home()
}
}
Bonus {
......
......@@ -99,39 +99,31 @@ var baseUrl = "qrc:/gcompris/src/activities/balancebox/resource";
var levelsFile = baseUrl + "/levels-default.json";
var level;
var map; // current map
var goal;
var goal = null;
var holes = new Array();
var walls = new Array();
var contacts = new Array();
var ballContacts = new Array();
var goalUnlocked;
var holes;
var walls;
var contacts;
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 userFile = GCompris.ApplicationInfo.getWritablePath() + "/balancebox/" + "levels-user.json"
function start(items_) {
items = items_;
if (GCompris.ApplicationInfo.isMobile) {
var or = GCompris.ApplicationInfo.getRequestedOrientation();
GCompris.ApplicationInfo.setRequestedOrientation(5);
/* -1: SCREEN_ORIENTATION_UNSPECIFIED
* 0: SCREEN_ORIENTATION_LANDSCAPE: forces landscape, inverted rotation
* on S2
* 5: SCREEN_ORIENTATION_NOSENSOR:
* forces 'main' orientation mode on each device (portrait on handy
* landscape on tablet and reports rotation correctly)
* 14: SCREEN_ORIENTATION_LOCKED: inverted rotation on tablet
*/
}
currentLevel = 0;
// set up dynamic variables for movement:
pixelsPerMeter = boardSizePix / boardSizeM / (items.dpi / dpiBase);
vFactor = pixelsPerMeter / box2dPpm;
console.log("Starting: pixelsPerM=" + items.world.pixelsPerMeter
console.log("Starting: mode=" + items.mode
+ " pixelsPerM=" + items.world.pixelsPerMeter
+ " timeStep=" + items.world.timeStep
+ " posIterations=" + items.world.positionIterations
+ " velIterations=" + items.world.velocityIterations
......@@ -140,21 +132,32 @@ function start(items_) {
+ " vFactor=" + vFactor
+ " dpi=" + items.dpi);
goal = null;
holes = new Array();
walls = new Array();
contacts = new Array();
ballContacts = new Array();
currentLevel = 0;
dataset = items.parser.parseFromUrl(levelsFile, validateLevels);
if (dataset == null) {
console.error("Balancebox: Error loading levels from " + levelsFile
+ ", can't continue!");
return;
if (items.mode === "play") {
if (GCompris.ApplicationInfo.isMobile) {
var or = GCompris.ApplicationInfo.getRequestedOrientation();
GCompris.ApplicationInfo.setRequestedOrientation(5);
/* -1: SCREEN_ORIENTATION_UNSPECIFIED
* 0: SCREEN_ORIENTATION_LANDSCAPE: forces landscape, inverted rotation
* on S2
* 5: SCREEN_ORIENTATION_NOSENSOR:
* forces 'main' orientation mode on each device (portrait on handy
* landscape on tablet and reports rotation correctly)
* 14: SCREEN_ORIENTATION_LOCKED: inverted rotation on tablet
*/
}
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();
}
......@@ -331,8 +334,6 @@ function initMap()
imageSource: baseUrl + "/door_closed.svg",
categories: items.goalType,
sensor: true});
//console.log("found goal at col/row " + col + "/" + row
// + " " + goalX + "/" + goalY);
}
if (map[row][col] & HOLE) {
......@@ -365,15 +366,12 @@ function initMap()
sensor: true,
orderNum: orderNum,
text: level.targets[orderNum-1]}));
//console.log("found contact at col/row " + col + "/" + row
// + " " + contactX + "/" + contactY + " w/h=" + (items.cellSize - items.wallSize));
}
}
//modelMap = modelMap.concat(map[row]);
}
// console.log("setting model to " + JSON.stringify(modelMap)
// + " cellsize=" + items.cellSize + " wallsize"+items.wallSize);
//items.mazeRepeater.model = modelMap;
if (goalUnlocked) // if we have no contacts at all
goal.imageSource = baseUrl + "/door.svg";
}
function addBallContact(item)
......@@ -422,14 +420,14 @@ function tearDown()
ballContacts = new Array();
}
function initLevel() {
function initLevel(testLevel) {
items.bar.level = currentLevel + 1;
// reset everything
tearDown();
map = dataset[currentLevel].map;
level = dataset[currentLevel];
map = level.map
initMap();
items.timer.start();
}
......@@ -503,147 +501,3 @@ function previousLevel() {
}
initLevel();
}
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
function initEditor(props)
{
console.log("XXX init editor " + props + " map: " + map);
props.mapModel.clear();
for (var row = 0; row < map.length; row++) {
for (var col = 0; col < map[row].length; col++) {
var contactValue = "";
var value = parseInt(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;
contactValue = Number(level.targets[orderNum-1]).toString();
if (contactValue > parseInt(props.contactValue) + 1)
props.contactValue = Number(parseInt(contactValue) + 1).toString();
}
props.mapModel.append({
"row": row,
"col": col,
"value": value,
"orn": orderNum,
"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 * 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 * map.length + col;
}
}
}
}
function dec2hex(i) {
return (i+0x10000).toString(16).substr(-4).toUpperCase();
}
function modelToLevel(props)
{
map = new Array();
var targets = new Array();
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;
if (obj.orn > 0) {
value |= (obj.orn << 8);
targets[obj.orn-1] = obj.contactValue;
}
map[row][col] = "0x" + dec2hex(value);
}
var level = {
level: "0",
map: map,
targets: targets
}
console.log("XXX level: " + JSON.stringify(level) + " - " + map);
}
function modifyMap(props, row, col)
{
//console.log("XXX modify map currentTool='" + props.currentTool + "'");
var modelIndex = row * map.length + col;
var obj = props.mapModel.get(modelIndex);
var newValue = obj.value;
// 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) {
console.log("GGGoal " + (obj.value & 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) {
props.mapModel.setProperty(row * map.length + col, "orn", ++props.lastOrderNum);
props.mapModel.setProperty(row * map.length + col, "contactValue", props.contactValue);
var newContact = Number(Number(props.contactValue) + 1).toString()
props.contactValue = newContact;
}
// update value by current tool bit:
newValue ^= props.currentTool;
console.log("XXX value=" + obj.value + " typeof=" + typeof(obj.value) + " newValue=" + newValue + " typeof=" + typeof(newValue));
props.mapModel.setProperty(modelIndex, "value", newValue);
}
/* GCompris - balancebox_common.js
*
* Copyright (C) 2015 Holger Kaelberer <holger.k@elberer.de>
*
* Authors:
* Holger Kaelberer <holger.k@elberer.de>
*
* 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 <http://www.gnu.org/licenses/>.
*/
var EMPTY = 0x0000;
var NORTH = 0x0001;
var EAST = 0x0002;
var SOUTH = 0x0004;
var WEST = 0x0008;
// all the following are mutually exclusive:
var START = 0x0010;
var GOAL = 0x0020;
var HOLE = 0x0040;
var CONTACT = 0x0080;
var baseUrl = "qrc:/gcompris/src/activities/balancebox/resource";
function validateLevels(doc)
{
// minimal syntax check:
if (undefined === doc || !Array.isArray(doc) || doc.length < 1)
return false;
for (var i = 0; i < doc.length; i++) {
if (undefined === doc[i].map || !Array.isArray(doc[i].map) ||
doc[i].map.length < 1)
return false;
}
return true;
}
/* GCompris - balanceboxeditor.js
*
* Copyright (C) 2015 Holger Kaelberer <holger.k@elberer.de>
*
* Authors:
* Holger Kaelberer <holger.k@elberer.de>
*
* 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 <http://www.gnu.org/licenses/>.
*/
.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 userFile = "file://" + GCompris.ApplicationInfo.getWritablePath()
+ "/balancebox/" + "levels-user.json"
var levels;
var level;
var currentLevel;
var numberOfLevel;
var levelChanged = false; // whether current level has unsaved changes
var props;
var currentIsNewLevel;
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()
{
if (currentLevel >= numberOfLevel) {
levels.push(createEmptyLevel());
numberOfLevel++;
currentIsNewLevel = true;
} else
currentIsNewLevel = false;
level = levels[currentLevel];
props.bar.level = currentLevel + 1
props.lastBallIndex = -1;
props.lastGoalIndex = -1;
props.mapModel.clear();
levelChanged = false;
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;
contactValue = Number(level.targets[orderNum-1]).toString();
if (contactValue > parseInt(props.contactValue) + 1)
props.contactValue = Number(parseInt(contactValue) + 1).toString();
}
props.mapModel.append({
"row": row,
"col": col,
"value": value,
"orn": orderNum,
"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;
}
}
}
}