var onLevelRestarted = new Event("levelRestarted"); var RandomGen = new RNG(); var intro_template = [ "..................................", "..................................", "..................................", "......Puzzle Script Terminal......", "..............v 1.7+..............", "..................................", "..................................", "..................................", ".........insert cartridge.........", "................................. ", "................................ ", "............................... ", ".............................. " ]; var sitelock_template = [ "..................................", "..................................", "..................................", "......Puzzle Script Terminal......", "..............v 1.7+..............", "..................................", "..................................", "..................................", ".....This game is sitelocked!.....", "................................. ", "................................ ", "............................... ", ".............................. " ]; var messagecontainer_template = [ "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "..........X to continue...........", "..................................", ".................................." ]; var messagecontainer_template_mouse = [ "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "........Click to continue.........", "..................................", ".................................." ]; var titletemplate_firstgo = [ "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "..........#.start game.#..........", "..................................", "..................................", "..................................", "..................................", "..................................", ".................................."]; var titletemplate_firstgo_selected = [ "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "###########.start game.###########", "..................................", "..................................", "..................................", "..................................", "..................................", ".................................."]; var titletemplate_empty = [ "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", "..................................", ".................................."]; var title_options = [[ ".............continue.............", "...........#.continue.#...........", "############.continue.############", "...........>.continue.<...........", ], [ "...........level select...........", ".........#.level select.#.........", "##########.level select.##########", ".........>.level select.<..........", ], [ ".............settings.............", "...........#.settings.#...........", "############.settings.############", "...........>.settings.<...........", ], [ ".............new game.............", "...........#.new game.#...........", "############.new game.############", "...........>.new game.<...........", //QQQ ]]; var titleImage=[]; var titleWidth=titletemplate_empty[0].length; var titleHeight=titletemplate_empty.length; var textMode=true; var titleScreen=true; var titleMode=0;//1 means title screen with options, 2 means level select var titleSelection=0; var titleSelectOptions=2; var titleSelected=false; var hoverSelection=-1; //When mouse controls are enabled, over which row the mouse is hovering. -1 when disabled. function showContinueOptionOnTitleScreen(){ return (curlevel>0||curlevelTarget!==null)&&(curlevel in state.levels); } function unloadGame() { state=introstate; level = new Level(0, 5, 5, 2, null, null); level.objects = new Int32Array(0); generateTitleScreen(); canvasResize(); redraw(); titleSelected=true; } function isContinueOptionSelected() { if(state.metadata["continue_is_level_select"] !== undefined) { return false; } return titleSelection == 0; // continue option is always 1st } function isNewGameOptionSelected() { return titleSelection == titleSelectOptions-1; // new game option is always on bottom } function isLevelSelectOptionSelected() { if(state.metadata["level_select"] === undefined) { return false; } if(state.metadata["continue_is_level_select"] !== undefined) { return titleSelection == 0; // continue option is always 1st } return titleSelection == 1; // level select option is always 2nd if it exists } function generateTitleScreen() { tryLoadCustomFont(); titleMode=showContinueOptionOnTitleScreen()?1:0; if (state.levels.length===0) { titleImage=intro_template; return; } if (isSitelocked()) { titleImage = sitelock_template; return; } var title = "PuzzleScript Game"; if (state.metadata.title!==undefined) { title=state.metadata.title; } if (titleMode===0) { titleSelectOptions = 1; if (titleSelected) { titleImage = deepClone(titletemplate_firstgo_selected); } else { titleImage = deepClone(titletemplate_firstgo); } } else { var availableOptions = [0]; titleSelectOptions = 2; if(state.metadata["level_select"] !== undefined && state.metadata["continue_is_level_select"] === undefined) { titleSelectOptions++; availableOptions.push(1); } if(state.metadata["settings"] !== undefined) { titleSelectOptions++; availableOptions.push(2); } availableOptions.push(3); titleImage = deepClone(titletemplate_empty); for(var i = 0; i < titleSelectOptions; i++) { var version = 0; var j = 0; if(titleSelectOptions == 2 && i == 1) { j = 1; } var lineInTitle = 5 + i + j; if(titleSelection == i) { if(titleSelected) { version = 2; } else { version = 1; } } if (hoverSelection == lineInTitle && !titleSelected && hoverSelection >= 0) { version = 3; } titleImage[lineInTitle] = deepClone(title_options[availableOptions[i]][version]); } } if (state.metadata.text_controls) { for (var i=0;i3){ titlelines.splice(3); logWarning("Game title is too long to fit on screen, truncating to three lines.",undefined,true); } } else { if ( titlelines.length>5){ titlelines.splice(5); logWarning("Game title is too long to fit on screen, truncating to five lines.",undefined,true); } } for (var i=0;i3){ attributionsplit.splice(3); logWarning("Author list too long to fit on screen, truncating to three lines.",undefined,true); } for (var i=0;iwidth){ line=line.slice(0,width); } var row = titleImage[3+i]; titleImage[3+i]=row.slice(0,width-line.length)+line; } } } var levelSelectScrollPos = 0; function gotoLevelSelectScreen() { if(state.metadata["level_select"] === undefined){ goToTitleScreen(); return; } levelSelectScrollPos = 0; titleSelected = false; timer = 0; quittingTitleScreen = false; quittingMessageScreen = false; messageselected = false; titleMode = 2; titleScreen = true; textMode = true; againing = false; messagetext = ""; if (titleSelection == false) { for(var i = 0; i < state.sections.length; i++) { if(state.sections[i].firstLevel > curlevel) { titleSelection = Math.max(0,i-1); break; } } } state.metadata = deepClone(state.default_metadata); twiddleMetadataExtras(); generateLevelSelectScreen(); redraw(); } function generateLevelSelectScreen() { /* "...........select level...........", "..................................", "[✓].#.Testy section maxxx long.#.O", ".................................|", "[ ]...Unselected section.........|", ".................................|", "[ ]...Another section............|", ".................................|", "[ ]...Another section............|", ".................................|", "[ ]...Another section............|", ".................................|", "[ ]...Another section............|" */ titleImage = [ " [ ESC: Back ] ", " Level Select " ]; if (hoverSelection == 0) { titleImage[0] = "[ ESC: Back ] "; } amountOfLevelsOnScreen = 9; titleSelectOptions = state.sections.length; if(titleSelection < levelSelectScrollPos) { //Up levelSelectScrollPos = titleSelection; } else if(titleSelection >= levelSelectScrollPos + amountOfLevelsOnScreen) { //Down levelSelectScrollPos = titleSelection - amountOfLevelsOnScreen + 1; } var posOnScreen = titleSelection - levelSelectScrollPos; if (posOnScreen == 0) { levelSelectScrollPos = Math.max(0, levelSelectScrollPos - 1); //console.log("On first position"); } if (posOnScreen == amountOfLevelsOnScreen-1) { levelSelectScrollPos = Math.min(state.sections.length-amountOfLevelsOnScreen, levelSelectScrollPos + 1); //console.log("On last position"); } if (levelSelectScrollPos != 0) { if (hoverSelection == 2) { titleImage.push(" [ PREV ]"); } else { titleImage.push(" [ PREV ] "); } } else { titleImage.push(" "); } var unlockedUntil = -1; if(state.metadata["level_select_lock"] !== undefined) { // find last solved section: var unsolvedSections = 0; for(var i = 0; i < state.sections.length; i++) { if(solvedSections.indexOf(state.sections[i].name) >= 0) { unlockedUntil = i; } else { unsolvedSections++; } } if(state.metadata.level_select_unlocked_ahead !== undefined && state.metadata.level_select_unlocked_rollover !== undefined) { logErrorNoLine("You can't both have level select unlocked ahead & rollover methods, please choose just one!", true); unlockedUntil += 1; } else if (state.metadata.level_select_unlocked_rollover !== undefined) { unlockedUntil = solvedSections.length + Number(state.metadata.level_select_unlocked_rollover) - 1; } else if(state.metadata.level_select_unlocked_ahead === undefined) { unlockedUntil += 1; } else { unlockedUntil += Number(state.metadata.level_select_unlocked_ahead); } //console.log("total: " + state.sections.length + "unsolved: " + unsolvedSections + " until:" + unlockedUntil) } //console.log(unlockedUntil); for(var i = levelSelectScrollPos; i < levelSelectScrollPos + amountOfLevelsOnScreen; i++) { if(i < 0 || i >= state.sections.length) { break; } var section = state.sections[i]; var solved = (solvedSections.indexOf(section.name) >= 0); var selected = (i == titleSelection); var locked = (unlockedUntil >= 0 && i > unlockedUntil); var name = section.name.substring(0, 24); if(locked) { if(selected && titleSelected) { titleSelected = false; quittingTitleScreen = false; } var l = name.length; name = ""; for(var j = 0; j < l; j++) { name += "*"; } } var solved_symbol = "✓"; if(state.metadata.level_select_solve_symbol !== undefined) { solved_symbol = state.metadata.level_select_solve_symbol; } var line = (solved ? solved_symbol : " ") + " "; var hover_symbol = " "; if (selected) {hover_symbol = "#"} if (hoverSelection - 3 + levelSelectScrollPos == i) {hover_symbol = ">"} line += hover_symbol + " " + name; for(var j = name.length; j < 25; j++) { if(selected && titleSelected && j != name.length) { line += "#"; } else { line += " "; } } line += (selected ? "#" : " ") + " "; titleImage.push(line); } if (levelSelectScrollPos != titleSelectOptions - amountOfLevelsOnScreen && titleSelectOptions - amountOfLevelsOnScreen > 0) { if (hoverSelection == 12) { titleImage.push(" [ NEXT ]") } else { titleImage.push(" [ NEXT ] ") } } else { titleImage.push(" "); } for(var i = titleImage.length; i < 13; i++) { titleImage.push(" "); } redraw(); } function gotoLevel(sectionIndex) { if (solving) { return; } if (sectionIndex < 0) {return;} //Invalid index //console.log(sectionIndex); againing = false; messagetext = ""; curlevel = state.sections[sectionIndex].firstLevel; loadLevelFromStateOrTarget(); updateLocalStorage(); resetFlickDat(); canvasResize(); clearInputHistory(); } var introstate = { title: "EMPTY GAME", attribution: "increpare", objectCount: 2, metadata:[], levels:[], bgcolor:"#000000", fgcolor:"#FFFFFF" }; var state = introstate; function deepClone(item) { if (!item) { return item; } // null, undefined values check var types = [ Number, String, Boolean ], result; // normalizing primitives if someone did new String('aaa'), or new Number('444'); types.forEach(function(type) { if (item instanceof type) { result = type( item ); } }); if (typeof result == "undefined") { if (Object.prototype.toString.call( item ) === "[object Array]") { result = []; item.forEach(function(child, index, array) { result[index] = deepClone( child ); }); } else if (typeof item == "object") { // testing that this is DOM if (item.nodeType && typeof item.cloneNode == "function") { var result = item.cloneNode( true ); } else if (!item.prototype) { // check that this is a literal if (item instanceof Date) { result = new Date(item); } else { // it is an object literal result = {}; for (var i in item) { result[i] = deepClone( item[i] ); } } } else { // depending what you would like here, // just keep the reference, or create new object /* if (false && item.constructor) { // would not advice to do that, reason? Read below result = new item.constructor(); } else */{ result = item; } } } else { result = item; } } return result; } function wordwrap( str, width, handleNewlines = false ) { width = width || 75; var cut = true; if (!str) { return str; } //console.log("Before: "+str) var regex = '.{1,' +width+ '}(\\s|$)' + (cut ? '|.{' +width+ '}|.+$' : '|\\S+?(\\s|$)'); if (!handleNewlines) { return str.match( RegExp(regex, 'g') ); } else { splitNewlines = str.split("\\n"); var splitString = []; splitNewlines.forEach(splitStr => { splitString = splitString.concat(splitStr.match( RegExp(regex, 'g') )); }) //console.log(splitString); return splitString; } } var splitMessage=[]; function drawMessageScreen() { tryLoadCustomFont(); titleMode=0; textMode=true; if ("text_message_continue" in state.metadata) { titleImage = deepClone(messagecontainer_template); for (var i=0;i=10) { if (count<12){ endPos = count + 1; } else { endPos = 12; } } if (quittingMessageScreen) { titleImage[endPos]=emptyLineStr; } else { titleImage[endPos]=xToContinueStr; } canvasResize(); } var loadedLevelSeed=0; function loadLevelFromLevelDat(state,leveldat,randomseed,clearinputhistory) { if (randomseed==null) { randomseed = (Math.random() + Date.now()).toString(); } loadedLevelSeed = randomseed; RandomGen = new RNG(loadedLevelSeed); forceRegenImages=true; ignoreNotJustPressedAction=true; titleScreen=false; titleMode=showContinueOptionOnTitleScreen()?1:0; titleSelection=0; titleSelected=false; dragging = false; rightdragging = false; state.metadata = deepClone(state.default_metadata); againing=false; if (leveldat===undefined) { consolePrint("Trying to access a level that doesn't exist.",true); goToTitleScreen(); return; } if (leveldat.message !== undefined) { // This "level" is actually a message. ignoreNotJustPressedAction=true; tryPlayShowMessageSound(); twiddleMetadataExtras(); drawMessageScreen(); canvasResize(); clearInputHistory(); } else if (leveldat.target !== undefined) { // This "level" is actually a goto. //tryPlayGotoSound(); setSectionSolved(state.levels[Number(curlevel)].section) gotoLevel(leveldat.target); } else { titleMode=0; textMode=false; level = leveldat.clone(); RebuildLevelArrays(); if (state!==undefined) { if (state.metadata.flickscreen!==undefined){ oldflickscreendat=[ 0, 0, Math.min(state.metadata.flickscreen[0],level.width), Math.min(state.metadata.flickscreen[1],level.height) ]; } else if (state.metadata.zoomscreen!==undefined){ oldflickscreendat=[ 0, 0, Math.min(state.metadata.zoomscreen[0],level.width), Math.min(state.metadata.zoomscreen[1],level.height) ]; } else if (state.metadata.smoothscreen!==undefined){ oldflickscreendat=[ 0, 0, Math.min(state.metadata.smoothscreen.screenSize.width,level.width), Math.min(state.metadata.smoothscreen.screenSize.height,level.height) ]; } } initSmoothCamera(); twiddleMetadataExtras(); backups=[] restartTarget=backupLevel(); keybuffer=[]; if ('run_rules_on_level_start' in state.metadata) { runrulesonlevelstart_phase=true; processInput(-1,true); runrulesonlevelstart_phase=false; } } if (clearinputhistory===true){ clearInputHistory(); } } function loadLevelFromStateTarget(state,levelindex,target,randomseed) { var leveldat = target; curlevel=levelindex; curlevelTarget=target; if (leveldat.message===undefined) { if (levelindex=== 0){ tryPlayStartLevelSound(); } else { tryPlayStartLevelSound(); } } loadLevelFromLevelDat(state,state.levels[levelindex],randomseed); restoreLevel(target, true); restartTarget=target; } function loadLevelFromState(state,levelindex,randomseed) { var leveldat = state.levels[levelindex]; curlevel=levelindex; curlevelTarget=null; if (leveldat!==undefined && leveldat.message===undefined) { document.dispatchEvent(new CustomEvent("psplusLevelLoaded", {detail: levelindex})); if (levelindex=== 0){ tryPlayStartLevelSound(); } else { tryPlayStartLevelSound(); } } loadLevelFromLevelDat(state,leveldat,randomseed); } var sprites = [ { color: '#423563', dat: [ [1, 1, 1, 1, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 1] ] }, { color: '#252342', dat: [ [0, 0, 1, 0, 0], [1, 1, 1, 1, 1], [0, 0, 1, 0, 0], [0, 1, 1, 1, 0], [0, 1, 0, 1, 0] ] } ]; loadedCustomFont = false; function tryLoadCustomFont() { if(state == null || state.metadata == null || state.metadata.custom_font == undefined || loadedCustomFont) { return; } var custom_font = new FontFace('PuzzleCustomFont', 'url('+state.metadata.custom_font+')'); custom_font.load().then(function(loaded_face) { document.fonts.add(loaded_face); loadedCustomFont = true; canvasResize(); redraw(); }).catch(function(error) {alert("Unable to load font!");}); } tryLoadCustomFont(); generateTitleScreen(); if (titleMode>0){ titleSelection=0; } canvasResize(); function tryPlaySimpleSound(soundname) { if (state.sfx_Events[soundname]!==undefined) { var seed = state.sfx_Events[soundname]; playSound(seed); } } function tryPlayTitleSound() { tryPlaySimpleSound("titlescreen"); } function tryPlayStartGameSound() { tryPlaySimpleSound("startgame"); } function tryPlayEndGameSound() { tryPlaySimpleSound("endgame"); } function tryPlayCancelSound() { tryPlaySimpleSound("cancel"); } function tryPlayStartLevelSound() { tryPlaySimpleSound("startlevel"); } function tryPlayEndLevelSound() { tryPlaySimpleSound("endlevel"); } function tryPlayUndoSound(){ tryPlaySimpleSound("undo"); } function tryPlayRestartSound(){ tryPlaySimpleSound("restart"); } function tryPlayShowMessageSound(){ tryPlaySimpleSound("showmessage"); } function tryPlayCloseMessageSound(){ tryPlaySimpleSound("closemessage"); } var backups=[]; var restartTarget; function backupLevel() { var ret = { dat : new Int32Array(level.objects), width : level.width, height : level.height, oldflickscreendat: oldflickscreendat.concat([]), cameraPositionTarget: Object.assign({}, cameraPositionTarget) }; if (state.metadata.runtime_metadata_twiddling !== undefined) { var metadata = deepClone(state.metadata) delete metadata.custom_font; ret.metadata = metadata; } return ret; } function level4Serialization() { var ret = { dat : Array.from(level.objects), width : level.width, height : level.height, oldflickscreendat: oldflickscreendat.concat([]), cameraPositionTarget: Object.assign({}, cameraPositionTarget) }; return ret; } function tryDeactivateYoutube(){ var youtubeFrame = document.getElementById("youtubeFrame"); if (youtubeFrame){ document.body.removeChild(youtubeFrame); } } function tryActivateYoutube(){ var youtubeFrame = document.getElementById("youtubeFrame"); if (youtubeFrame){ return; } if (canYoutube) { if ('youtube' in state.metadata) { var youtubeid=state.metadata['youtube']; var url = "https://www.youtube.com/embed/"+youtubeid+"?autoplay=1&loop=1&playlist="+youtubeid; ifrm = document.createElement("IFRAME"); ifrm.setAttribute("src",url); ifrm.setAttribute("id","youtubeFrame"); ifrm.style.visibility="hidden"; ifrm.style.width="500px"; ifrm.style.height="500px"; ifrm.style.position="absolute"; ifrm.style.top="-1000px"; ifrm.style.left="-1000px"; document.body.appendChild(ifrm); } } } function setGameState(_state, command, randomseed) { oldflickscreendat=[]; timer=0; autotick=0; winning=false; againing=false; messageselected=false; STRIDE_MOV=_state.STRIDE_MOV; STRIDE_OBJ=_state.STRIDE_OBJ; sfxCreateMask=new BitVec(STRIDE_OBJ); sfxDestroyMask=new BitVec(STRIDE_OBJ); if (command===undefined) { command=["restart"]; } if ((state.levels.length===0 || _state.levels.length===0) && command.length>0 && command[0]==="rebuild") { command=["restart"]; } if (randomseed===undefined) { randomseed=null; } RandomGen = new RNG(randomseed); state = _state; if (command[0]!=="rebuild"){ backups=[]; } //set sprites sprites = []; for (var n in state.objects) { if (state.objects.hasOwnProperty(n)) { var object = state.objects[n]; var sprite = { colors: object.colors, dat: object.spritematrix }; sprites[object.id] = sprite; } } if (state.metadata.realtime_interval!==undefined) { autotick=0; autotickinterval=state.metadata.realtime_interval*1000; } else { autotick=0; autotickinterval=0; } if (state.metadata.key_repeat_interval!==undefined) { repeatinterval=state.metadata.key_repeat_interval*1000; } else { repeatinterval=150; } if (state.metadata.tween_length!==undefined) { tweeninterval=state.metadata.tween_length*1000; } else { tweeninterval = 0; } if (state.metadata.again_interval!==undefined) { againinterval=state.metadata.again_interval*1000; } else { againinterval=150; } if (throttle_movement && autotickinterval===0) { logWarning("throttle_movement is designed for use in conjunction with realtime_interval. Using it in other situations makes games gross and unresponsive, broadly speaking. Please don't."); } norepeat_action = state.metadata.norepeat_action!==undefined; switch(command[0]){ case "restart": { if (restarting==true){ logWarning('A "restart" command is being triggered in the "run_rules_on_level_start" section of level creation, which would cause an infinite loop if it was actually triggered, but it\'s being ignored, so it\'s not.'); break; } winning=false; timer=0; titleScreen=true; tryPlayTitleSound(); textMode=true; titleSelection=0; titleSelected=false; quittingMessageScreen=false; quittingTitleScreen=false; messageselected=false; titleMode = 0; if (showContinueOptionOnTitleScreen()) { titleMode=1; } if (state.metadata.skip_title_screen!==undefined) { consolePrint("skip_title_screen enabled, proceeding to do exactly as it says on the tin.") if(state.metadata["continue_is_level_select"] !== undefined) { gotoLevelSelectScreen(); } else if(titleMode <= 1) { nextLevel(); } else if(titleMode == 2) { gotoLevel(titleSelection); } } else { generateTitleScreen(); } break; } case "rebuild": { //do nothing break; } case "loadFirstNonMessageLevel":{ for (var i=0;i=0;i--) { var level= state.levels[i]; if(level.lineNumber<=targetLine+1) { curlevel=i; curlevelTarget=null; winning=false; timer=0; titleScreen=false; textMode=false; //titleSelection=showContinueOptionOnTitleScreen()?1:0; titleSelected=false; quittingMessageScreen=false; quittingTitleScreen=false; messageselected=false; titleMode = 0; loadLevelFromState(state,i); break; } } break; } } if(command[0] !== "rebuild") { clearInputHistory(); } canvasResize(); if (state.sounds.length==0&&state.metadata.youtube==null){ killAudioButton(); } else { showAudioButton(); } } function RebuildLevelArrays() { level.movements = new Int32Array(level.n_tiles * STRIDE_MOV); level.rigidMovementAppliedMask = []; level.rigidGroupIndexMask = []; level.rowCellContents = []; level.rowCellContents_Movements = []; level.colCellContents = []; level.colCellContents_Movements = []; level.mapCellContents = new BitVec(STRIDE_OBJ); level.mapCellContents_Movements = new BitVec(STRIDE_MOV); _movementVecs = [new BitVec(STRIDE_MOV),new BitVec(STRIDE_MOV),new BitVec(STRIDE_MOV)]; _o1 = new BitVec(STRIDE_OBJ); _o2 = new BitVec(STRIDE_OBJ); _o2_5 = new BitVec(STRIDE_OBJ); _o3 = new BitVec(STRIDE_OBJ); _o4 = new BitVec(STRIDE_OBJ); _o5 = new BitVec(STRIDE_OBJ); _o6 = new BitVec(STRIDE_OBJ); _o7 = new BitVec(STRIDE_OBJ); _o8 = new BitVec(STRIDE_OBJ); _o9 = new BitVec(STRIDE_OBJ); _o10 = new BitVec(STRIDE_OBJ); _o11 = new BitVec(STRIDE_OBJ); _o12 = new BitVec(STRIDE_OBJ); _m1 = new BitVec(STRIDE_MOV); _m2 = new BitVec(STRIDE_MOV); _m3 = new BitVec(STRIDE_MOV); for (var i=0;i=result.length){ if (result.length2 && !backups[backups.length-1].hasOwnProperty("diff")){ backups[backups.length-3]=consolidateDiff(backups[backups.length-3],backups[backups.length-2]); } } function DoRestart(force) { if (restarting===true){ return; } if (force!==true && ('norestart' in state.metadata)) { return; } restarting=true; if (force!==true) { addUndoState(backupLevel()); } if (verbose_logging) { consolePrint("--- restarting ---",true); } restoreLevel(restartTarget, true); tryPlayRestartSound(); document.dispatchEvent(new CustomEvent("psplusLevelRestarted", {detail: curlevel})); if ('run_rules_on_level_start' in state.metadata) { processInput(-1,true); } twiddleMetadataExtras(); level.commandQueue=[]; level.commandQueueSourceRules=[]; restarting=false; } function backupDiffers(){ if (backups.length==0){ return true; } var bak = backups[backups.length-1]; if (bak.hasOwnProperty("diff")){ return bak.dat.length!==0 && bak.dat[1]!==0;//if it's empty or if it's all 0s } else { for (var i=0;i0) { var torestore = backups[backups.length-1]; restoreLevel(torestore, null, resetTween); backups = backups.splice(0,backups.length-1); if (! force) { tryPlayUndoSound(); } } } function getPlayerPositions() { var result=[]; var playerMask = state.playerMask; for (i=0;i0) || (ty===0&&dy<0) || (ty===maxy&&dy>0)) { return false; } var targetIndex = (positionIndex+delta[1]+delta[0]*level.height); var layerMask = state.layerMasks[layer]; var targetMask = level.getCellInto(targetIndex,_o7); var sourceMask = level.getCellInto(positionIndex,_o8); if (layerMask.anyBitsInCommon(targetMask) && (dirMask!=16)) { return false; } for (var i=0;i>5] |= 1 << (ind & 31); } BitVec.prototype.ibitclear = function(ind) { this.data[ind>>5] &= ~(1 << (ind & 31)); } BitVec.prototype.get = function(ind) { return (this.data[ind>>5] & 1 << (ind & 31)) !== 0; } BitVec.prototype.getshiftor = function(mask, shift) { var toshift = shift & 31; var ret = this.data[shift>>5] >>> (toshift); if (toshift) { ret |= this.data[(shift>>5)+1] << (32 - toshift); } return ret & mask; } BitVec.prototype.ishiftor = function(mask, shift) { var toshift = shift&31; var low = mask << toshift; this.data[shift>>5] |= low; if (toshift) { var high = mask >> (32 - toshift); this.data[(shift>>5)+1] |= high; } } BitVec.prototype.ishiftclear = function(mask, shift) { var toshift = shift & 31; var low = mask << toshift; this.data[shift>>5] &= ~low; if (toshift){ var high = mask >> (32 - (shift & 31)); this.data[(shift>>5)+1] &= ~high; } } BitVec.prototype.equals = function(other) { if (this.data.length !== other.data.length) return false; for (var i = 0; i < this.data.length; ++i) { if (this.data[i] !== other.data[i]) return false; } return true; } BitVec.prototype.setZero = function() { for (var i = 0; i < this.data.length; ++i) { this.data[i]=0; } } BitVec.prototype.iszero = function() { for (var i = 0; i < this.data.length; ++i) { if (this.data[i]) return false; } return true; } BitVec.prototype.bitsSetInArray = function(arr) { for (var i = 0; i < this.data.length; ++i) { if ((this.data[i] & arr[i]) !== this.data[i]) { return false; } } return true; } BitVec.prototype.bitsClearInArray = function(arr) { for (var i = 0; i < this.data.length; ++i) { if (this.data[i] & arr[i]) { return false; } } return true; } BitVec.prototype.anyBitsInCommon = function(other) { return !this.bitsClearInArray(other.data); } function Rule(rule) { this.direction = rule[0]; /* direction rule scans in */ this.patterns = rule[1]; /* lists of CellPatterns to match */ this.hasReplacements = rule[2]; this.lineNumber = rule[3]; /* rule source for debugging */ this.isEllipsis = rule[4]; /* true if pattern has ellipsis */ this.groupNumber = rule[5]; /* execution group number of rule */ this.isRigid = rule[6]; this.commands = rule[7]; /* cancel, restart, sfx, etc */ this.isRandom = rule[8]; this.cellRowMasks = rule[9]; this.cellRowMasks_Movements = rule[10]; this.isGlobal = rule[11]; this.ruleMask = this.cellRowMasks.reduce( (acc, m) => { acc.ior(m); return acc }, new BitVec(STRIDE_OBJ) ); /*I tried out doing a ruleMask_movements as well along the lines of the above, but it didn't help at all - I guess because almost every tick there are movements somewhere on the board - move filtering works well at a row/col level, but is pretty useless (or worse than useless) on a boardwide level*/ this.cellRowMatches = []; for (var i=0;i=cellRow.length) { return true; //result.push([i,k]); } } break; } else if (!cellPattern.matches(targetIndex)) { break; } } } return false; } */ //say cellRow has length 3 /* CellRow Matches can be specialized to look something like: function cellRowMatchesFunctionGenerate(direction,cellRow,i) { var delta = dirMasksDelta[direction]; var d = delta[1]+delta[0]*level.height; return cellRow[0].matches(i)&&cellRow[1].matches((i+d)%level.n_tiles)&&cellRow[2].matches((i+2*d)%level.n_tiles); } */ /* function DoesCellRowMatch(direction,cellRow,i,k) { var cellPattern = cellRow[0]; if (cellPattern.matches(i)) { var delta = dirMasksDelta[direction]; var d0 = delta[0]*level.height; var d1 = delta[1]; var cr_l = cellRow.length; var targetIndex = i; for (var j=1;j=cellRow.length) { return true; } } return false; } */ function matchCellRow(direction, cellRowMatch, cellRow, cellRowMask,cellRowMask_Movements,d, isGlobal) { var result=[]; if ((!cellRowMask.bitsSetInArray(level.mapCellContents.data))|| (!cellRowMask_Movements.bitsSetInArray(level.mapCellContents_Movements.data))) { return result; } if(isGlobal || state.metadata.local_radius === undefined){ xmin=0; xmax=level.width; ymin=0; ymax=level.height; } else{ var localradius = parseInt(state.metadata.local_radius); xmin=Math.max(0, (playerPositions[0]/level.height|0) - localradius); xmax=Math.min(level.width, (playerPositions[0]/level.height|0) + localradius +1); ymin=Math.max(0, playerPositions[0]%level.height - localradius); ymax=Math.min(level.height, playerPositions[0]%level.height + localradius+1); } var len=cellRow.length; switch(direction) { case 1://up { ymin+=(len-1); break; } case 2: //down { ymax-=(len-1); break; } case 4: //left { xmin+=(len-1); break; } case 8: //right { xmax-=(len-1); break; } default: { window.console.log("EEEP "+direction); } } var horizontal=direction>2; if (horizontal) { for (var y=ymin;y2; if (horizontal) { for (var y=ymin;y1){ return true; } } } return false; } Rule.prototype.applyAt = function(level,tuple,check,delta) { var rule = this; //have to double check they apply //(cf test ellipsis bug: rule matches two candidates, first replacement invalidates second) if (check) { for (var cellRowIndex=0; cellRowIndexRule ${rule.lineNumber} ${ruleDirection} applied.`; consolePrint(logString,false,rule.lineNumber,inspect_ID); } return result; }; Rule.prototype.tryApply = function(level) { const delta = level.delta_index(this.direction); //get all cellrow matches var matches = this.findMatches(); if (matches.length===0) { return false; } var result=false; if (this.hasReplacements) { var tuples = generateTuples(matches); for (var tupleIndex=0;tupleIndex0; var success = this.applyAt(level,tuple,shouldCheck,delta); result = success || result; } } if (matches.length>0) { this.queueCommands(); } return result; }; Rule.prototype.queueCommands = function() { var commands = this.commands; if (commands.length==0){ return; } //commandQueue is an array of strings, message.commands is an array of array of strings (For messagetext parameter), so I search through them differently var preexisting_cancel=level.commandQueue.indexOf("cancel")>=0; var preexisting_restart=level.commandQueue.indexOf("restart")>=0; var currule_cancel = false; var currule_restart = false; for (var i=0;i restart > everything else //if cancel is the queue from other rules, ignore everything if (preexisting_cancel){ return; } //if restart is in the queue from other rules, only apply if there's a cancel present here if (preexisting_restart && !currule_cancel){ return; } //if you are writing a cancel or restart, clear the current queue if (currule_cancel || currule_restart){ level.commandQueue=[]; level.commandQueueSourceRules=[]; messagetext=""; } for(var i=0;i=0) { continue; } level.commandQueue.push(command[0]); level.commandQueueSourceRules.push(this); if (verbose_logging){ var lineNumber = this.lineNumber; var ruleDirection = dirMaskName[this.direction]; var logString = 'Rule ' + lineNumber.toString() + ' triggers command '+command[0]+'.'; consolePrint(logString,false,lineNumber,null); } if (command[0]==='message') { messagetext=command[1]; } if (command[0]==='goto') { gotoLevel(command[1]); } if (state.metadata.runtime_metadata_twiddling !== undefined && twiddleable_params.includes(command[0])) { value = command[1]; if (value == "wipe") { delete state.metadata[command[0]]; //value = undefined; value = null; } else if (value == "default") { value = deepClone(state.default_metadata[command[0]]); } if (value != null) { state.metadata[command[0]] = value; } if (command[0] === "zoomscreen" || command[0] === "flickscreen") { twiddleMetaData(state, true); canvasResize(); } if (command[0] === "smoothscreen") { if (value !== undefined) { twiddleMetaData(state, true); initSmoothCamera() } else { smoothscreen = false; } canvasResize(); } twiddleMetadataExtras() if (state.metadata.runtime_metadata_twiddling_debug !== undefined) { var log = "Metadata twiddled: Flag "+command[0] + " set to " + value; if (value != command[1]) { log += " ("+command[1]+")" } consolePrintFromRule(log,this,true); } } } }; function twiddleMetadataExtras() { if (state.metadata.realtime_interval!==undefined) { autotick=0; autotickinterval=state.metadata.realtime_interval*1000; } else { autotick=0; autotickinterval=0; } if (state.metadata.again_interval!==undefined) { againinterval=state.metadata.again_interval*1000; } else { againinterval=150; } if (state.metadata.tween_length!==undefined) { tweeninterval=Math.max(state.metadata.tween_length*1000, 0); } else { tweeninterval = 0; } if (state.metadata.key_repeat_interval!==undefined) { repeatinterval=state.metadata.key_repeat_interval*1000; } else { repeatinterval=150; } if ('background_color' in state.metadata) { state.bgcolor=colorToHex(colorPalette,state.metadata.background_color); } else { state.bgcolor="#000000"; } if ('text_color' in state.metadata) { state.fgcolor=colorToHex(colorPalette,state.metadata.text_color); } else { state.fgcolor="#FFFFFF"; } } function showTempMessage() { if (solving) {return;} keybuffer=[]; textMode=true; titleScreen=false; quittingMessageScreen=false; messageselected=false; ignoreNotJustPressedAction=true; tryPlayShowMessageSound(); drawMessageScreen(); canvasResize(); } function processOutputCommands(commands) { for (var i=0;i0) { var tuples = generateTuples(ruleMatches); for (var j=0;j200) { logErrorCacheable("Got caught looping lots in a rule group :O",ruleGroup[0].lineNumber,true); break; } propagated=false; for (var ruleIndex=0;ruleIndex0; var loopCount = 0; for (var ruleGroupIndex=startRuleGroupindex;ruleGroupIndex 200) { var ruleGroup=rules[ruleGroupIndex]; logErrorCacheable("got caught in an endless startloop...endloop vortex, escaping!", ruleGroup[0].lineNumber,true); break; } if (verbose_logging){ debugger_turnIndex++; addToDebugTimeline(level,-2);//pre-movement-applied debug state } } else { ruleGroupIndex++; if (ruleGroupIndex===rules.length) { if (loopPropagated && loopPoint[ruleGroupIndex]!==undefined) { ruleGroupIndex = loopPoint[ruleGroupIndex]; loopPropagated=false; loopCount++; if (loopCount > 200) { var ruleGroup=rules[ruleGroupIndex]; logErrorCacheable("got caught in an endless startloop...endloop vortex, escaping!", ruleGroup[0].lineNumber,true); break; } } } if (verbose_logging){ debugger_turnIndex++; addToDebugTimeline(level,-2);//pre-movement-applied debug state } } } } //if this returns!=null, need to go back and reprocess function resolveMovements(level, bannedGroup){ var moved=true; while(moved){ moved=false; for (var i=0;i=0 && dir<=4) { switch(dir){ case 0://up { dir=parseInt('00001', 2);; break; } case 1://left { dir=parseInt('00100', 2);; break; } case 2://down { dir=parseInt('00010', 2);; break; } case 3://right { dir=parseInt('01000', 2);; break; } case 4://action { dir=parseInt('10000', 2);; break; } } playerPositions = startMovement(dir); } if (verbose_logging) { consolePrint('Applying rules'); var inspect_ID = addToDebugTimeline(level,-1); if (dir===-1) { consolePrint(`Turn starts with no input.`,false,null,inspect_ID) } else { // consolePrint('======================='); consolePrint(`Turn starts with input of ${['up','left','down','right','action','mouse'][startDir]}.`,false,null,inspect_ID); } } bannedGroup = []; level.commandQueue=[]; level.commandQueueSourceRules=[]; var startRuleGroupIndex=0; var rigidloop=false; const startState = { objects: new Int32Array(level.objects), movements: new Int32Array(level.movements), rigidGroupIndexMask: level.rigidGroupIndexMask.concat([]), rigidMovementAppliedMask: level.rigidMovementAppliedMask.concat([]), commandQueue: [], commandQueueSourceRules: [] } sfxCreateMask.setZero(); sfxDestroyMask.setZero(); seedsToPlay_CanMove=[]; seedsToPlay_CantMove=[]; calculateRowColMasks(); var i=0; do { //not particularly elegant, but it'll do for now - should copy the world state and check //after each iteration rigidloop=false; i++; applyRules(state.rules, state.loopPoint, startRuleGroupIndex, bannedGroup); var shouldUndo = resolveMovements(level,bannedGroup); if (shouldUndo) { rigidloop=true; { // trackback consolePrint("Rigid movement application failed. Rolling back...") //don't need to concat or anythign here, once something is restored it won't be used again. level.objects = new Int32Array(startState.objects) level.movements = new Int32Array(startState.movements) level.rigidGroupIndexMask = startState.rigidGroupIndexMask.concat([]) level.rigidMovementAppliedMask = startState.rigidMovementAppliedMask.concat([]) // TODO: shouldn't we also save/restore the level data computed by level.calculateRowColMasks() ? level.commandQueue = startState.commandQueue.concat([]) level.commandQueueSourceRules = startState.commandQueueSourceRules.concat([]) sfxCreateMask.setZero() sfxDestroyMask.setZero() // TODO: should } if (verbose_logging && rigidloop && i>0){ consolePrint('Relooping through rules because of rigid.'); debugger_turnIndex++; addToDebugTimeline(level,-2);//pre-movement-applied debug state } startRuleGroupIndex=0;//rigidGroupUndoDat.ruleGroupIndex+1; } else { if (verbose_logging){ var eof_idx = debug_visualisation_array[debugger_turnIndex].length+1;//just need some number greater than any rule group var inspect_ID = addToDebugTimeline(level,eof_idx); consolePrint(`Processed movements.`,false,null,inspect_ID); if (state.lateRules.length>0){ debugger_turnIndex++; addToDebugTimeline(level,-2);//pre-movement-applied debug state consolePrint('Applying late rules'); } } applyRules(state.lateRules, state.lateLoopPoint, 0); startRuleGroupIndex=0; } } while (i < 250 && rigidloop); if (i>=250) { consolePrint("looped through 250 times, gave up. Too many loops!"); applyRules(state.lateRules, state.lateLoopPoint, 0); startRuleGroupIndex=0; backups.push(bak); DoUndo(true,false); return false; } /// Taken from zarawesome, thank you :) if (level.commandQueue.indexOf('undo')>=0) { if (verbose_logging) { consoleCacheDump(); consolePrint('UNDO command executed, undoing turn.',true); } messagetext = ""; DoUndo(true,false); return true; } if (playerPositionsAtTurnStart.length>0 && state.metadata.require_player_movement!==undefined && dir >= 0) { var somemoved=false; for (var i=0;i=0) { if (verbose_logging) { consoleCacheDump(); var r = level.commandQueueSourceRules[level.commandQueue.indexOf('cancel')]; consolePrintFromRule('CANCEL command executed, cancelling turn.',r,true); } processOutputCommands(level.commandQueue); addUndoState(bak); DoUndo(true,false); tryPlayCancelSound(); return false; } if (level.commandQueue.indexOf('restart')>=0) { if (verbose_logging) { var r = level.commandQueueSourceRules[level.commandQueue.indexOf('restart')]; consolePrintFromRule('RESTART command executed, reverting to restart state.',r); consoleCacheDump(); } processOutputCommands(level.commandQueue); addUndoState(bak); DoRestart(true); return true; } if (level.commandQueue.indexOf('quit')>=0 && !solving) { if (verbose_logging) { var r = level.commandQueueSourceRules[level.commandQueue.indexOf('restart')]; consolePrintFromRule('QUIT command executed, exiting level.',r); consoleCacheDump(); } if (state.metadata.level_select !== undefined) { gotoLevelSelectScreen(); } else { goToTitleScreen(); } messagetext = ""; canvasResize(); return true; } if (dontModify && level.commandQueue.indexOf('win')>=0) { return true; } var save_backup = true; if(!winning && level.commandQueue.indexOf('nosave')>=0) { if (verbose_logging) { var r = level.commandQueueSourceRules[level.commandQueue.indexOf('nosave')]; consolePrintFromRule('NOSAVE command executed, not storing current state to undo queue.',r); } save_backup = false; } var modified=false; for (var i=0;i=0) { return true; } if (dontModify) { if (verbose_logging) { consoleCacheDump(); } return false; } for (var i=0;i=0) { if (verbose_logging) { var r = level.commandQueueSourceRules[level.commandQueue.indexOf('checkpoint')]; consolePrintFromRule('CHECKPOINT command executed, saving current state to the restart state.',r); } restartTarget=level4Serialization(); hasUsedCheckpoint=true; var backupStr = JSON.stringify(restartTarget); storage_set(document.URL+'_checkpoint',backupStr); storage_set(document.URL,curlevel); } if (level.commandQueue.indexOf('again')>=0 && modified) { var r = level.commandQueueSourceRules[level.commandQueue.indexOf('again')]; //first have to verify that something's changed var old_verbose_logging=verbose_logging; var oldmessagetext = messagetext; verbose_logging=false; if (processInput(-1,true,true)) { verbose_logging=old_verbose_logging; if (verbose_logging) { consolePrintFromRule('AGAIN command executed, with changes detected - will execute another turn.',r); } againing=true; timer=0; } else { verbose_logging=old_verbose_logging; if (verbose_logging) { consolePrintFromRule('AGAIN command not executed, it wouldn\'t make any changes.',r); } } verbose_logging=old_verbose_logging; messagetext = oldmessagetext; } } if (verbose_logging) { consolePrint(`Turn complete`); } currentMovedEntities = newMovedEntities; tweentimer = 0; level.commandQueue=[]; level.commandQueueSourceRules=[]; } if (verbose_logging) { consoleCacheDump(); } if (winning) { againing=false; } return modified; } function checkWin(dontDoWin) { if (levelEditorOpened) { dontDoWin=true; } if (level.commandQueue.indexOf('win')>=0) { if (runrulesonlevelstart_phase){ consolePrint("Win Condition Satisfied (However this is in the run_rules_on_level_start rule pass, so I'm going to ignore it for you. Why would you want to complete a level before it's already started?!)"); } else { if (!solving) { consolePrint("Win Condition Satisfied"); } } if(!dontDoWin){ DoWin(); } return; } var won= false; if (state.winconditions.length>0) { var passed=true; for (var wcIndex=0;wcIndexstate.levels.length) ){ curlevel=state.levels.length-1; } ignoreNotJustPressedAction=true; if (titleScreen && titleMode <= 1) { if(isContinueOptionSelected()) { // continue loadLevelFromStateOrTarget(); } else if(isNewGameOptionSelected()) { // new game curlevel=0; curlevelTarget=null; if (state.metadata.level_select === undefined) { clearLocalStorage(); } loadLevelFromStateOrTarget(); } else if(isLevelSelectOptionSelected()) { // level select titleSelection = 0; gotoLevelSelectScreen(); } else { // settings // TODO } } else { if (hasUsedCheckpoint){ curlevelTarget=null; hasUsedCheckpoint=false; } if (curlevel<(state.levels.length-1)) { var skip = false; var curSection = state.levels[Number(curlevel)].section; var nextSection = state.levels[Number(curlevel)+1].section; if(nextSection != curSection) { setSectionSolved(state.levels[Number(curlevel)].section); if(solvedSections.length == state.sections.length && state.winSection != undefined) { curlevel = state.winSection.firstLevel - 1; // it's gonna be increased to match few lines below } else if (nextSection == "__WIN__") { gotoLevelSelectScreen(); skip = true; } } if(!skip) { curlevel++; curlevelTarget=null; textMode=false; titleScreen=false; quittingMessageScreen=false; messageselected=false; loadLevelFromStateOrTarget(); } } else { if(solvedSections.length == state.sections.length) { if(state.metadata["level_select"] === undefined) { // solved all try{ storage_remove(document.URL); storage_remove(document.URL+'_checkpoint'); } catch(ex){ } curlevel=0; curlevelTarget=null; goToTitleScreen(); } else { gotoLevelSelectScreen(); } tryPlayEndGameSound(); } else { if(state.levels[Number(curlevel)].section != null) { setSectionSolved(state.levels[Number(curlevel)].section); } gotoLevelSelectScreen(); } } //continue existing game } updateLocalStorage(); resetFlickDat(); canvasResize(); } function loadLevelFromStateOrTarget() { if (curlevelTarget!==null){ loadLevelFromStateTarget(state,curlevel,curlevelTarget); } else { loadLevelFromState(state,curlevel); } } function goToTitleScreen(){ againing=false; messagetext=""; titleScreen=true; textMode=true; hoverSelection=-1; doSetupTitleScreenLevelContinue(); //titleSelection=showContinueOptionOnTitleScreen()?1:0; state.metadata = deepClone(state.default_metadata); twiddleMetadataExtras(); if (canvas!==null){//otherwise triggers error in cat bastard test regenSpriteImages(); } generateTitleScreen(); } function resetFlickDat() { if (state!==undefined && state.metadata.flickscreen!==undefined){ oldflickscreendat=[0,0,Math.min(state.metadata.flickscreen[0],level.width),Math.min(state.metadata.flickscreen[1],level.height)]; } } function updateLocalStorage() { try { storage_set(document.URL,curlevel); if (curlevelTarget!==null){ restartTarget=level4Serialization(); var backupStr = JSON.stringify(restartTarget); storage_set(document.URL+'_checkpoint',backupStr); } else { storage_remove(document.URL+"_checkpoint"); } } catch (ex) { } } function setSectionSolved(section) { if(section == null || section == undefined) { return; } if(section.name == "__WIN__") { return; } if(solvedSections.indexOf(section) >= 0) { return; } try { if(!!window.localStorage) { solvedSections.push(section); localStorage.setItem(document.URL + "_sections", JSON.stringify(solvedSections)); } } catch(ex) { } } function clearLocalStorage() { curlevel = 0; curlevelTarget = null; solvedSections = []; try { if (!!window.localStorage) { localStorage.removeItem(document.URL); localStorage.removeItem(document.URL+'_checkpoint'); localStorage.removeItem(document.URL+'_sections'); } } catch(ex){ } } var cameraPositionTarget = null; var cameraPosition = { x: 0, y: 0 }; function initSmoothCamera() { if (state===undefined || state.metadata.smoothscreen===undefined) { return; } screenwidth=state.metadata.smoothscreen.screenSize.width; screenheight=state.metadata.smoothscreen.screenSize.height; var boundarySize = state.metadata.smoothscreen.boundarySize; var flick = state.metadata.smoothscreen.flick; var playerPositions = getPlayerPositions(); if (playerPositions.length>0) { var playerPosition = { x: (playerPositions[0]/(level.height))|0, y: (playerPositions[0]%level.height)|0 }; cameraPositionTarget = { x: flick ? getFlickCameraPosition(playerPosition.x, level.width, screenwidth, boundarySize.width) : getCameraPosition(playerPosition.x, level.width, screenwidth), y: flick ? getFlickCameraPosition(playerPosition.y, level.height, screenheight, boundarySize.height) : getCameraPosition(playerPosition.y, level.height, screenheight) }; cameraPosition.x = cameraPositionTarget.x; cameraPosition.y = cameraPositionTarget.y; } } function getCameraPosition (targetPosition, levelDimension, screenDimension) { return Math.min( Math.max(targetPosition, Math.floor(screenDimension / 2)), levelDimension - Math.ceil(screenDimension / 2) ); } function getFlickCameraPosition (targetPosition, levelDimension, screenDimension, boundaryDimension) { var flickGridOffset = (Math.floor(screenDimension / 2) - Math.floor(boundaryDimension / 2)); var flickGridPlayerPosition = targetPosition - flickGridOffset; var flickGridPlayerCell = Math.floor(flickGridPlayerPosition / boundaryDimension); var maxFlickGridCell = Math.floor((levelDimension - Math.ceil(screenDimension / 2) - Math.floor(boundaryDimension / 2) - flickGridOffset) / boundaryDimension); return Math.min(Math.max(flickGridPlayerCell, 0), maxFlickGridCell) * boundaryDimension + Math.floor(screenDimension / 2); } function updateCameraPositionTarget() { var smoothscreenConfig = state.metadata.smoothscreen; var playerPositions = getPlayerPositions(); if (!smoothscreenConfig || playerPositions.length === 0) { return } var playerPosition = { x: (playerPositions[0]/(level.height))|0, y: (playerPositions[0]%level.height)|0 }; ['x', 'y'].forEach(function (coord) { var screenDimension = coord === 'x' ? screenwidth : screenheight; var dimensionName = coord === 'x' ? 'width' : 'height'; var levelDimension = level[dimensionName]; var boundaryDimension = smoothscreenConfig.boundarySize[dimensionName]; var playerVector = playerPosition[coord] - cameraPositionTarget[coord]; var direction = Math.sign(playerVector); var boundaryVector = direction > 0 ? Math.ceil(boundaryDimension / 2) : -(Math.floor(boundaryDimension / 2) + 1); if (Math.abs(playerVector) - Math.abs(boundaryVector) >= 0) { cameraPositionTarget[coord] = smoothscreenConfig.flick ? getFlickCameraPosition(playerPosition[coord], levelDimension, screenDimension, boundaryDimension) : getCameraPosition(playerPosition[coord] - boundaryVector + direction, levelDimension, screenDimension); } }) } function IsMouseGameInputEnabled() { return "mouse_left" in state.metadata || "mouse_drag" in state.metadata || "mouse_up" in state.metadata; }