diff --git a/2016 - diamond run/images/sprites.png b/2016 - diamond run/images/sprites.png
new file mode 100644
index 0000000..66b61d3
Binary files /dev/null and b/2016 - diamond run/images/sprites.png differ
diff --git a/2016 - diamond run/images/sprites.xcf b/2016 - diamond run/images/sprites.xcf
new file mode 100644
index 0000000..b90a7e3
Binary files /dev/null and b/2016 - diamond run/images/sprites.xcf differ
diff --git a/2016 - diamond run/images/sprites32-min.png b/2016 - diamond run/images/sprites32-min.png
new file mode 100644
index 0000000..206adc8
Binary files /dev/null and b/2016 - diamond run/images/sprites32-min.png differ
diff --git a/2016 - diamond run/index.html b/2016 - diamond run/index.html
new file mode 100644
index 0000000..a636dab
--- /dev/null
+++ b/2016 - diamond run/index.html
@@ -0,0 +1,55 @@
+
+
+
+ Diamond Run
+
+
+
+
+
+
+
.
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/2016 - diamond run/js/controls.js b/2016 - diamond run/js/controls.js
new file mode 100644
index 0000000..551fcbf
--- /dev/null
+++ b/2016 - diamond run/js/controls.js
@@ -0,0 +1,196 @@
+/**
+ * @constructor
+ */
+function Controls()
+{
+ this.mouseX = 0;
+ this.mouseY = 0;
+ this.windowLayout = false; // default initialization, will be set to actual value by onLayoutChange()
+ this.mouseInPlayArea = false; // true everywhere but the control bar
+ this.mouseInBullsEye = false; // true in the top-left view, in hacking mode
+ this.keyBelowMouse = -1;
+ this.dropTargetBelowMouse = -1;
+ this.mouseLeftButton=false;
+ this.totalClear();
+}
+
+Controls.prototype = {
+
+ totalClear : function() {
+ this.menuClicked=-1;
+ this.controlUp=false;
+ this.controlDown=false;
+ this.controlEscape=false;
+ this.controlFire=false;
+ this.worldDX=0;
+ this.worldDY=0;
+ },
+
+ /**
+ * Handler for key up events
+ */
+ onKeyUp : function(event) {
+ return !this.keyControl(event, false);
+ },
+
+ /**
+ * Handler for key down events
+ */
+ onKeyDown : function(event) {
+ return !this.keyControl(event, true);
+ },
+
+ /**
+ * Delegated handler for keyboard events
+ * Records key presses and releases, for both standard keys (arrows, enter, escape)
+ * and configurable controls (through the controls menu)
+ * Returns true if the event is handled, false otherwise.
+ */
+ keyControl : function(event, value) {
+ var handled = true;
+ var key = 0;
+ if (window.event) { // IE
+ key = window.event.keyCode;
+ } else { // FF, Opera,...
+ key = event.which;
+ }
+
+ // test against static, non-redefinable keys (arrows for menu navigation, escape)
+ switch (key) {
+ case 38 : // top arrow
+ this.controlUp = value;
+ break;
+ case 40 : // down arrow
+ this.controlDown = value;
+ break;
+ case 32 : // space bar
+ case 13 : // enter
+ this.controlFire = value;
+ break;
+ case 27 : // escape
+ this.controlEscape = value;
+ break;
+
+ default :
+ handled = false;
+ }
+
+ return handled;
+ },
+
+ /**
+ * Handler for mouse up events
+ */
+ onMouseUp : function(event) {
+ // main menu : select the clicked line
+ if (this.windowLayout) {
+ this.menuClicked = Math.floor(10*this.mouseY/(this.windowLayout.playArea[0]+2*this.windowLayout.playArea[2])-3);
+ }
+
+ this.mouseLeftButton=false;
+ return true;
+ },
+
+ /**
+ * Handler for mouse down events
+ */
+ onMouseDown : function(event) {
+ this.mouseLeftButton=true;
+ return true;
+ },
+
+ /**
+ * Handler for touch start / touch enter events
+ * Call onMouseMove (pointer motion handler) first to record position as the event chain begins with onTouchStart
+ * and there is no permanent tracking of the cursor position (unlike mouse event)
+ */
+ onTouchStart : function(event)
+ {
+ this.onMouseMove(event);
+ return this.onMouseDown(event);
+ },
+
+ /**
+ * Handler for touch end / touch leave events
+ */
+ onTouchEnd : function(event)
+ {
+ event.preventDefault();
+ return this.onMouseUp(event);
+ },
+
+ /**
+ * Handler for touch move events
+ */
+ onTouchMove : function(event)
+ {
+ event.preventDefault();
+ var result = this.onMouseMove(event);
+ return result;
+ },
+
+ /**
+ * Consume a mouse click, so that the icon will not
+ * be activated again the next frame, until the user clicks again
+ * (used for double-click requirement on reset / menu icons)
+ */
+ acknowledgeMouseClick : function() {
+ this.mouseLeftButton=false;
+ this.menuClicked = -1;
+ },
+
+ /**
+ * Handler for mouse/touch move events
+ */
+ onMouseMove : function(event) {
+ var clientX = 0, clientY = 0;
+ if ('changedTouches' in event && event.changedTouches.length > 0)
+ { //touchmove event
+ clientX = event.changedTouches[0].clientX;
+ clientY = event.changedTouches[0].clientY;
+ }
+ if ('clientX' in event && 'clientY' in event)
+ { // mousemove event
+ clientX = event.clientX;
+ clientY = event.clientY;
+ }
+ if (this.windowLayout) // has been set
+ {
+ this.mouseX=clientX;
+ this.mouseY=clientY;
+ this.worldDX=(this.mouseX-this.windowLayout.playArea[1]) / this.windowLayout.playArea[0];
+ this.worldDY=-(this.mouseY-this.windowLayout.playArea[2]) / this.windowLayout.playArea[0];
+
+ this.keyBelowMouse = -1;
+ this.dropTargetBelowMouse = -1;
+ this.mouseInPlayArea = this.mouseY < this.windowLayout.controlBar[0];
+ this.mouseInBullsEye = this.mouseY < 4*this.windowLayout.playArea[0] && this.mouseX < 4*this.windowLayout.playArea[0];
+ if (!this.mouseInPlayArea) {
+ this.keyBelowMouse = Math.floor(this.mouseX / this.windowLayout.playArea[0]);
+ } else {
+ // in game : check if above a drop point
+ var dz = .5 * this.windowLayout.hackingArea[0];
+ for (var i=0;i<8;++i) {
+ var dx = this.mouseX - this.windowLayout.hackingArea[i*2+1];
+ var dy = this.mouseY - this.windowLayout.hackingArea[i*2+2];
+ if (dx*dx+dy*dy <= dz*dz) {
+ this.dropTargetBelowMouse = i;
+ }
+ }
+ }
+ }
+ return true;
+ },
+
+
+
+ /**
+ * Second-level handler for window resize / layout change. Called by Game.
+ * Keeps track of the new layout with the click areas
+ * @param windowLayout Object containing the layout of the different panels and toolbars
+ */
+ onLayoutChange : function(windowLayout) {
+ this.windowLayout = windowLayout;
+ }
+
+}
diff --git a/2016 - diamond run/js/game.js b/2016 - diamond run/js/game.js
new file mode 100644
index 0000000..f15c28b
--- /dev/null
+++ b/2016 - diamond run/js/game.js
@@ -0,0 +1,170 @@
+/**
+ * Main application class
+ *
+ * The state member variable defines the current screen :
+ * 0 : main menu
+ * 1 : tutorial
+ * 2 : game in progress
+ * 3 : tutorial ends
+ *
+ * @constructor
+ */
+function Game(controls, savedData)
+{
+ this.controls = controls;
+ this.mainMenu = new MenuDriver(3, controls);
+
+ this.persistentData = {
+ };
+
+ this.world = new World(controls);
+ if (savedData.hasOwnProperty('gameProgress')) {
+ this.world.loadGame(savedData.gameProgress);
+ }
+ this.mainMenu.setMinLine(this.world.gameInProgress ? 0 : 1);
+ this.world.addSaveGameListener(this);
+ this.state = -1;
+}
+
+Game.prototype = {
+
+
+ /**
+ * Launch the game : set the state to main menu and start the timers (main loop and rendering loop)
+ */
+ launch : function() {
+ this.changeState(0); // initialize and show main menu
+ this.intervalId = setInterval (function() { game.mainLoop(); }, 40);
+ //requestAnimationFrame = requestAnimationFrame || webkitRequestAnimationFrame;
+ requestAnimationFrame(function() {game.renderLoop();});
+ },
+
+
+ /**
+ * Change the current state to the new one
+ * Set the transition timer to the default value (20)
+ * Reinitialize menus (selected item, keypresses)
+ */
+ changeState : function(newState) {
+ this.controls.totalClear(); // do not forward mouse or keyboard actions to the new state
+
+ if (newState == 0) { // entering menu
+ this.mainMenu.initialize();
+ this.mainMenu.setMinLine(this.world.gameInProgress ? 0 : 1);
+ if (!this.world.gameInProgress) {
+ // enter the menu after ending a game : clear all progress in saved game (local storage)
+ delete this.persistentData.gameProgress;
+ this.storeData();
+ }
+ }
+ /*
+ if (newState == 1) { // entering tutorial
+ this.world.startTutorial();
+ }*/
+ this.state = newState;
+
+ },
+
+
+ /**
+ * Main loop, actions only, no rendering
+ * Performs all the model edition + controls effect
+ */
+ mainLoop : function() {
+ if (this.state == 0) // main menu
+ {
+ this.mainMenu.processEvents();
+ if (this.mainMenu.done) {
+ switch(this.mainMenu.selectedLine) {
+ case 1 :// new game
+ this.world.startNewGame();
+ // no break : we start the game by changing state
+ case 0 : // resume game
+ this.changeState(2);
+ break;
+ /*
+ case 2 : // tutorial
+ this.changeState(1);
+ break;
+ */
+ case 2 : // save and quit
+ this.world.saveGame();
+ //window.close();
+ // already saved : quit
+ break;
+ }
+ }
+ }
+
+
+
+ if (this.state == 1 || this.state == 2) {
+ this.world.processControls();
+
+ if (this.controls.controlEscape) {
+ // escape pressed : invoke main menu
+ this.changeState(0);
+ }
+
+ this.world.animateItems();
+
+ if (!this.world.gameInProgress) {
+ // game ended, won or lost : return to main menu
+ this.changeState(0);
+ }
+
+ }
+
+ },
+
+ /**
+ * Performs all the rendering (view) with no alteration of the model
+ * + controls related to the view only
+ */
+ renderLoop : function() {
+
+ this.renderer.drawMessage(this.state>0);
+ this.renderer.drawMain();
+ if (this.state == 0) {
+ this.renderer.drawMainMenu(this.mainMenu);
+ }
+ requestAnimationFrame(function() {game.renderLoop();});
+ },
+
+ /**
+ * Define the renderer in charge (one does both overlay canvas and scenery canvas)
+ */
+ setRenderer : function(renderer) {
+ this.renderer = renderer;
+ },
+
+ /**
+ * Private method to synchronize local storage with current data
+ */
+ storeData : function() {
+ localStorage.setItem("GlitchData", JSON.stringify(this.persistentData));
+ },
+
+
+ /**
+ * Called when a manual or auto save of the game in progress is requested
+ */
+ notifySave : function(posX, posY, floor, keysAcquired, doors, ground, timer) {
+ this.persistentData.gameProgress = {
+ posX : posX,
+ posY : posY,
+ floor : floor,
+ keysAcquired : keysAcquired,
+ doors : doors,
+ ground : ground,
+ timer : timer};
+ this.storeData();
+ },
+
+ /**
+ * Called when the window is resized and the window layout changes
+ */
+ layoutChanged : function(windowLayout) {
+ this.controls.onLayoutChange(windowLayout);
+ }
+}
diff --git a/2016 - diamond run/js/levelLoader.js b/2016 - diamond run/js/levelLoader.js
new file mode 100644
index 0000000..bbda2e2
--- /dev/null
+++ b/2016 - diamond run/js/levelLoader.js
@@ -0,0 +1,680 @@
+/**
+ * @constructor
+ */
+
+function LevelLoader() {
+
+}
+
+LevelLoader.prototype = {
+
+ loadLevel : function(floors, levelWidth, levelHeight, ground, wallsH, wallsV, roomNumber, terminals){
+ var data = [0, 0, 14, 4, 2, // rooms
+ 0, 0, 12, 4, 2,
+ 0, 0, 8, 2, 4,
+ 0, 4, 12, 10, 4,
+ 0, 2, 8, 6, 4,
+ 0, 14, 10, 2, 4,
+ 0, 8, 10, 6, 2,
+ 0, 8, 8, 8, 2,
+ 0, 0, 0, 4, 8,
+ 0, 4, 5, 6, 3,
+ 0, 10, 5, 3, 3,
+ 0, 13, 5, 3, 3,
+ 0, 4, 0, 12, 5,
+ 1, 0, 0, 3, 3,
+ 1, 3, 0, 3, 3,
+ 1, 6, 0, 7, 3,
+ 1, 13, 0, 3, 3,
+ 1, 0, 3, 6, 2,
+ 1, 6, 3, 7, 2,
+ 1, 13, 3, 3, 2,
+ 1, 0, 5, 10, 3,
+ 1, 10, 5, 3, 3,
+ 1, 13, 5, 3, 3,
+ 1, 0, 8, 2, 8,
+ 1, 2, 8, 7, 2,
+ 1, 2, 10, 3, 5,
+ 1, 5, 10, 2, 5,
+ 1, 7, 10, 2, 3,
+ 1, 7, 13, 2, 2,
+ 1, 2, 15, 8, 1,
+ 1, 9, 8, 1, 7,
+ 1, 10, 9, 2, 4,
+ 1, 12, 8, 2, 6,
+ 1, 14, 10, 2, 4,
+ 1, 10, 14, 6, 2,
+ 2, 0, 0, 3, 3,
+ 2, 3, 0, 4, 3,
+ 2, 7, 0, 3, 3,
+ 2, 10, 0, 3, 3,
+ 2, 13, 0, 3, 3,
+ 2, 0, 3, 3, 1,
+ 2, 3, 3, 4, 2,
+ 2, 7, 3, 3, 4,
+ 2, 10, 3, 2, 4,
+ 2, 12, 3, 2, 7,
+ 2, 14, 3, 2, 7,
+ 2, 0, 4, 1, 5,
+ 2, 1, 4, 2, 5,
+ 2, 3, 5, 4, 4,
+ 2, 0, 9, 7, 1,
+ 2, 0, 10, 3, 3,
+ 2, 0, 13, 3, 3,
+ 2, 3, 10, 4, 6,
+ 2, 7, 7, 1, 9,
+ 2, 8, 7, 2, 2,
+ 2, 10, 9, 2, 4,
+ 2, 8, 9, 2, 4,
+ 2, 8, 13, 4, 1,
+ 2, 8, 14, 4, 2,
+ 2, 12, 10, 2, 4,
+ 2, 14, 10, 2, 4,
+ 2, 12, 14, 2, 2,
+ 2, 14, 14, 2, 2,
+ 2, 10, 7, 2, 2,
+ 0, 14, 14, 2, 2,
+ 1, 14, 8, 2, 2,
+ 1, 10, 13, 2, 1,
+ 1, 10, 8, 2, 1,
+ 3, 0, 0, 2, 2,
+ 3, 2, 0, 2, 2,
+ 3, 0, 2, 2, 2,
+ 3, 2, 2, 2, 2,
+ 3, 0, 4, 2, 3,
+ 3, 2, 4, 2, 3,
+ 3, 4, 0, 3, 3,
+ 3, 7, 0, 3, 3,
+ 3, 0, 7, 3, 3,
+ 3, 0, 10, 3, 3,
+ 3, 0, 13, 3, 3,
+ 3, 4, 3, 4, 2,
+ 3, 4, 5, 2, 2,
+ 3, 10, 0, 4, 2,
+ 3, 14, 0, 2, 4,
+ 3, 3, 7, 3, 6,
+ 3, 6, 5, 2, 8,
+ 3, 8, 3, 2, 5,
+ 3, 10, 2, 3, 6,
+ 3, 13, 2, 1, 6,
+ 3, 3, 13, 7, 3,
+ 3, 10, 14, 6, 2,
+ 3, 10, 9, 2, 5,
+ 3, 8, 9, 2, 4,
+ 3, 8, 8, 4, 1,
+ 3, 12, 8, 2, 2,
+ 3, 12, 10, 2, 4,
+ 3, 14, 4, 2, 4,
+ 3, 14, 8, 2, 2,
+ 3, 14, 10, 2, 4,
+ 4, 0, 0, 4, 4,
+ 4, 0, 4, 3, 3,
+ 4, 0, 7, 3, 3,
+ 4, 4, 0, 3, 3,
+ 4, 4, 3, 3, 1,
+ 4, 7, 0, 5, 4,
+ 4, 12, 0, 4, 3,
+ 4, 12, 3, 2, 5,
+ 4, 14, 3, 2, 1,
+ 4, 14, 4, 2, 4,
+ 4, 12, 8, 4, 8,
+ 4, 3, 13, 9, 3,
+ 4, 0, 10, 3, 6,
+ 4, 3, 4, 1, 3,
+ 4, 4, 4, 8, 1,
+ 4, 11, 5, 1, 3,
+ 4, 4, 12, 3, 1,
+ 4, 4, 5, 7, 7,
+ 4, 3, 7, 1, 3,
+ 4, 3, 10, 1, 3,
+ 4, 11, 8, 1, 2,
+ 4, 11, 10, 1, 2,
+ 4, 7, 12, 3, 1,
+ 4, 10, 12, 2, 1
+ ];
+ var levelSize = levelWidth*levelHeight;
+ this.levelWidth = levelWidth;
+ this.levelSize = levelSize;
+
+
+ this.roomCount = 1;
+ for (var i=0; ithis.minLine?-1:0)
+ +(this.controls.controlDown&&this.selectedLine=this.minLine && this.controls.menuClicked < this.lineCount) {
+ this.selectedLine = this.controls.menuClicked;
+ }
+ if ((this.controls.menuClicked>=this.minLine && this.controls.menuClicked < this.lineCount) || this.controls.controlFire) { // selection clicked/validated
+ // true to leave the menu
+ this.done = true;
+ }
+ this.controls.totalClear();
+ }
+
+
+
+}
diff --git a/2016 - diamond run/js/renderer.js b/2016 - diamond run/js/renderer.js
new file mode 100644
index 0000000..f749dc1
--- /dev/null
+++ b/2016 - diamond run/js/renderer.js
@@ -0,0 +1,697 @@
+/**
+ * @constructor
+ */
+
+function Renderer(sceneryCanvas, overlayCanvas, spriteImg, messageBox, game)
+{
+ this.game = game;
+ this.world = game.world;
+ this.sceneryCanvas = sceneryCanvas;
+ this.overlayCanvas = overlayCanvas;
+ this.sceneryContext = sceneryCanvas.getContext("2d");
+ this.overlayContext = overlayCanvas.getContext("2d");
+ this.windowLayout = {
+ playArea : [],
+ controlBar : [],
+ hackingMode : 0,
+ hackingArea : []
+ };
+ this.messageBox = messageBox;
+
+ this.mainMenuText = ["RESUME", "NEW GAME", "SAVE GAME"];
+ this.messages = [ "Here we are. Main security is disabled, the doors are on local. Only four stories between you and the diamond. Remember, you only have one hour.",
+ "The diamond is here ! But you have a feeling that something isn't right .. maybe you forgot to disable some hidden sensors and all hell will break loose once you grab it.",
+ "Oops .. you half expected a wailing siren, but instead the building went dark. The elevators seem to be out as well, you will need another way down.",
+ "You escape the tower empty handed, trying to recover your breath from the stress. Looking at the building again, you swear to come back better prepared. After all, there still is some time left.",
+ "You lost count of time ... the main security system reactivates, bolting all doors shut and trapping you inside. GAME OVER.",
+ "You close the entrance door and prompty vanish into the darkness. Slowly, you start to realize that you made it, the diamond is yours. A new life begins. YOU WIN."
+ ];
+
+ // create a mirrored version of the sprite sheet
+ this.spriteSheet = document.createElement("canvas");
+ this.spriteSheet.width=480; //spriteImg.width*2;
+ this.spriteSheet.height=288; // spriteImg.height+40;
+ var bufferContext = this.spriteSheet.getContext('2d');
+
+ bufferContext.drawImage(spriteImg, 0, 0);
+
+ // draw mirror animation for main character
+ bufferContext.scale(1, -1);
+ bufferContext.drawImage(spriteImg, 48, 192, 192, 48, 240, -240, 192, 48);
+ bufferContext.scale(1, -1);
+
+
+ this.resizeWindow(); // define the appropriate pixel zoom for the play area
+
+ this.frameCount = 0;
+ this.terminalId = 0;
+
+ this.keyColors = ["#414","#fea","#46d","#e41","#c5c","#f28"];
+}
+
+
+Renderer.prototype = {
+
+
+ /**
+ * Handler for global window resize event, also called once at init time
+ * Defines the zoom factor for the canvases and (re)aligns everything
+ * Zoom level is the largest integer so that 320 (game height) times zoom level fits vertically in window
+ */
+ resizeWindow : function() {
+
+ // Set both canvases to full window size
+ this.overlayCanvas.width = this.sceneryCanvas.width = window.innerWidth;
+ this.overlayCanvas.height = this.sceneryCanvas.height = window.innerHeight;
+ this.overlayCanvas.style.width = this.sceneryCanvas.style.width = window.innerWidth+"px";
+ this.overlayCanvas.style.height = this.sceneryCanvas.style.height = window.innerHeight+"px";
+
+ // Determine zoom level. Squares are 24x24, 32x32, 40x40 or 48x48 depending on screen size
+ var minSize = Math.min(window.innerWidth, window.innerHeight);
+ var blockSize = Math.min(48, Math.max(24, 8*Math.floor(minSize/100)));
+ var playAreaHeight = window.innerHeight-blockSize;
+ this.windowLayout.playArea = [blockSize, window.innerWidth >> 1, playAreaHeight >> 1];
+ this.windowLayout.controlBar = [playAreaHeight];
+
+ // place character at center of screen
+ this.characterPixelX = this.sceneryCanvas.width >> 1;
+ this.characterPixelY = this.windowLayout.controlBar[0] >> 1;
+
+ // define layout of the hacking screen
+ var circuitBaseSize = minSize<320?24:(minSize<480?32:40);
+ circuitBaseSize = Math.min(circuitBaseSize, 16*Math.floor(playAreaHeight-4*blockSize)/144);
+ this.windowLayout.hackingArea = [circuitBaseSize];
+ for (var i=0; i<4; ++i) {
+ this.windowLayout.hackingArea.push(2*blockSize-circuitBaseSize, 4*blockSize+(2*i+1)*circuitBaseSize);
+ this.windowLayout.hackingArea.push(2*blockSize+circuitBaseSize, 4*blockSize+(2*i+2)*circuitBaseSize);
+ }
+ this.game.layoutChanged(this.windowLayout);
+ },
+
+
+ /**
+ * Draw text on the text canvas, with shadow
+ * @param text The text to write
+ * @param x X-coordinate of the text, left/center/right depending on the textAlign property of the canvas
+ * @param y Y-coordinate of the text
+ */
+ drawShadedText : function(text, x, y)
+ {
+ this.sceneryContext.shadowOffsetX = -1;
+ this.sceneryContext.shadowOffsetY = -1;
+ this.sceneryContext.fillText(text, x, y);
+ this.sceneryContext.shadowOffsetX = 2;
+ this.sceneryContext.shadowOffsetY = 2;
+ this.sceneryContext.fillText(text, x, y);
+ },
+
+ /**
+ * Draw text on the text canvas, with an outline
+ * @param text The text to write
+ * @param x X-coordinate of the text, left/center/right depending on the textAlign property of the canvas
+ * @param y Y-coordinate of the text
+ * @param size Text size in px
+ */
+ outlineText : function(text,x,y,size) {
+ this.overlayContext.font = Math.ceil(size)+"px cursive";
+ this.overlayContext.strokeText(text, x, y);
+ this.overlayContext.fillText(text, x, y);
+ },
+
+ /**
+ * Show the current message - if any - on top of the playing area
+ * @param canShow true if the message may be shown. False to force hide it.
+ */
+ drawMessage : function(canShow) {
+ if (this.world.currentMessage && canShow) {
+ this.messageBox.style.visibility = "visible";
+ var textNode = this.messageBox.firstChild;
+ textNode.data = this.messages[this.world.currentMessage-1];
+ } else {
+ this.messageBox.style.visibility = "hidden";
+ }
+ },
+
+
+ /**
+ * Draw both scenery and overlay canvas
+ */
+ drawMain : function() {
+
+ // animated toggle between move and hack modes
+ if (this.world.hacking) {
+ this.windowLayout.hackingMode = Math.min(this.windowLayout.hackingMode+.02, 1);
+ } else {
+ this.windowLayout.hackingMode = Math.max(this.windowLayout.hackingMode-.02, 0);
+ }
+
+ this.characterPixelX = 2*this.windowLayout.playArea[0]*this.windowLayout.hackingMode + (1-this.windowLayout.hackingMode)*(this.sceneryCanvas.width >> 1);
+ this.characterPixelY = 2*this.windowLayout.playArea[0]*this.windowLayout.hackingMode + (1-this.windowLayout.hackingMode)*(this.windowLayout.controlBar[0] >> 1);
+
+ this.drawPlayfield();
+
+ // Draw hacking view
+ if (this.windowLayout.hackingMode > 0) {
+ this.drawHackingView();
+ } else {
+ this.overlayContext.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height);
+ }
+
+ // Draw control bar (keys)
+ this.drawControlBar();
+
+ ++this.frameCount;
+ },
+
+
+ /**
+ * Draw the main game view on the scenery canvas
+ */
+ drawPlayfield : function() {
+ var dark = this.world.playerHasDiamond();
+ this.sceneryContext.clearRect(0, 0, this.sceneryCanvas.width, this.sceneryCanvas.height);
+
+ if (this.world.floorTransition) {
+ this.sceneryContext.save();
+ var alpha = (this.world.floor < this.world.targetFloor ? this.world.floorTransition/32 : 1-this.world.floorTransition/32);
+ this.sceneryContext.translate(this.characterPixelX, this.characterPixelY);
+ this.sceneryContext.scale(1-.8*alpha, 1-.8*alpha);
+ this.sceneryContext.translate(-this.characterPixelX, -this.characterPixelY);
+ this.drawOneFloor(Math.min(this.world.floor, this.world.targetFloor), this.world.elevatorFrom, this.world.elevatorTo, true, false, dark);
+ this.sceneryContext.restore();
+ this.sceneryContext.save();
+ this.sceneryContext.translate(this.characterPixelX, this.characterPixelY);
+ this.sceneryContext.scale(7-6*alpha, 7-6*alpha);
+ this.sceneryContext.translate(-this.characterPixelX, -this.characterPixelY);
+ this.drawOneFloor(Math.max(this.world.floor, this.world.targetFloor), this.world.elevatorFrom, this.world.elevatorTo, true, false, dark);
+ this.sceneryContext.restore();
+ this.drawOneFloor(this.world.floor, this.world.elevatorFrom, this.world.elevatorTo, false, false, dark);
+ } else {
+ this.drawOneFloor(this.world.floor, this.world.elevatorFrom, this.world.elevatorTo, false, true, dark);
+ }
+
+ // Draw character
+ this.sceneryContext.save();
+ this.sceneryContext.translate(this.characterPixelX, this.characterPixelY);
+ this.sceneryContext.rotate(-this.world.orientation);
+ var animStep = (this.world.motionPath.length ? 1+((this.frameCount>>1)&7) : 0);
+ this.sceneryContext.drawImage(this.spriteSheet, 48*animStep, 192, 48, 48, -this.windowLayout.playArea[0]/2, -this.windowLayout.playArea[0]/2, this.windowLayout.playArea[0], this.windowLayout.playArea[0]);
+ this.sceneryContext.restore();
+ this.sceneryContext.strokeStyle = "#00f";
+
+ if (this.world.thoughtPuzzled) {
+ this.sceneryContext.fillStyle = "#fff";
+ this.sceneryContext.fillRect(this.characterPixelX+10, this.characterPixelY-25, 15, 15);
+ this.sceneryContext.strokeText("?", this.characterPixelX+15, this.characterPixelY-14);
+ }
+
+ // Draw movement target
+ if (this.world.motionPath.length > 0) {
+ var x = this.worldToPixelX(this.world.motionPath[this.world.motionPath.length-2]);
+ var y = this.worldToPixelY(this.world.motionPath[this.world.motionPath.length-1]);
+ this.sceneryContext.beginPath();
+ this.sceneryContext.moveTo(x-(this.windowLayout.playArea[0]/8),y-(this.windowLayout.playArea[0]/8));
+ this.sceneryContext.lineTo(x+(this.windowLayout.playArea[0]/8),y+(this.windowLayout.playArea[0]/8));
+ this.sceneryContext.moveTo(x+(this.windowLayout.playArea[0]/8),y-(this.windowLayout.playArea[0]/8));
+ this.sceneryContext.lineTo(x-(this.windowLayout.playArea[0]/8),y+(this.windowLayout.playArea[0]/8));
+ this.sceneryContext.stroke();
+ }
+
+
+ },
+
+ drawOneFloor : function(floor, room1, room2, exclude, allRooms, dark) {
+ var windowMinX = Math.max(0, Math.floor(this.world.posX-this.characterPixelX/this.windowLayout.playArea[0]));
+ var windowMaxX = Math.min(16, Math.ceil(this.world.posX+this.characterPixelX/this.windowLayout.playArea[0]));
+ var windowMinY = Math.max(0, Math.floor(this.world.posY-(this.windowLayout.controlBar[0]-this.characterPixelY)/this.windowLayout.playArea[0]));
+ var windowMaxY = Math.min(16, 1+Math.ceil(this.world.posY+this.characterPixelY/this.windowLayout.playArea[0]));
+
+ var deltaWallH = 0;
+ if (!allRooms && !exclude && room1>0) {
+ // drawing the elevator animation, keep only the room size
+ windowMinX = windowMinY = 16;
+ windowMaxX = windowMaxY = 0;
+ for (var j=0; j<16; ++j) {
+ for (var i=0 ; i<16 ; ++i) {
+ if (this.world.roomNumber[i+j*this.world.levelWidth+floor*this.world.levelSize] == room1) {
+ windowMinX = Math.min(windowMinX, i);
+ windowMaxX = Math.max(windowMaxX, i+1);
+ windowMinY = Math.min(windowMinY, j);
+ windowMaxY = Math.max(windowMaxY, j+1);
+ }
+ }
+ }
+ // do not show a wall at the end of stairs
+ deltaWallH = (this.world.inStairs ? 1 : 0);
+ }
+
+ // Draw floor, terminals and objects
+ for (var j=windowMinY; j127&&type<255?96:(type==64||type==65?48:0)), dark?96:0, 48, 48, this.worldToPixelX(i), this.worldToPixelY(j+1), this.windowLayout.playArea[0], this.windowLayout.playArea[0]);
+ if (type>0) {
+ if (type<6) {
+ // pickable object, key or diamond
+ this.sceneryContext.drawImage(this.spriteSheet, type*48-48, 144, 48, 48, this.worldToPixelX(i), this.worldToPixelY(j+1), this.windowLayout.playArea[0], this.windowLayout.playArea[0]);
+ } else if (type>2048) {
+ // terminal
+ var orientation = (type&1536)>>9;
+ this.sceneryContext.drawImage(this.spriteSheet, 48*orientation, 48, 48, 48, this.worldToPixelX(i), this.worldToPixelY(j+1), this.windowLayout.playArea[0], this.windowLayout.playArea[0]);
+ if (this.world.highlightedTerminal == (type&511)) {
+ this.sceneryContext.strokeStyle = "#fff";
+ var x0 = this.worldToPixelX(i+[.55, 0, 0, 0][orientation]);
+ var y0 = this.worldToPixelY(j+[1, 1, 1, .45][orientation]);
+ var dx = this.windowLayout.playArea[0]*[.45, 1, .45, 1][orientation];
+ var dy = this.windowLayout.playArea[0]*[1, .45, 1, .45][orientation];
+ this.sceneryContext.strokeRect(x0, y0, dx, dy);
+ }
+ if (this.world.selectedTerminal == (type&511)) {
+ this.sceneryContext.strokeStyle = "#f00";
+ var x0 = this.worldToPixelX(i+[.55, 0, 0, 0][orientation]);
+ var y0 = this.worldToPixelY(j+[1, 1, 1, .45][orientation]);
+ var dx = this.windowLayout.playArea[0]*[.45, 1, .45, 1][orientation];
+ var dy = this.windowLayout.playArea[0]*[1, .45, 1, .45][orientation];
+ this.sceneryContext.strokeRect(x0, y0, dx, dy);
+ }
+ }
+ }
+ // debug : room ID
+ /*
+ {
+ var x = this.worldToPixelX(i);
+ var y = this.worldToPixelY(j);
+ if (type >= 2048) {
+ this.sceneryContext.fillStyle = "#f00";
+ this.sceneryContext.fillText((type&255)+" -> "+this.world.terminals[type&255].door, x+this.windowLayout.playArea[0]/4, y-this.windowLayout.playArea[0]/2);
+ } else {
+ this.sceneryContext.fillStyle = "#000";
+ this.sceneryContext.fillText(roomNumber, x+this.windowLayout.playArea[0]/2, y-this.windowLayout.playArea[0]/2);
+ }
+ }*/
+ }
+ }
+ }
+
+
+ // draw doors
+
+ for (var j=windowMinY+deltaWallH; j<=windowMaxY-deltaWallH; ++j) {
+ var y = this.worldToPixelY(j);
+ for (var i=windowMinX ; i 0) {
+ // debug : door ID
+ /*
+ {
+ this.sceneryContext.fillStyle = "#00f";
+ this.sceneryContext.fillText(wallH, x+this.windowLayout.playArea[0]/2, y);
+ }*/
+ wallH = this.world.doors[wallH];
+ }
+ if (wallH) {
+ if (wallH > 0) { // door
+ var dys = Math.floor(48*wallH/40);
+ var dyd = Math.floor(this.windowLayout.playArea[0]*wallH/40);
+ this.sceneryContext.drawImage(this.spriteSheet, 216-dys, 24, dys, 8, x, y-4, dyd, 8);
+ this.sceneryContext.drawImage(this.spriteSheet, 216, 24, dys, 8, x+this.windowLayout.playArea[0]-dyd, y-4, dyd, 8);
+ } else { // wall
+ this.sceneryContext.drawImage(this.spriteSheet, 192, 0, 48, 8, x, y-4, this.windowLayout.playArea[0], 8);
+ }
+ }
+ }
+ }
+
+ for (var j=windowMinY; j 0) {
+ // debug : door ID
+ /*
+ {
+ this.sceneryContext.fillStyle = "#00f";
+ this.sceneryContext.fillText(wallV, x, y-this.windowLayout.playArea[0]/2);
+ }*/
+ wallV = this.world.doors[wallV];
+ }
+ if (wallV) {
+ if (wallV > 0) { // door
+ var dys = Math.floor(48*wallV/40);
+ var dyd = Math.floor(this.windowLayout.playArea[0]*wallV/40);
+ this.sceneryContext.drawImage(this.spriteSheet, 216, 72-dys, 8, dys, x-4, y-this.windowLayout.playArea[0], 8, dyd);
+ this.sceneryContext.drawImage(this.spriteSheet, 216, 72, 8, dys, x-4, y-dyd, 8, dyd);
+
+
+ } else { // wall
+ this.sceneryContext.drawImage(this.spriteSheet, 192, 48, 8, 48, x-4, y-this.windowLayout.playArea[0], 8, this.windowLayout.playArea[0]);
+ }
+ }
+ }
+ }
+
+ },
+
+ worldToPixelX : function(x) {
+ return this.characterPixelX-this.windowLayout.playArea[0]*(this.world.posX - x);
+ },
+
+ worldToPixelY : function(y) {
+ return this.characterPixelY-this.windowLayout.playArea[0]*(y-this.world.posY);
+ },
+
+ /**
+ * Draw the view for the hacking mode, when attempting to open a door through a terminal
+ */
+ drawHackingView : function() {
+ // keep the view in the outside of a circle around the playfield
+ this.overlayContext.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height);
+ this.overlayContext.save();
+ this.overlayContext.beginPath();
+ this.overlayContext.rect(0, 0, this.overlayCanvas.width, this.windowLayout.controlBar[0]);
+ this.overlayContext.arc(this.characterPixelX, this.characterPixelY, this.characterPixelX, 0, 2*Math.PI, true);
+ this.overlayContext.clip();
+ this.overlayContext.fillStyle = "#040";
+ this.overlayContext.fillRect(0, 0, this.overlayCanvas.width, this.windowLayout.controlBar[0]);
+
+ // text
+ this.terminalId = this.world.selectedTerminal>0?this.world.selectedTerminal:this.terminalId;
+ this.overlayContext.fillStyle = "#2a2";
+ this.overlayContext.font = this.windowLayout.playArea[0]+"px Impact,fantasy";
+ var message = "DOOR #"+this.world.floor+(this.terminalId<15?"0":"")+(1+this.terminalId).toString(16).toUpperCase();
+ this.overlayContext.fillText(message, this.overlayCanvas.width/2, 3*this.windowLayout.playArea[0]);
+
+ // circuit : edges / conductive horizontal lines
+ var resultChipX = Math.floor(this.overlayCanvas.width-8*this.windowLayout.hackingArea[0]);
+ for (var i=0; i<8; ++i) {
+ var y = this.windowLayout.hackingArea[2]+i*this.windowLayout.hackingArea[0];
+ this.overlayContext.fillRect(resultChipX, y-6, this.overlayCanvas.width-resultChipX, 12);
+ }
+
+ var x0 = this.windowLayout.hackingArea[3];
+ var dx = Math.floor((resultChipX - x0)/this.world.circuitGfxWidth);
+ this.overlayContext.fillStyle = "#2a2";
+ for (var i=0; i0 && node[0]<5) { // NOT door
+ this.drawChip(x0+dx*node[1], y, .5*this.windowLayout.hackingArea[0], 1, node[0]);
+ }
+ if ((node[0]>=5 && node[0]<=6) || node[0]==13) { // AND2, OR2, decrease
+ this.drawChip(x0+dx*node[1], y, .5*this.windowLayout.hackingArea[0], 2, node[0]);
+ }
+ }
+
+
+ // entry points
+ for (var i=0; i<8; ++i) {
+
+ this.overlayContext.fillStyle = (this.world.circuitInputs[i] == -1 ?"#fff" : this.keyColors[this.world.keysAcquired[this.world.circuitInputs[i]]]);
+ this.overlayContext.beginPath();
+ this.overlayContext.arc(this.windowLayout.hackingArea[2*i+1], this.windowLayout.hackingArea[2*i+2], this.windowLayout.hackingArea[0]/2, 0, 7);
+ this.overlayContext.fill();
+
+ if (i==this.world.controls.dropTargetBelowMouse) {
+ this.overlayContext.lineWidth = 2;
+ this.overlayContext.strokeStyle = (this.world.selectedKey == -1 ?"#fff" : this.keyColors[this.world.keysAcquired[this.world.selectedKey]]);
+ this.overlayContext.beginPath();
+ this.overlayContext.arc(this.windowLayout.hackingArea[2*i+1], this.windowLayout.hackingArea[2*i+2], this.windowLayout.hackingArea[0]/2-2, 0, 7);
+ this.overlayContext.stroke();
+ }
+ }
+
+ // registry output
+ this.overlayContext.fillStyle = this.keyColors[this.world.doorOpen?1:0];
+ for (var i=0; i<8; ++i) {
+ var y = this.windowLayout.hackingArea[2]+i*this.windowLayout.hackingArea[0];
+ this.overlayContext.fillRect(resultChipX, y-3, this.overlayCanvas.width-resultChipX, 6);
+ }
+
+ // door internal memory registry
+ this.drawChip(resultChipX, this.windowLayout.hackingArea[2], 1.5*this.windowLayout.hackingArea[0], 8, 8);
+
+ this.drawRegistry(this.world.currentRegistry, resultChipX-.5*this.windowLayout.hackingArea[0], this.windowLayout.hackingArea[2]+.5*this.windowLayout.hackingArea[0], this.windowLayout.hackingArea[0], this.windowLayout.hackingArea[0]*.75);
+
+ // GlitchMaker
+ this.overlayContext.fillStyle = "#028";
+ this.overlayContext.fillRect(this.overlayCanvas.width-4*this.windowLayout.hackingArea[0], this.windowLayout.hackingArea[2]-3*this.windowLayout.hackingArea[0], 4*this.windowLayout.hackingArea[0], 11*this.windowLayout.hackingArea[0]);
+ this.overlayContext.fillStyle = "#001";
+ this.overlayContext.fillRect(this.overlayCanvas.width-3.8*this.windowLayout.hackingArea[0], this.windowLayout.hackingArea[2]-2*this.windowLayout.hackingArea[0], 3.9*this.windowLayout.hackingArea[0], 9.8*this.windowLayout.hackingArea[0]);
+ this.overlayContext.font=(this.windowLayout.hackingArea[0]>>1)+"px cursive";
+ this.overlayContext.textAlign = "left";
+ this.overlayContext.fillText(" GlitchMaker 3000", this.overlayCanvas.width-4*this.windowLayout.hackingArea[0], this.windowLayout.hackingArea[2]-2.5*this.windowLayout.hackingArea[0]);
+ this.overlayContext.textAlign = "center";
+ this.overlayContext.fillStyle = "#08f";
+ this.overlayContext.fillText(this.world.doorOpen?"ACCESS":"MATCH TO", this.overlayCanvas.width-1.5*this.windowLayout.hackingArea[0], this.windowLayout.hackingArea[2]-this.windowLayout.hackingArea[0]);
+ this.overlayContext.fillText(this.world.doorOpen?"GRANTED":"OPEN", this.overlayCanvas.width-1.5*this.windowLayout.hackingArea[0], this.windowLayout.hackingArea[2]-this.windowLayout.hackingArea[0]/2);
+
+ this.drawRegistry(this.world.targetRegistry, this.overlayCanvas.width-2*this.windowLayout.hackingArea[0], this.windowLayout.hackingArea[2]+.5*this.windowLayout.hackingArea[0], this.windowLayout.hackingArea[0], this.windowLayout.hackingArea[0]*.75);
+
+ this.overlayContext.restore();
+ },
+
+ /**
+ * Draw one chip : dark box, pins, and symbol
+ */
+ drawChip (x, y, halfWidth, height, type) {
+ this.overlayContext.fillStyle = "#111";
+ this.overlayContext.fillRect(x-halfWidth, y-.5*this.windowLayout.hackingArea[0], 2*halfWidth, height*this.windowLayout.hackingArea[0]);
+ if (type<7 || type==13) {
+ var index = (type<5 ? 0 : (type==6 ? 1 : (type==5 ? 2 : 3)));
+ this.overlayContext.drawImage(this.spriteSheet, index*48,240, 48, 48, x-halfWidth, y+(height-1)*this.windowLayout.hackingArea[0]/2-halfWidth, 2*halfWidth, 2*halfWidth);
+ if (type<5) { // NOT door : show which color
+ this.overlayContext.fillStyle = this.keyColors[type];
+ this.overlayContext.fillRect(x, y-.5*this.windowLayout.hackingArea[0]+2, halfWidth-2, 3);
+ }
+ }
+ // pins
+ for (var i=0; i6) {
+ this.overlayContext.drawImage(this.spriteSheet, 198, 240, 5, 16, x+halfWidth-1, y+i*this.windowLayout.hackingArea[0]-8, 5, 16);
+ }
+ }
+ if (type==5 ||type==6) {
+ this.overlayContext.drawImage(this.spriteSheet, 198, 240, 5, 16, x+halfWidth-1, y+this.windowLayout.hackingArea[0]/2-8, 5, 16);
+ }
+ },
+
+ /**
+ * Draw one slanted piece of track
+ */
+ drawSlantedTrack (x, y, halfWidth, dY0, dY1) {
+ this.overlayContext.beginPath();
+ this.overlayContext.moveTo(x-halfWidth*this.windowLayout.hackingArea[0], y+dY0*this.windowLayout.hackingArea[0]);
+ this.overlayContext.lineTo(x+halfWidth*this.windowLayout.hackingArea[0], y+dY1*this.windowLayout.hackingArea[0]);
+ this.overlayContext.stroke();
+ },
+
+ /**
+ * Draw the registry values on a chip
+ */
+ drawRegistry : function(registry, x, y, sizeX, sizeY) {
+ this.overlayContext.strokeStyle = "#000";
+ this.overlayContext.lineWidth = 2;
+ for (var i=0; i<8; ++i) {
+ this.overlayContext.fillStyle = this.keyColors[registry[i]];
+ this.overlayContext.fillRect(x, y+i*sizeY, sizeX, sizeY);
+ this.overlayContext.strokeRect(x, y+i*sizeY, sizeX, sizeY);
+ }
+ },
+
+ /**
+ * Draw the keys obtained by the player
+ */
+ drawControlBar : function() {
+ this.sceneryContext.fillStyle = "#122";
+ this.sceneryContext.fillRect(0, this.windowLayout.controlBar[0], this.sceneryCanvas.width, this.windowLayout.playArea[0]);
+ for (var keyIndex = 0 ; keyIndex < this.world.keysAcquired.length; ++keyIndex) {
+ if (this.world.selectedKey == keyIndex) {
+ this.sceneryContext.strokeStyle = "#fff";
+ this.sceneryContext.beginPath();
+ this.sceneryContext.arc((keyIndex+.5)*this.windowLayout.playArea[0], this.windowLayout.controlBar[0]+this.windowLayout.playArea[0]/2, this.windowLayout.playArea[0]*.45, 0, 7);
+ this.sceneryContext.stroke();
+ }
+
+ var key = this.world.keysAcquired[keyIndex];
+ this.sceneryContext.strokeStyle = this.keyColors[key];
+ var x = keyIndex*this.windowLayout.playArea[0];
+ var y = this.windowLayout.controlBar[0];
+ if (key<5) {
+ // draw a circle around keys, not around the diamond
+ if (this.world.hacking && !this.world.keysAvailable[keyIndex]) {
+ this.sceneryContext.setLineDash([5]);
+ }
+ this.sceneryContext.beginPath();
+ this.sceneryContext.arc(x+this.windowLayout.playArea[0]/2, y+this.windowLayout.playArea[0]/2, this.windowLayout.playArea[0]*.4, 0, 7);
+ this.sceneryContext.stroke();
+ this.sceneryContext.setLineDash([]);
+ }
+ if (this.world.selectedKey == keyIndex && this.world.dragging) {
+ x = this.world.controls.mouseX-this.windowLayout.playArea[0]/2;
+ y = this.world.controls.mouseY-this.windowLayout.playArea[0]/2;
+ this.overlayContext.drawImage(this.spriteSheet, key*48-48, 144, 48, 48, x, y, this.windowLayout.playArea[0], this.windowLayout.playArea[0]);
+ } else if (!this.world.hacking || this.world.keysAvailable[keyIndex]){
+ this.sceneryContext.drawImage(this.spriteSheet, key*48-48, 144, 48, 48, x, y, this.windowLayout.playArea[0], this.windowLayout.playArea[0]);
+ }
+
+ }
+
+ // time
+ this.overlayContext.textAlign = "center";
+ this.overlayContext.fillStyle = "#f00";
+ this.overlayContext.strokeStyle = "#811";
+ var seconds = Math.floor((this.world.timer/25)%60);
+ var minutes = Math.floor(this.world.timer/1500);
+ var textMsg = (minutes<10?"0":"")+minutes+":"+(seconds<10?"0":"")+seconds;
+ this.outlineText(textMsg, this.overlayCanvas.width-this.windowLayout.playArea[0]*2, this.windowLayout.controlBar[0]+this.windowLayout.playArea[0]*.65, this.windowLayout.playArea[0]/2);
+
+ // floor
+ this.overlayContext.fillStyle = "#ccc";
+ this.overlayContext.strokeStyle = "#766";
+ var floorNames = ["Ground", "1st", "2nd", "3rd", "4th", "Tutorial"];
+ var floorMsg = floorNames[this.world.floor];
+ if (this.world.targetFloor != this.world.floor) {
+ floorMsg += " -> "+ floorNames[this.world.targetFloor];
+ }
+ this.outlineText(floorMsg, this.sceneryCanvas.width-this.windowLayout.playArea[0]*5, this.windowLayout.controlBar[0]+this.windowLayout.playArea[0]*.4, this.windowLayout.playArea[0]/2);
+ this.outlineText("FLOOR", this.sceneryCanvas.width-this.windowLayout.playArea[0]*5, this.windowLayout.controlBar[0]+this.windowLayout.playArea[0]*.9, this.windowLayout.playArea[0]/2);
+
+
+ },
+
+ /**
+ * Display title and main menu
+ */
+ /**
+ * Draw the main menu options
+ * @param menu the active menu
+ */
+ drawMainMenu : function(menu) {
+
+ this.overlayContext.save();
+ var highlightColor = "#fff";
+ if ((this.frameCount%3)==0 && ((Math.floor(this.frameCount/50)&1)==0)) {
+ highlightColor = "#f77";
+ }
+ var gray = Math.round(50+50*Math.cos(this.frameCount/20));
+
+ this.overlayContext.lineWidth = 6;
+ this.overlayContext.textAlign="center";
+ for (var index=0; index<3; ++index) {
+ this.overlayContext.fillStyle=(menu.selectedLine==index?highlightColor : (index0) for door
+ */
+ wallAt : function (floor, x, y, horiz) {
+ var index = x+y*this.levelWidth+floor*this.levelSize;
+ return (horiz ? this.wallsH[index] : this.wallsV[index]);
+ },
+
+ /**
+ * Access a terminal and try to open a door
+ */
+ startHacking : function() {
+ this.keysAvailable = [];
+ for (var i=0; i 0) {
+ var targetX = this.motionPath[0];
+ var targetY = this.motionPath[1];
+ var dist2 = (targetX - this.posX)*(targetX - this.posX) + (targetY - this.posY)*(targetY - this.posY);
+ var step = this.inStairs ? .1 : .2;
+ if (step*step > dist2) {
+ this.posX = targetX;
+ this.posY = targetY;
+ this.motionPath.splice(0, 2);
+ this.computeShortestPaths();
+ if (this.motionPath.length == 0) {
+ // move ends
+ var itemType = this.ground[Math.floor(this.posX)+Math.floor(this.posY)*this.levelWidth+this.floor*this.levelSize];
+ if (this.selectedTerminal > -1) { // arriving near a terminal : start hacking
+ this.orientation = Math.PI*((itemType&1536)>>9)/2;
+ this.startHacking();
+ }
+ if (itemType==64 || itemType == 65) {
+ if (this.elevatorFrom == 0 && !this.playerHasDiamond()) {
+ // enter elevator (without the diamond): close the doors
+ this.elevatorFrom = this.roomNumber[Math.floor(this.posX)+Math.floor(this.posY)*this.levelWidth+this.floor*this.levelSize];
+ this.elevatorTo = this.roomNumber[Math.floor(this.posX)+Math.floor(this.posY)*this.levelWidth+(this.floor+(itemType==64?1:-1))*this.levelSize];
+ this.doorsMoving.push(this.elevatorDoors[this.elevatorFrom], 1, 20);
+ this.doorsMoving.push(this.elevatorDoors[this.elevatorTo], 1, 20);
+ }
+ }
+ if (this.posX<0) {
+ // the player exited the building : end the game
+ this.currentMessage = (this.playerHasDiamond() ? 6 : 4);
+ }
+ }
+ if (this.roomNumber[Math.floor(this.posX)+Math.floor(this.posY)*this.levelWidth+this.floor*this.levelSize] == 116 && !this.visitedDiamondRoom) {
+ // first time in the diamond room : prompt a message
+ this.visitedDiamondRoom = true;
+ this.currentMessage = 2;
+ }
+
+ } else {
+ this.orientation = Math.atan2(targetY-this.posY, targetX-this.posX);
+ this.posX += step*Math.cos(this.orientation);
+ this.posY += step*Math.sin(this.orientation);
+
+ var itemType = this.ground[Math.floor(this.posX)+Math.floor(this.posY)*this.levelWidth+this.floor*this.levelSize];
+ if (itemType > 0 && itemType < 63) {
+ // grab object
+ var insertIndex = 0;
+ while (insertIndexthis.keysAcquired[insertIndex]) {
+ ++insertIndex;
+ }
+ this.keysAcquired.splice(insertIndex, 0, itemType);
+
+ this.ground[Math.floor(this.posX)+Math.floor(this.posY)*this.levelWidth+this.floor*this.levelSize] = 0;
+ if (itemType == 5) {
+ // got the diamond ! Inform the player that (s)he now has to run for the exit
+ this.currentMessage = 3;
+ }
+ } else if (itemType>127 && itemType<192) {
+ // stairs
+ var direction = (Math.sin(this.orientation)>0 ? 1 : -1);
+
+ // force destination at end of stairs
+ this.motionPath[this.motionPath.length-1] = direction < 0 ? Math.floor(this.posY) - .5 - itemType + 128 : Math.floor(this.posY) + .5 + 132 - itemType;
+ this.targetFloor = this.floor - direction;
+ var alpha = 8*(itemType-128+this.posY-Math.floor(this.posY))
+ this.floorTransition = Math.floor(direction>0 ? alpha : 32-alpha);
+ this.elevatorFrom = this.roomNumber[Math.floor(this.posX)+Math.floor(this.posY)*this.levelWidth+this.floor*this.levelSize];
+ this.elevatorTo = this.roomNumber[Math.floor(this.posX)+Math.floor(this.posY)*this.levelWidth+(this.floor-direction)*this.levelSize];
+ this.inStairs = true;
+ } else {
+ if (this.inStairs) {
+ this.floor = this.targetFloor;
+ this.floorTransition = 0;
+ this.inStairs = false;
+ this.elevatorFrom = this.elevatorTo = 0;
+ }
+
+ }
+ }
+ }
+
+ // move elevators
+ if (this.inElevator) {
+ ++this.floorTransition;
+ if (this.floorTransition>=32) {
+ // new floor reached : open elevator doors
+ this.floor = this.targetFloor;
+ this.floorTransition = 0;
+ this.doorsMoving.push(this.elevatorDoors[this.elevatorFrom], -1, 0);
+ this.doorsMoving.push(this.elevatorDoors[this.elevatorTo], -1, 0);
+ this.elevatorFrom = this.elevatorTo = 0;
+ this.inElevator = false;
+ }
+ }
+
+ // open and close doors
+ for (var i=0; i 0 && this.doors[this.doorsMoving[i]] >= this.doorsMoving[i+2])
+ || (this.doorsMoving[i+1] < 0 && this.doors[this.doorsMoving[i]] <= this.doorsMoving[i+2])) {
+ // sensor : stop motion when the door is open or closed
+ this.doors[this.doorsMoving[i]] = this.doorsMoving[i+2];
+ this.doorsMoving.splice(i, 3);
+ i-=3;
+ if (this.elevatorFrom) {
+ // elevator door closed : move up or down
+ // possible but unlikely bug, if another door closes first, it triggers the elevator
+ this.floorTransition = 0;
+ this.inElevator = true;
+ this.targetFloor = this.floor + (this.ground[Math.floor(this.posX)+Math.floor(this.posY)*this.levelWidth+this.floor*this.levelSize]==64 ? 1 : -1);
+ }
+ this.saveGame();
+ this.computeShortestPaths(); // opening or closing a door changes the paths
+ // TODO : handle the fact that the path we took just closed
+ }
+ }
+ },
+
+
+ /**
+ * Take into account user actions (mouse move / click)
+ * Called once each step.
+ */
+ processControls : function()
+ {
+ if (this.controls.mouseLeftButton) {
+ if (this.currentMessage) {
+ // modal dialog showing a message, consumes a mouse click
+ this.controls.acknowledgeMouseClick();
+ if (this.currentMessage == 5 || this.currentMessage == 6) {
+ // endgame messages (win and loss)
+ this.gameInProgress = false;
+ // the Game object will change the state to the main menu
+ }
+ this.currentMessage = 0;
+ }
+ else if (this.dragging) {
+ // mouse move event while dragging : check if a drop target is below
+
+ } else { // mouse down : first event with button down, or setting motion
+ if (this.controls.mouseInPlayArea) {
+ if (this.hacking) {
+ if (this.controls.mouseInBullsEye) {
+ // click in the circle showing main view : exit hacking mode
+ this.hacking = false; // for now, leave the hacking mode
+ this.selectedTerminal = -1;
+ this.controls.acknowledgeMouseClick();
+ } else {
+ if (this.controls.dropTargetBelowMouse > -1) {
+ this.selectedKey = this.circuitInputs[this.controls.dropTargetBelowMouse];
+ this.circuitInputs[this.controls.dropTargetBelowMouse] = -1;
+ this.dragging = true;
+ }
+ }
+ } else if (!this.inStairs) {
+ // define a new target. Not allowed in stairs
+ var targetX = this.posX + this.controls.worldDX;
+ var targetY = this.posY + this.controls.worldDY;
+ var itemAtTarget = 0;
+ if (targetX >= 0 && targetY >=0 && targetX<16 && targetY<16) {
+ itemAtTarget = this.ground[Math.floor(targetX)+Math.floor(targetY)*this.levelWidth+this.floor*this.levelSize];
+ }
+ if (itemAtTarget&2048) {
+ // there is a terminal or lock at target : move in front or the panel
+ var orientation = (itemAtTarget&1536)>>9;
+ targetX = Math.floor(targetX)+[.25, .5, .75, .5][orientation];
+ targetY = Math.floor(targetY)+[.5, .25, .5, .75][orientation];
+ this.selectedTerminal = itemAtTarget & 511;
+ } else {
+ this.selectedTerminal = -1;
+ // keep one's distance from the wall
+ if (this.wallsV[Math.floor(targetX)+Math.floor(targetY)*this.levelWidth+this.floor*this.levelSize]) {
+ targetX = Math.max(targetX, Math.floor(targetX)+.4);
+ }
+ if (this.wallsV[Math.floor(targetX+1)+Math.floor(targetY)*this.levelWidth+this.floor*this.levelSize]) {
+ targetX = Math.min(targetX, Math.floor(targetX)+.6);
+ }
+ if (this.wallsH[Math.floor(targetX)+Math.floor(targetY)*this.levelWidth+this.floor*this.levelSize]) {
+ targetY = Math.max(targetY, Math.floor(targetY)+.4);
+ }
+ if (this.wallsH[Math.floor(targetX+1)+Math.floor(targetY+1)*this.levelWidth+this.floor*this.levelSize]) {
+ targetY = Math.min(targetY, Math.floor(targetY)+.6);
+ }
+ }
+ var currentRoom = this.roomNumber[Math.floor(this.posX)+Math.floor(this.posY)*this.levelWidth+this.floor*this.levelSize];
+ var targetRoom = 0;
+ if (targetX >= 0 && targetY >=0 && targetX<16 && targetY<16) {
+ targetRoom = this.roomNumber[Math.floor(targetX)+Math.floor(targetY)*this.levelWidth+this.floor*this.levelSize];
+ }
+ var path = this.shortestPaths[targetRoom];
+ if (path) {
+ this.motionPath = path.slice();
+ if (!targetRoom) {
+ // recenter any target outside the building to aim in front of the door
+ targetX = -.9;
+ targetY = 8.5;
+ }
+ this.motionPath.push(targetX, targetY);
+ this.thoughtPuzzled = false;
+ } else {
+ this.thoughtPuzzled = true;
+ }
+ }
+ } else if (this.controls.keyBelowMouse < this.keysAcquired.length) {
+ // click in control bar : choose key and prepare to drag it
+ if (this.hacking && this.keysAvailable[this.controls.keyBelowMouse] && this.keysAcquired[this.controls.keyBelowMouse]<5) {
+ this.selectedKey = this.controls.keyBelowMouse;
+ this.dragging = true;
+ }
+ } else {
+ // click beyond inventory = on ground info or timer => trigger main menu
+ this.controls.controlEscape = true;
+ this.controls.acknowledgeMouseClick();
+ }
+ }
+ } else if (this.dragging) {
+ // mouse up event : release drag
+ this.dragging = false;
+ if (this.hacking) {
+ if (this.controls.dropTargetBelowMouse > -1) { // drop the key onto a target
+
+ // if there is a key in place, release it
+ var formerKey = this.circuitInputs[this.controls.dropTargetBelowMouse];
+ if (formerKey > -1) {
+ this.keysAvailable[formerKey] = true;
+ }
+
+ // and put the new key, if any
+ this.circuitInputs[this.controls.dropTargetBelowMouse] = this.selectedKey;
+ this.keysAvailable[this.selectedKey] = false;
+ } else { // drop the key somewhere .. return it to the inventory (control bar)
+ this.keysAvailable[this.selectedKey] = true;
+ }
+ this.updateCircuitBoard();
+ }
+ this.selectedKey = -1;
+ }
+
+ // highlight any terminal below the mouse pointer
+ this.highlightedTerminal = -1;
+ if (this.controls.mouseInPlayArea) {
+ var targetX = Math.floor(this.posX + this.controls.worldDX);
+ var targetY = Math.floor(this.posY + this.controls.worldDY);
+ var itemAtTarget = this.ground[targetX+targetY*this.levelWidth];
+ if (itemAtTarget & 2048) {
+ this.highlightedTerminal = itemAtTarget & 511;
+ }
+ }
+ },
+
+ /**
+ * Prepare the graph of passageways between rooms
+ * that will later be used to identify the shortest path between current and requested location
+ */
+/* precomputeTopology : function() {
+ var squaresInRoom = [0, 0];
+ var squaresBeyondRoom = [];
+ this.roomNumber = [];
+ this.passageways = [];
+ for (var j=0; j0 || squaresBeyondRoom.length>0) {
+ var x = 0 , y = 0;
+ if (squaresInRoom.length>0) {
+ x = squaresInRoom[0];
+ y = squaresInRoom[1];
+ squaresInRoom.splice(0,2);
+ } else {
+ x = squaresBeyondRoom[0];
+ y = squaresBeyondRoom[1];
+ squaresBeyondRoom.splice(0,2);
+ if (!this.roomNumber[x+y*this.levelWidth]) {
+ ++this.roomCount;
+ }
+ }
+
+ if (!this.roomNumber[x+y*this.levelWidth]) {
+ this.roomNumber[x+y*this.levelWidth] = this.roomCount;
+ if (x+1 < this.levelWidth) {
+ if (this.wallAt(x+1,y,false)) {
+ squaresBeyondRoom.push(x+1, y);
+ } else {
+ squaresInRoom.push(x+1, y);
+ }
+ }
+ if (x > 0) {
+ if (this.wallAt(x,y,false)) {
+ squaresBeyondRoom.push(x-1, y);
+ } else {
+ squaresInRoom.push(x-1, y);
+ }
+ }
+ if (y+1 < this.levelHeight) {
+ if (this.wallAt(x,y+1,true)) {
+ squaresBeyondRoom.push(x, y+1);
+ } else {
+ squaresInRoom.push(x, y+1);
+ }
+ }
+ if (y > 0) {
+ if (this.wallAt(x,y,true)) {
+ squaresBeyondRoom.push(x, y-1);
+ } else {
+ squaresInRoom.push(x, y-1);
+ }
+ }
+ }
+ }
+
+ // record passageways between two adjacent rooms
+ // constraint : only one single door between the two same rooms
+ for (var j=0; j<=this.roomCount; ++j) {
+ for (var i=0; i0) {
+ var room1 = this.roomNumber[i-1+j*this.levelWidth];
+ var room2 = this.roomNumber[i+j*this.levelWidth];
+ this.passageways[room1+room2*this.roomCount] = [doorId, i-.5, j+.5, i+.5, j+.5];
+ this.passageways[room2+room1*this.roomCount] = [doorId, i+.5, j+.5, i-.5, j+.5];
+ }
+ doorId =this.wallAt(i, j, true);
+ if (doorId>0) {
+ var room1 = this.roomNumber[i+(j-1)*this.levelWidth];
+ var room2 = this.roomNumber[i+j*this.levelWidth];
+ this.passageways[room1+room2*this.roomCount] = [doorId, i+.5, j-.5, i+.5, j+.5];
+ this.passageways[room2+room1*this.roomCount] = [doorId, i+.5, j+.5, i+.5, j-.5];
+ }
+ }
+ }
+
+ },*/
+
+ /**
+ * Compute the shortest paths between the accessible rooms
+ */
+ computeShortestPaths : function() {
+ this.shortestPaths = [];
+ for (var i=0; i this.shortestPaths[currentRoom].length + wayOut.length) {
+ this.shortestPaths[nextRoom] = this.shortestPaths[currentRoom].slice(); // shallow copy
+ this.shortestPaths[nextRoom].push(wayOut[1], wayOut[2], wayOut[3], wayOut[4]);
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Hacking mode : update the values of the circuit
+ * call each time the inputs change, or if there is a timing chip involved
+ */
+ updateCircuitBoard : function() {
+
+ // first edges are directly connected to the inputs
+ this.circuitEdgeValues = [];
+ for (var i=0; i<8; ++i) {
+ var keyIndex = this.circuitInputs[i];
+ this.circuitEdgeValues.push(keyIndex>-1 ? this.keysAcquired[keyIndex] : 0);
+ }
+
+ var outputLine = 0;
+ var edgeIndex = 8;
+ this.doorOpen = true;
+
+ for (var i=0; i 0) {
+ this.doorsMoving.push(controlledDoor, -1, 0);
+ } else {
+ this.doorsMoving.push(controlledDoor, 1, 20);
+ }
+ }
+
+ },
+
+ /**
+ * Prepare the rendering of the circuit
+ */
+ computeCircuitBoardGfx : function()
+ {
+ var outputLine = 0;
+ this.circuitGfxWidth = 1;
+ this.circuitGfxEdges = [];
+ for (var edgeIndex=0; edgeIndex<8; ++edgeIndex) {
+ this.circuitGfxEdges.push([0, edgeIndex]); // x0, y0
+ }
+
+ this.circuitGfxNodes = [];
+
+ for (var i=0; i