File indexing completed on 2024-04-28 15:07:51
0001 /* GCompris - gletters.js 0002 * 0003 * SPDX-FileCopyrightText: 2014-2016 Holger Kaelberer 0004 * 0005 * Authors: 0006 * Bruno Coudoin <bruno.coudoin@gcompris.net> (GTK+ version) 0007 * Holger Kaelberer <holger.k@elberer.de> (Qt Quick port) 0008 * Aiswarya Kaitheri Kandoth <aiswaryakk29@gmail.com> (add speedSetting) 0009 * Timothée Giet <animtim@gmail.com> (random numbers in setLevelData) 0010 * 0011 * SPDX-License-Identifier: GPL-3.0-or-later 0012 */ 0013 0014 /* ToDo / open issues: 0015 * - adjust wordlist filenames once we have an ApplicationInfo.dataPath() or so 0016 */ 0017 0018 .pragma library 0019 .import QtQuick 2.12 as Quick 0020 .import GCompris 1.0 as GCompris //for ApplicationInfo 0021 .import "qrc:/gcompris/src/core/core.js" as Core 0022 0023 var currentSubLevel = 0; 0024 var level = null; 0025 var numberOfLevel = 0; 0026 var maxSubLevel = 0; // store number of falling elements for each level 0027 var items; 0028 var uppercaseOnly; 0029 var speedSetting; 0030 var mode; 0031 var levelData; // array to store level words 0032 0033 //speed calculations, common: 0034 var speed = 0; // how fast letters fall 0035 var fallSpeed = 0; // how often new letters are dropped 0036 var incFallSpeed = 1000; // how much drop rate increases per sublevel 0037 var incSpeed = 10; // how much speed increases per sublevel 0038 // gletters: 0039 var fallRateBase = 40; // default for how fast letters fall (smaller is faster) 0040 var fallRateMult = 80; // default for how much falling speed increases per level (smaller is faster) 0041 var dropRateBase = 5000; // default for how often new letters are dropped 0042 var dropRateMult = 100; // default for how much drop rate increases per level 0043 // wordsgame: 0044 var wgMaxFallSpeed = 7000; 0045 var wgMaxSpeed = 150; 0046 var wgMinFallSpeed = 3000; 0047 var wgMinSpeed = 50; 0048 var wgDefaultFallSpeed = 8000; 0049 var wgDefaultSpeed = 170; 0050 var wgAddSpeed = 20; 0051 var wgAddFallSpeed = 1000; 0052 var wgMaxFallingItems; 0053 0054 var droppedWords; 0055 var droppedWordsCounter = 0; 0056 var currentWord = null; // reference to the word currently typing, null if n/a 0057 var wordComponent = null; 0058 0059 var successRate // Falling speed depends on it 0060 0061 var noShiftLocale = false // for specific locales that don't need shift key, set to true in start() 0062 0063 function start(items_, uppercaseOnly_, _mode, speedSetting_) { 0064 items = items_; 0065 uppercaseOnly = uppercaseOnly_; 0066 mode = _mode; 0067 speedSetting = speedSetting_; 0068 currentSubLevel = 0; 0069 0070 incSpeed = 1 * speedSetting; 0071 incFallSpeed = 100 * speedSetting; 0072 0073 fallRateBase = 400 / speedSetting; 0074 fallRateMult = 800 / speedSetting; 0075 dropRateBase = 60000 / speedSetting; 0076 dropRateMult = 1000 / speedSetting; 0077 0078 if (mode == "word") { 0079 wgMaxFallSpeed = 90000 / speedSetting; 0080 wgMaxSpeed = 1500 / speedSetting; 0081 wgMinFallSpeed = 70000 / speedSetting; 0082 wgMinSpeed = 1300 / speedSetting; 0083 wgDefaultFallSpeed = 90000 / speedSetting; 0084 wgDefaultSpeed = 1500 / speedSetting; 0085 wgAddSpeed = 2 * speedSetting; 0086 wgAddFallSpeed = 100 * speedSetting; 0087 } 0088 0089 var locale = items.locale == "system" ? "$LOCALE" : items.locale 0090 0091 if(locale === "ml_IN") 0092 noShiftLocale = true; 0093 else 0094 noShiftLocale = false; 0095 0096 // register the voices for the locale 0097 GCompris.DownloadManager.updateResource(GCompris.GCompris.VOICES, {"locale": locale}) 0098 0099 if(!items.levels) 0100 items.wordlist.loadFromFile(GCompris.ApplicationInfo.getLocaleFilePath( 0101 items.ourActivity.dataSetUrl + "default-"+locale+".json")); 0102 else 0103 items.wordlist.loadFromJSON(items.levels); 0104 // If wordlist is empty, we try to load from short locale and if not present again, we switch to default one 0105 var localeUnderscoreIndex = locale.indexOf('_') 0106 // probably exist a better way to see if the list is empty 0107 if(items.wordlist.maxLevel == 0) { 0108 var localeShort; 0109 // We will first look again for locale xx (without _XX if exist) 0110 if(localeUnderscoreIndex > 0) { 0111 localeShort = locale.substring(0, localeUnderscoreIndex) 0112 } 0113 else { 0114 localeShort = locale; 0115 } 0116 // If not found, we will use the default file 0117 items.wordlist.useDefault = true 0118 if(!items.levels) 0119 items.wordlist.loadFromFile(GCompris.ApplicationInfo.getLocaleFilePath( 0120 items.ourActivity.dataSetUrl + "default-"+localeShort+".json")); 0121 else 0122 items.wordlist.loadFromJSON(items.levels); 0123 // We remove the using of default file for next time we enter this function 0124 items.wordlist.useDefault = false 0125 } 0126 numberOfLevel = items.wordlist.maxLevel; 0127 items.currentLevel = Core.getInitialLevel(numberOfLevel); 0128 droppedWords = new Array(); 0129 droppedWordsCounter = 0; 0130 initLevel(); 0131 } 0132 0133 function stop() { 0134 deleteWords(); 0135 wordComponent = null 0136 items.wordDropTimer.stop(); 0137 } 0138 0139 function initLevel() { 0140 items.score.currentSubLevel = 0; 0141 if(items.levels) 0142 items.instructionText = items.levels[items.currentLevel].objective 0143 items.audioVoices.clearQueue() 0144 items.inputLocked = false; 0145 wgMaxFallingItems = 3 0146 successRate = 1.0 0147 droppedWordsCounter = 0 0148 0149 // initialize level 0150 deleteWords(); 0151 level = items.wordlist.getLevelWordList(items.currentLevel + 1); 0152 /* for smallnumbers2, maxSubLevel will take value of sublevels attribute from json which represent number of 0153 falling elements in each level and for other activities it will be 0 here.*/ 0154 maxSubLevel = items.wordlist.getMaxSubLevel(items.currentLevel + 1); 0155 levelData = new Array(); 0156 0157 // for smallnumbers2 and smallnumbers activities levelData will contain random data, while for other activity it contains same data as level.words 0158 if(items.ourActivity.useDataset === true) 0159 setLevelData(); 0160 else 0161 levelData = level.words 0162 0163 if (maxSubLevel == 0) { 0164 // If "sublevels" length is not set in wordlist, use the words length 0165 maxSubLevel = levelData.length 0166 } 0167 items.score.numberOfSubLevels = maxSubLevel; 0168 setSpeed(); 0169 /*console.log("Gletters: initializing level " + (items.currentLevel + 1) 0170 + " maxSubLvl=" + maxSubLevel 0171 + " wordCount=" + level.words.length 0172 + " speed=" + speed + " fallspeed=" + fallSpeed);*/ 0173 0174 { 0175 /* populate VirtualKeyboard for mobile: 0176 * 1. for < 10 letters print them all in the same row 0177 * 2. for > 10 letters create 3 rows with equal amount of keys per row 0178 * if possible, otherwise more keys in the upper rows 0179 * 3. if we have both upper- and lowercase letters activate the shift 0180 * key*/ 0181 // first generate a map of needed letters 0182 var letters = new Array(); 0183 items.keyboard.shiftKey = false; 0184 for (var i = 0; i < levelData.length; i++) { 0185 if(mode ==='letter') { 0186 // The word is a letter, even if it has several chars (digraph) 0187 var letter = levelData[i]; 0188 var isUpper = (letter == letter.toLocaleUpperCase()); 0189 var isDigit = (letter.toLocaleLowerCase() === letter.toLocaleUpperCase()) 0190 if (!isDigit && isUpper && letters.indexOf(letter.toLocaleLowerCase()) !== -1 0191 && !noShiftLocale) 0192 items.keyboard.shiftKey = true; 0193 else if (!isDigit && !isUpper && letters.indexOf(letter.toLocaleUpperCase()) !== -1 0194 && !noShiftLocale) 0195 items.keyboard.shiftKey = true; 0196 else if (letters.indexOf(letter) === -1) 0197 letters.push(levelData[i]); 0198 } else { 0199 // We split each word in char to create the keyboard 0200 for (var j = 0; j < levelData[i].length; j++) { 0201 var letter = levelData[i].charAt(j); 0202 var isUpper = (letter == letter.toLocaleUpperCase()); 0203 if (isUpper && letters.indexOf(letter.toLocaleLowerCase()) !== -1 0204 && !noShiftLocale) 0205 items.keyboard.shiftKey = true; 0206 else if (!isUpper && letters.indexOf(letter.toLocaleUpperCase()) !== -1 0207 && !noShiftLocale) 0208 items.keyboard.shiftKey = true; 0209 else if (letters.indexOf(letter) === -1) 0210 letters.push(levelData[i].charAt(j)); 0211 } 0212 } 0213 } 0214 letters = GCompris.ApplicationInfo.localeSort(letters, items.locale); 0215 // generate layout from letter map 0216 var layout = new Array(); 0217 var row = 0; 0218 var offset = 0; 0219 while (offset < letters.length) { 0220 var cols = letters.length <= 10 ? letters.length : (Math.ceil((letters.length-offset) / (3 - row))); 0221 layout[row] = new Array(); 0222 for (var j = 0; j < cols; j++) 0223 layout[row][j] = { label: letters[j+offset] }; 0224 offset += j; 0225 row++; 0226 } 0227 items.keyboard.layout = layout; 0228 } 0229 if(items.ourActivity.useDataset === true) 0230 items.wordlist.randomWordList = levelData 0231 else 0232 items.wordlist.initRandomWord(items.currentLevel + 1) 0233 0234 initSubLevel() 0235 } 0236 0237 // function to create array of random data 0238 function setLevelData() { 0239 // generate a random index for an element to be added in levelData 0240 // to increase probability to get a specific number, add it several times in the dataset list accordingly 0241 var previousNumber = 0 0242 var nextNumber = 0 0243 // special case if only 2 numbers available 0244 if(level.words.length === 2) { 0245 for(var i = 0; i < maxSubLevel; i++) { 0246 var index = Math.floor(Math.random() * level.words.length); 0247 levelData.push(level.words[index]); 0248 } 0249 } 0250 else { 0251 for(var i = 0; i < maxSubLevel; i++) { 0252 // avoid to have twice same number in a row 0253 while(nextNumber == previousNumber) { 0254 var index = Math.floor(Math.random() * level.words.length); 0255 nextNumber = level.words[index]; 0256 } 0257 previousNumber = nextNumber 0258 levelData.push(nextNumber) 0259 } 0260 } 0261 0262 } 0263 0264 function initSubLevel() { 0265 currentWord = null; 0266 if (currentSubLevel != 0) { 0267 // increase speed 0268 speed = Math.max(speed - incSpeed, wgMinSpeed); 0269 items.wordDropTimer.interval = fallSpeed = Math.max(fallSpeed - incFallSpeed, wgMinFallSpeed); 0270 } 0271 // note, last word is still fading out so better use droppedWordsCounter than droppedWords.length in this case 0272 if ((currentSubLevel == 0 || droppedWordsCounter == 0) && !items.inputLocked) 0273 dropWord(); 0274 //console.log("Gletters: initializing subLevel " + (currentSubLevel + 1) + " words=" + JSON.stringify(level.words)); 0275 } 0276 0277 function processKeyPress(text) { 0278 if(items.inputLocked) 0279 return 0280 var typedText = uppercaseOnly ? text.toLocaleUpperCase() : text; 0281 0282 if (currentWord !== null) { 0283 // check against a currently typed word 0284 if (!currentWord.checkMatch(typedText)) { 0285 currentWord = null; 0286 audioCrashPlay() 0287 } else { 0288 playLetter(text) 0289 } 0290 } else { 0291 // no current word, check against all available words 0292 var found = false 0293 for (var i = 0; i< droppedWords.length; i++) { 0294 if (droppedWords[i].checkMatch(typedText)) { 0295 // typed correctly 0296 currentWord = droppedWords[i]; 0297 playLetter(text) 0298 found = true 0299 break; 0300 } 0301 } 0302 if(!found) { 0303 audioCrashPlay() 0304 } 0305 } 0306 0307 if (currentWord !== null && currentWord.isCompleted()) { 0308 // win! 0309 droppedWordsCounter -= 1 0310 currentWord.won(); // note: deleteWord() is triggered after fadeout 0311 successRate += 0.1 0312 currentWord = null 0313 nextSubLevel(); 0314 } 0315 } 0316 0317 function setSpeed() 0318 { 0319 if (mode === "letter") { 0320 speed = (level.speed !== undefined) ? level.speed : (fallRateBase + Math.floor(fallRateMult / (items.currentLevel + 1))); 0321 fallSpeed = (level.fallspeed !== undefined) ? level.fallspeed : Math.floor((dropRateBase - (dropRateMult * (items.currentLevel + 1)))); 0322 } else { // wordsgame 0323 speed = (level.speed !== undefined) ? level.speed : wgDefaultSpeed - (items.currentLevel + 1)*wgAddSpeed; 0324 fallSpeed = (level.fallspeed !== undefined) ? level.fallspeed : wgDefaultFallSpeed - (items.currentLevel + 1)*wgAddFallSpeed 0325 0326 if(speed < wgMinSpeed ) speed = wgMinSpeed; 0327 if(speed > wgMaxSpeed ) speed = wgMaxSpeed; 0328 if(fallSpeed < wgMinFallSpeed ) fallSpeed = wgMinFallSpeed; 0329 if(fallSpeed > wgMaxFallSpeed ) fallSpeed = wgMaxFallSpeed; 0330 } 0331 items.wordDropTimer.interval = fallSpeed; 0332 } 0333 0334 function deleteWords() 0335 { 0336 if (droppedWords === undefined || droppedWords.length < 1) 0337 return; 0338 for (var i = 0; i< droppedWords.length; i++) 0339 droppedWords[i].destroy(); 0340 droppedWords.length = 0; 0341 } 0342 0343 function deleteWord(w) 0344 { 0345 if (droppedWords === undefined || droppedWords.length < 1) 0346 return; 0347 if (w == currentWord) 0348 currentWord = null; 0349 for (var i = 0; i< droppedWords.length; i++) 0350 if (droppedWords[i] == w) { 0351 droppedWords[i].destroy(); 0352 droppedWords.splice(i, 1); 0353 break; 0354 } 0355 } 0356 0357 function createWord() 0358 { 0359 if (wordComponent.status == 1 /* Component.Ready */) { 0360 var text = items.wordlist.getRandomWord(); 0361 if(!text) { 0362 items.wordDropTimer.restart(); 0363 return 0364 } 0365 0366 // if uppercaseOnly case does not matter otherwise it does 0367 if (uppercaseOnly) 0368 text = text.toLocaleUpperCase(); 0369 0370 var word 0371 0372 if(items.ourActivity.getImage(text)) { 0373 word = wordComponent.createObject( items.background, 0374 { 0375 "text": text, 0376 "image": items.ourActivity.getImage(text), 0377 // assume x=width-25px for now, Word auto-adjusts onCompleted(): 0378 "x": Math.random() * (items.main.width - 25), 0379 "y": -25, 0380 }); 0381 } else if(items.ourActivity.getDominoValues(text).length) { 0382 word = wordComponent.createObject( items.background, 0383 { 0384 "text": text, 0385 "mode": items.ourActivity.getMode(), 0386 "dominoValues": items.ourActivity.getDominoValues(text), 0387 // assume x=width-25px for now, Word auto-adjusts onCompleted(): 0388 "x": Math.random() * (items.main.width - 25), 0389 "y": -25, 0390 }); 0391 } else { 0392 word = wordComponent.createObject( items.background, 0393 { 0394 "text": text, 0395 // assume x=width-25px for now, Word auto-adjusts onCompleted(): 0396 "x": Math.random() * (items.main.width - 25), 0397 "y": -25, 0398 "mode": mode, 0399 }); 0400 } 0401 0402 if (word === null) 0403 console.log("Gletters: Error creating word object"); 0404 else { 0405 droppedWords[droppedWords.length] = word; 0406 droppedWordsCounter += 1 0407 // speed to duration: 0408 var duration = (items.main.height / 2) * speed / successRate; 0409 /* console.debug("Gletters: dropping new word " + word.text 0410 + " duration=" + duration + " (speed=" + speed + ")" 0411 + " num=" + droppedWords.length);*/ 0412 word.startMoving(duration); 0413 } 0414 items.wordDropTimer.restart(); 0415 } else if (wordComponent.status == 3 /* Component.Error */) { 0416 console.log("Gletters: error creating word component: " + wordComponent.errorString()); 0417 } 0418 } 0419 0420 function dropWord() 0421 { 0422 // Do not create too many falling items 0423 if(droppedWords.length > wgMaxFallingItems) { 0424 items.wordDropTimer.restart(); 0425 return 0426 } 0427 0428 if (wordComponent !== null) 0429 createWord(); 0430 else { 0431 var text = items.wordlist.getRandomWord(); 0432 items.wordlist.appendRandomWord(text) 0433 var fallingItem 0434 if(items.ourActivity.getImage(text)) 0435 fallingItem = "FallingImage.qml" 0436 else if(items.ourActivity.getDominoValues(text).length) 0437 fallingItem = "FallingDomino.qml" 0438 else 0439 fallingItem = "FallingWord.qml" 0440 0441 0442 wordComponent = Qt.createComponent("qrc:/gcompris/src/activities/gletters/" + fallingItem); 0443 if (wordComponent.status == 1 /* Component.Ready */) 0444 createWord(); 0445 else if (wordComponent.status == 3 /* Component.Error */) { 0446 console.log("Gletters: error creating word component: " + wordComponent.errorString()); 0447 } else 0448 wordComponent.statusChanged.connect(createWord); 0449 } 0450 } 0451 0452 function appendRandomWord(word) { 0453 items.wordlist.appendRandomWord(word) 0454 } 0455 0456 function audioCrashPlay() { 0457 if(successRate > 0.5) 0458 successRate -= 0.1 0459 items.audioEffects.play("qrc:/gcompris/src/core/resource/sounds/crash.wav") 0460 } 0461 0462 function nextLevel() { 0463 items.currentLevel = Core.getNextLevel(items.currentLevel, numberOfLevel); 0464 currentSubLevel = 0; 0465 initLevel(); 0466 } 0467 0468 function previousLevel() { 0469 items.currentLevel = Core.getPreviousLevel(items.currentLevel, numberOfLevel); 0470 currentSubLevel = 0; 0471 initLevel(); 0472 } 0473 0474 function nextSubLevel() { 0475 items.score.currentSubLevel += 1; 0476 items.score.playWinAnimation(); 0477 if(++currentSubLevel >= maxSubLevel) { 0478 // Stop having more words dropping once we have won 0479 items.wordDropTimer.stop(); 0480 items.inputLocked = true; 0481 // In case we have no audio voices for the locale, we directly play the bonus, else it is played at the end of the audio 0482 if(items.audioVoices.files.length == 0) { 0483 items.bonus.good("lion"); 0484 } 0485 } else { 0486 initSubLevel(); 0487 } 0488 } 0489 0490 function playLetter(letter) { 0491 var locale = GCompris.ApplicationInfo.getVoicesLocale(items.locale) 0492 0493 items.audioVoices.append(GCompris.ApplicationInfo.getAudioFilePath("voices-$CA/"+locale+"/alphabet/" 0494 + Core.getSoundFilenamForChar(letter))) 0495 } 0496 0497 function focusTextInput() { 0498 if (!GCompris.ApplicationInfo.isMobile && items && items.textinput) 0499 items.textinput.forceActiveFocus(); 0500 }