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 }