MediaWiki:Pikcross.js
Jump to navigation
Jump to search
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
// Canvas constants.
const CANVAS = {
// Width.
WIDTH: 800,
// Height.
HEIGHT: 800
};
// Length of a border gradient effect.
const BORDER_GRADIENT_LENGTH = 20;
// Width and height of each cell, in pixels.
const CELL_SIZE = 32;
// Padding between each cell.
const CELL_PADDING = 1;
// Maximum number of rows or columns a board can have.
const MAX_COLS_OR_ROWS = 40;
// Minimum number of rows or columns a board can have.
const MIN_COLS_OR_ROWS = 2;
// Padding between sections of the game screen.
const SECTION_PADDING = 10;
// Transition total duration.
const TRANSITION_DURATION = 0.4;
// Color for the game's background.
const COLOR_BG = 'rgb(17, 51, 17)';
// Color for the board's background.
const COLOR_BOARD_BG = 'rgba(34, 68, 34, 0.6)';
// Color for the hints' background.
const COLOR_HINTS_BG = 'rgb(51, 85, 51)';
// Color for the footer's background.
const COLOR_FOOTER_BG = 'rgb(68, 102, 68)';
// Colors for the hint text.
const COLOR_HINT = ['#622', '#338'];
// Color for the hint text when defied.
const COLOR_HINT_DEFIED = '#E11';
// Color to tint the hint button with when defied.
const COLOR_HINT_DEFIED_TINT = 'rgba(255, 0, 0, 0.7)';
// Color for a button's background.
const COLOR_BUTTON_BG = '#BCB';
// Colors for a button's shadow.
const COLOR_BUTTON_SHADOW = ['rgba(64, 0, 0, 0.5)', 'rgba(0, 0, 64, 0.5)'];
// Colors for buttons' text.
const COLOR_BUTTON_TEXT = ['#622', '#338'];
// Color for buttons' text while disabled.
const COLOR_BUTTON_TEXT_DISABLED = '#888';
// Color for a blank cell.
const COLOR_BLANK = '#EFE';
// Color for a filled cell.
const COLOR_FILLED = '#696';
// Color for a marked cell.
const COLOR_MARKED = '#EDD';
// Color for a maker tool that's green.
const COLOR_MAKER_GREEN = '#262';
// Color for a maker tool that's red.
const COLOR_MAKER_RED = '#622';
// Colors for generic text.
const COLOR_TEXT_GENERIC = ['#ECC', '#BBE'];
// Value for a blank cell.
const CELL_BLANK = 0;
// Value for a filled cell.
const CELL_FILLED = 1;
// Value for a marked cell.
const CELL_MARKED = 2;
// Main menu.
const STATE_MAIN_MENU = 0;
// Playing a puzzle.
const STATE_PLAYING = 1;
// Making a puzzle.
const STATE_MAKING = 2;
// In-game pause menu.
const PANEL_STATE_PAUSE = 0;
// Congratulating the player on a finished puzzle.
const PANEL_STATE_CONGRATS = 1;
// Asking for the new puzzle's name.
const PANEL_STATE_MAKING_NAME = 2;
// Showing the new puzzle's code.
const PANEL_STATE_MAKING_CODE = 3;
// Asking for a custom puzzle code to play on.
const PANEL_STATE_PLAYING_CODE = 4;
// Warning the player of an error while loading the level.
const PANEL_STATE_LOAD_ERROR = 5;
// Warning the player of an error while saving the level.
const PANEL_STATE_SAVE_ERROR = 6;
// Warning the player they have a bookmark when they try to start a level.
const PANEL_STATE_BOOKMARK_WARNING = 7;
// Info.
const PANEL_STATE_INFO = 8;
// When encoding or decoding data, do it for a level.
const BOARD_DATA_CONTEXT_LEVEL = 0;
// When encoding or decoding data, do it for a bookmark.
const BOARD_DATA_CONTEXT_BOOKMARK = 1;
// Button.
const GUI_ITEM_BUTTON = 0;
// Text.
const GUI_ITEM_TEXT = 1;
// Spritesheet, in the context of loading things.
const LOAD_CONTENT_SPRITES = 0;
// Game setup, in the context of loading things.
const LOAD_CONTENT_SETUP = 1;
// All levels.
const LEVELS = [
// P.
'BQV//zEAAVA=',
// Leaf.
'CgrAw48///z5548fHwwABExlYWY=',
// Bud.
'CgpYsOOee9///79//MAAA0J1ZA==',
// Flower.
'Cgow4IHH/887x49//jEDBkZsb3dlcg==',
// Red Pikmin.
'CgoAAuz/xh424AMDMggAClJlZCBQaWttaW4=',
// Yellow Pikmin.
'Cgr4cIQgyOSr9lf/fA0DDVllbGxvdyBQaWttaW4=',
// Blue Pikmin.
'Cgp4GHPaeGEBAxyw0IAEC0JsdWUgUGlrbWlu',
// Bomb Rock.
'Cgp40Ofoz78/H7x3vMABCUJvbWIgUm9jaw==',
// Swooping Snitchbug.
'Dw/HHxVVPpUgIDawO9g8CD/4L/6X/kv/JX8qnxZAARJTd29vcGluZyBTbml0Y2hidWc=',
// Purple Pikmin.
'Dw8AZKAb50fcU2SPnuS/xA/8B/gD/AH+AD4AHgAJAA1QdXJwbGUgUGlrbWlu',
// Rubber Ugly.
'Dw/gAfgB/gHXQG+R2YX/hP/8f/6//9//x//Bf4APAAtSdWJiZXIgVWdseQ==',
// Breadbug.
'Dw/wDwQI/YsD737///++/+A/YA+wR5kHhP9j/yIAAAhCcmVhZGJ1Zw==',
// White Pikmin.
'Dw8Hz3lA+AH4yf5n/wP/g/1j/8A/8F/4P/5P/3f/AQxXaGl0ZSBQaWttaW4=',
// Burrowing Snagret.
'Dw8AD8AP/A//h99HzjPj/fE3/Al+gg/AB/AD+Pz/ARFCdXJyb3dpbmcgU25hZ3JldA==',
// The Key.
'Dw8YAD6Af+D78PB54Png+fHwf/wf78fL8Q64A/gAAAdUaGUgS2V5',
// Captain Olimar.
'FA/gAPwDAUIEEnwKf2W++l/9L/0bARb/Cf4E/gL/gTOAGcAP8AP4AQ5DYXB0YWluIE9saW1hcg==',
// Data File.
'FA/+hwBE/6WAVdwtmdZYa6g11pJpcbQAWgCtqhZAS6UF0P5vAND/BwlEYXRhIEZpbGU=',
// Sunseed Berry.
'Dw8AAAAAOALigoRggVEpdfv2O69eu2b38X/wBwAAAA1TdW5zZWVkIEJlcnJ5',
// Citrus Lump.
'Dw8AAAAA/oGNIQ8LEyUCB4gh0IJgBFEQZIDBfwAAAAtDaXRydXMgTHVtcA==',
// Juice.
'FA/wBwwG/gMBgf8gkUiQBED8H/2X/kv5pf9SfumplFXyJwGQAQz/AQVKdWljZQ==',
// Rock Pikmin.
'FBQA/ADwH8CPA/4x8B+OGJus9cgYl/x/6f+v/v/9/x/9f5H/F/s/E/8r4B8AtgFADADPAwtSb2NrIFBpa21pbg==',
// Pellet Posy.
'GQ8AAFABVAG9QT/R/PCedF8cF/oL+waTAbYASAAUAA4ABYACwAEQAagAz0CooKbgDAtQZWxsZXQgUG9zeQ==',
// Spicy Spray.
'FBQABgBgAAA+AGAAAA8A/APgfwABCAgAgQAQBADCzCy8d8PuLbi7gf8f8P8A/gfAPwDwAAtTcGljeSBTcHJheQ==',
// Winged Pikmin.
'FBQAOAN4fMD4CeO0GIPMMHgEPuBOfu9t7I6CBDDIAIz4gQ2YMEAtAlIioGYGREsADAcAAA1XaW5nZWQgUGlrbWlu',
// Pyroclasmic Slooch.
'Hh4ACAAAjg8AgCYCACCHAACMYQAAYxgAADAAAAAMAAAABwAAQAEAAPgAAAAyAADAHwAAEM6DAx6//8H8k/NY4N8z53/uSYZ/Y3IHlrm0/2e+H/7I1z8A//T5PwO+A3AA/h8PANb/AQANAACAPQAAwAcAABJQeXJvY2xhc21pYyBTbG9vY2g=',
// Sandbelching Meerslug.
'GRkA4AAA/A8A/v8A/+8DE/8PAvg/BOB52MLh4J/jQYv/gwD/BwP/D/j/H8D/PwD+fwDg/wCA/wEA/wMA/gcA/g/A/B+A/x/QfRR4AAAoAAAACE1lZXJzbHVn',
// Waddlepuss.
'FB4A+AcAADEOAPiPDwDx/wRAjnMC8OP4AP5/fsD//z84H/8ft7//59rU97809f1PTnx+x4e/OQL4j1kA+HsYh/wCiGGeABw4MAD48wcKV2FkZGxlcHVzcw==',
// Plasm Wraith.
'HhSAHwD+D/DPgf85/BjmB+N+cP4H/3/w/4/////3/wX/T+D8DODPAP4Y4I8D/zjwjwf/cPgPh/99+P/H/1/8/8X/T3j+BC8nwHIBP34MUGxhc20gV3JhaXRo',
// Skutterchuck.
'Hh4AAAAAAMgA\AADNAACwSAAAchUAQCQKALBUBAAqFwJAJIIAUIQgAIhACAwSYQKMlPAMTNmwx1Qi9A1anO+Ay/8b4P3/DPj1nwG+/UHAjQs4uP8CBvOAgIAHEBAAAAQDAIAgAAAgBAAAAAEAAEAAAAAAAAxTa3V0dGVyY2h1Y2s=',
// S.S. Drake.
'Hh4AAIAADwAg4AQACHgCAAK+8IeAPwYjwMtmCQCZuQPA9/4AGP/vAAMASGAAAiOEQAMSEfAQRYR4SBEQHkIMhAcQHgDBA/sRz4DBfxzgAeAByP9/gIs5IUCBZBBw8B8EEAYMAcQARgARABHAA4AHAAAAAApTLlMuIERyYWtl',
// Fire-Breathing Feast.
'Hh4AADg4AIDJCWAwPwFM4lCAeh8SkP25DLKPp0Pe/Nnc8H+qS/yfrjz/o+nf/2z59/9K//2Bw38/4PE/c3h8QD9Wz/eflfz9Z4V//ynz33/P/vPPsfn+Exy8TwI5d/iA3wMe8AeGA/j19wN8/c0A1r8cABRGaXJlLUJyZWF0aGluZyBGZWFzdA==',
// The 3 Koppaites.
'HigADwDgB+Z/BvgNGY8J/BsJAAn+GwEACD884TiIH3hSVYQP+BJHxD/O4jjEr8pCEMx3x0IQnAfhgg98h3DnOH4CIA8CvwUQfvAAmgj8bwAsBPAtx+kD+I0oYg/4j6piN8SPOOJjhh/HEcAFLkQIiAVgOATwDUAAHFgYgBB+PigAx/1/BM7//z8EMe/5H4IQ7tATghA4EBAPVGhlIDMgS29wcGFpdGVz',
];
// Sprite data in the spritesheet.
const SPRITES = {
// Blank cell.
CELL_BLANK: {
x: 0, y: 0, width: 32, height: 32
},
// Filled cell.
CELL_FILLED: {
x: 32, y: 0, width: 32, height: 32
},
// Marked cell.
CELL_MARKED: {
x: 64, y: 0, width: 32, height: 32
},
// Logo.
LOGO: {
x: 96, y: 0, width: 200, height: 47
},
// Hocotate background.
HOCOTATE: {
x: 0, y: 47, width: 800, height: 800
},
// Koppai background.
KOPPAI: {
x: 800, y: 47, width: 800, height: 800
}
};
// Screen section coordinate information.
const sectionCoords = {
// Main menu header.
mainMenuHeader: {
x: 0,
y: 0,
width: CANVAS.WIDTH,
height: CANVAS.HEIGHT * 0.2
},
// Main menu level selection.
levelSelect: {
x: 0,
y: CANVAS.HEIGHT * 0.2,
width: CANVAS.WIDTH,
height: CANVAS.HEIGHT * 0.8
},
// Board coordinates.
board: {
x: 0,
y: 0,
width: 0,
height: 0
},
// Miniature coordinates.
miniature: {
x: 0,
y: 0,
width: 0,
height: 0
},
// Row hints coordinates.
rowBanner: {
x: 0,
y: 0,
width: 0,
height: 0
},
// Column hints coordinates.
colBanner: {
x: 0,
y: 0,
width: 0,
height: 0
},
// Footer coordinates.
footer: {
x: 0,
y: 0,
width: 0,
height: 0
},
// Panel.
panel: {
x: CANVAS.WIDTH * 0.2,
y: CANVAS.HEIGHT * 0.2,
width: CANVAS.WIDTH * 0.6,
height: CANVAS.HEIGHT * 0.6
}
};
//Coordinates for GUI items.
const guiItemCoords = {
// Title, in the main menu.
mainMenuTitle: {
x: 0.3,
y: 0.2,
width: 0.4,
height: 0.4
},
// Continue button in the main menu.
mainMenuContinue: {
x: 0.025,
y: 0.12,
width: 0.20,
height: 0.30
},
// Info button in the main menu.
mainMenuInfo: {
x: 0.025,
y: 0.58,
width: 0.20,
height: 0.30
},
// Make custom button in the main menu.
mainMenuMake: {
x: 0.775,
y: 0.12,
width: 0.20,
height: 0.30
},
// Play custom button in the main menu.
mainMenuCustom: {
x: 0.775,
y: 0.58,
width: 0.20,
height: 0.30
},
// Swap side button in the main menu.
mainMenuSide: {
x: 0.40,
y: 0.65,
width: 0.20,
height: 0.22
},
// Pause button.
pause: {
x: SECTION_PADDING,
y: SECTION_PADDING,
width: 45,
y2: -SECTION_PADDING
},
// Title, in the playing state.
playingTitle: {
x: 0.425,
y: 0.10,
width: 0.15,
height: 0.40
},
// Level number, in the playing state.
playingLevel: {
x: 0.00,
y: 0.50,
width: 1.00,
height: 0.40
},
// Zoom in button.
zoomIn: {
x: -110,
y: SECTION_PADDING,
width: 45,
y2: -SECTION_PADDING
},
// Zoom out button.
zoomOut: {
x: -55,
y: SECTION_PADDING,
width: 45,
y2: -SECTION_PADDING
},
// Pause panel continue button.
continue: {
x: 0.2,
y: 0.1,
width: 0.6,
height: 0.15
},
// Pause panel restart button.
restart: {
x: 0.2,
y: 0.3,
width: 0.6,
height: 0.15
},
// Pause panel finish button.
finish: {
x: 0.2,
y: 0.5,
width: 0.6,
height: 0.15
},
// Pause panel quit button.
quit: {
x: 0.2,
y: 0.75,
width: 0.6,
height: 0.15
},
// General panel ok button.
ok: {
x: 0.2,
y: 0.75,
width: 0.6,
height: 0.15
},
// Congrats panel header text.
congratsHeader: {
x: 0.2,
y: 0.05,
width: 0.6,
height: 0.15
},
// Congrats panel puzzle miniature.
congratsMiniature: {
x: 0.2,
y: 0.20,
width: 0.6,
height: 0.40
},
// Congrats panel level name text.
congratsLevelName: {
x: 0.2,
y: 0.60,
width: 0.6,
height: 0.15
},
// New puzzle panel name prompt.
newPuzzleNamePrompt: {
x: 0.0,
y: 0.2,
width: 1.0,
height: 0.1
},
// New puzzle panel done text.
newPuzzleDoneText: {
x: 0.0,
y: 0.2,
width: 1.0,
height: 0.1
},
// Custom puzzle panel explanation text.
customPuzzleText: {
x: 0.0,
y: 0.2,
width: 1.0,
height: 0.1
},
// Load error explanation 1 text.
loadError1: {
x: 0.0,
y: 0.2,
width: 1.0,
height: 0.1
},
// Load error explanation 2 text.
loadError2: {
x: 0.0,
y: 0.4,
width: 1.0,
height: 0.1
},
// Load error explanation 3 text.
loadError3: {
x: 0.0,
y: 0.5,
width: 1.0,
height: 0.1
},
// Bookmark warning explanation 1 text.
bookmarkWarning1: {
x: 0.0,
y: 0.15,
width: 1.0,
height: 0.1
},
// Bookmark warning explanation 2 text.
bookmarkWarning2: {
x: 0.0,
y: 0.20,
width: 1.0,
height: 0.1
},
// Bookmark warning explanation 3 text.
bookmarkWarning3: {
x: 0.0,
y: 0.25,
width: 1.0,
height: 0.1
},
// Bookmark warning explanation 4 text.
bookmarkWarning4: {
x: 0.0,
y: 0.30,
width: 1.0,
height: 0.1
},
// Bookmark warning explanation 5 text.
bookmarkWarning5: {
x: 0.0,
y: 0.35,
width: 1.0,
height: 0.1
},
// Bookmark warning go back button.
bookmarkWarningBack: {
x: 0.2,
y: 0.55,
width: 0.6,
height: 0.15
},
// Bookmark warning play level button.
bookmarkWarningStart: {
x: 0.2,
y: 0.75,
width: 0.6,
height: 0.15
},
// Info 1 text.
info1: {
x: 0.0,
y: 0.15,
width: 1.0,
height: 0.1
},
// Info 2 text.
info2: {
x: 0.0,
y: 0.30,
width: 1.0,
height: 0.1
},
// Info 3 text.
info3: {
x: 0.0,
y: 0.35,
width: 1.0,
height: 0.1
},
// Info 4 text.
info4: {
x: 0.0,
y: 0.40,
width: 1.0,
height: 0.1
},
// Info 5 text.
info5: {
x: 0.0,
y: 0.45,
width: 1.0,
height: 0.1
},
};
// Camera information.
let cam = {
// Current coordinates. Used for panning.
coords: {x: 0, y: 0},
// Current zoom level.
zoom: 1.0
};
// Player input information. Is controlled by both the mouse and mobile touches.
let input = {
// Coordinates in the game world.
worldCoords: {x: 0, y: 0},
// Coordinates on-screen, with 0,0 being the top-left of the canvas.
screenCoords: {x: 0, y: 0},
// Is the player currently dragging?
dragging: false,
// X/Y coordinates of where the dragging started.
dragStart: {x: 0, y: 0},
// Lock either coordinate when dragging.
dragLockCoord: {x: false, y: false},
// What is the player currently doing to the cells? -1 means nothing.
dragAction: -1,
// Is the player dragging to pan the view?
dragPanning: false
};
// Board information.
let board = {
// Number of rows. Cache for convenience.
nrRows: 0,
// Number of columns. Cache for convenience.
nrCols: 0,
// State of every cell.
cells: [],
// Solution.
solution: [],
// Hints for each row.
rowHints: [],
// Hints for each column.
colHints: [],
// Whether a given row is auto-marked.
autoMarkedRows: [],
// Whether a given column is auto-marked.
autoMarkedCols: [],
// Whether a given row is defied.
defiedRows: [],
// Whether a given column is defied.
defiedCols: [],
// Puzzle name.
name: '',
// Level number. 0 means custom.
levelNumber: 0,
// Level code.
levelCode: '',
};
// Array of true/false, representing whether a given level's been cleared.
let progression = [];
// Array with data about all levels.
let levelsData = [];
// Canvas object.
let canvas;
// Canvas HTML element.
let canvasEl;
// Input box HTML element.
let inputEl;
// Spritesheet image.
let sprites;
// What things have been loaded. If any of these are false, the game's not ready. Use the LOAD_CONTENTS_ constants.
let loaded = [false, false];
// Game state.
let state = STATE_MAIN_MENU;
// Are we currently showing the panel?
let inPanel = false;
// State of the panel.
let panelState = PANEL_STATE_PAUSE;
// Gradients to show on the borders of the board, when parts of the board are off-camera.
let borderGradients = {
left: null,
right: null,
up: null,
down: null
}
// Have we warned the player of their pending bookmark yet?
let gaveBookmarkWarning = false;
// Current game side. 0 for Hocotate, 1 for Koppai.
let gameSide = 0;
// Scene transition time left.
let transitionAnimTime = 0;
// Transition phase.
let transitionPhase = 0;
// State to change to after the transition.
let transitionNewState = 0;
// Transition code to run after the state change. undefined for none.
let transitionCodeAfter = undefined;
/**
* Code to run when the player presses Ok when the panel shows the input box.
*/
function acceptInputBox() {
let input = inputEl.value;
input = input.replace(/[^\x00-\x7F]/g, "");
switch(panelState) {
case PANEL_STATE_MAKING_NAME:
if(inputEl.value.length == 0) return;
if(input.length == 0) input = 'Puzzle';
board.name = input;
var boardData = {
nrRows: board.nrRows,
nrCols: board.nrCols,
cells: board.cells,
name: board.name
};
inputEl.value = encodeBoard(boardData, BOARD_DATA_CONTEXT_LEVEL);
panelState = PANEL_STATE_MAKING_CODE;
showInputBox(true);
break;
case PANEL_STATE_MAKING_CODE:
hideInputBox();
inPanel = false;
break;
case PANEL_STATE_PLAYING_CODE:
hideInputBox();
if(inputEl.value.length == 0) {
inPanel = false;
} else {
if(loadLevel(0, input, STATE_PLAYING)) {
changeState(STATE_PLAYING, function() { inPanel = false; });
clearBookmark();
} else {
panelState = PANEL_STATE_LOAD_ERROR;
}
}
break;
}
updateCanvas();
}
/**
* Checks if all hints in a row/column have been filled by the player.
* @param {array} hints Array of hints to check.
*/
function allHintsAreFilled(hints) {
for(var h = 0; h < hints.length; h++) {
if(hints[h].state != CELL_FILLED) return false;
}
return true;
}
/**
* Given an X or Y coordinate of a GUI item inside of some parent coordinates, this returns
* what the final on-screen coordinates are, depending on how the GUI item's coordinates are formatted.
* This function can take either X coordinates and widths, or it can take Y coordinates and heights.
* 1 to infinity means the item is offset these many pixels from the parent's start.
* 0 to 1 means the item is within this ratio of parent size, starting from the parent's start.
* -1 to 0 means the item is within this ratio of parent size, starting from the parent's end.
* -Infinity to -1 means the item is offset these many pixels from the parent's end.
* @param {number} coord Coordinate to calculate.
* @param {number} parentStart Start coordinate of the parent.
* @param {number} parentSize Size of the parent.
* @returns The calculated final coordinate.
*/
function calculateChildCoord(coord, parentStart, parentSize) {
let parentEnd = parentStart + parentSize;
if(coord > 1.00) {
// Pixel offset from start.
return parentStart + coord;
} else if(coord >= 0.00) {
// Ratio of size from start.
return parentStart + (parentSize * coord);
} else if(coord > -1.00) {
// Ratio of size from end.
return parentEnd - (parentSize * -coord);
} else {
// Pixel offset from end.
return parentEnd - -coord;
}
}
/**
* Returns the final X and Y coordinates of a child object, making use of calculateChildCoord.
* @param {object} coords Object with the child coordinates.
* @param {object} parentCoords Object with the parent coordinates.
*/
function calculateChildCoords(coords, parentCoords) {
return {
x: calculateChildCoord(coords.x, parentCoords.x, parentCoords.width),
y: calculateChildCoord(coords.y, parentCoords.y, parentCoords.height)
};
}
/**
* Given a width or height of a GUI item inside of some parent coordinates, this returns
* what the final on-screen size is, depending on how the GUI item's width/height is formatted.
* This function can take either X coordinates and widths, or it can take Y coordinates and heights.
* 1 to infinity means the item's size is these many pixels.
* 0 to 1 means the item is this ratio of parent size.
* @param {number} coord Coordinate to calculate.
* @param {number} parentSize Size of the parent.
* @returns The calculated final size.
*/
function calculateChildDimension(coord, parentSize) {
if(coord > 1.00) {
// Pixel size.
return coord;
} else if(coord > 0.00) {
// Ratio size.
return parentSize * coord;
}
}
/**
* Returns the final X and Y dimensions of a child object, making use of calculateChildDimension.
* @param {object} coords Object with the child coordinates.
* @param {object} finalXY The object's final X and Y coordinates, calculated elsewhere.
* @param {object} parentCoords Object with the parent coordinates.
*/
function calculateChildDimensions(coords, finalXY, parentCoords) {
let finalCoords = {};
if(coords.x2 == undefined) {
finalCoords.width = calculateChildDimension(coords.width, parentCoords.width);
} else {
let x2 = calculateChildCoord(coords.x2, parentCoords.x, parentCoords.width);
finalCoords.width = x2 - finalXY.x;
}
if(coords.y2 == undefined) {
finalCoords.height = calculateChildDimension(coords.height, parentCoords.height);
} else {
let y2 = calculateChildCoord(coords.y2, parentCoords.y, parentCoords.height);
finalCoords.height = y2 - finalXY.y;
}
return finalCoords;
}
/**
* Changes the state of a cell, and updates everything accordingly.
* @param {number} row Cell row number.
* @param {number} col Cell column number.
* @param {number} newState The cell's new state.
* @returns True if the cell changed, false otherwise.
*/
function changeCell(row, col, newState) {
if(board.cells[row][col] == newState) {
return false;
}
board.cells[row][col] = newState;
if(state == STATE_PLAYING) {
updateRow(row);
updateColumn(col);
let solved = true;
for(var r = 0; r < board.nrRows; r++) {
for(var c = 0; c < board.nrCols; c++) {
var cellState = board.cells[r][c];
if(cellState == CELL_MARKED) cellState = CELL_BLANK;
if(cellState != board.solution[r][c]) {
solved = false;
break;
}
}
if(!solved) break;
}
if(solved) {
if(board.levelNumber != 0) {
progression[board.levelNumber - 1] = true;
saveProgression();
}
clearBookmark();
inPanel = true;
panelState = PANEL_STATE_CONGRATS;
} else {
saveBookmark();
}
}
return true;
}
/**
* Changes the current game state with a transition.
* @param {number} newState New state.
* @param {function} codeAfter Code to run after the transition.
*/
function changeState(newState, codeAfter) {
transitionNewState = newState;
transitionAnimTime = TRANSITION_DURATION;
transitionPhase = 0;
transitionCodeAfter = codeAfter;
}
/**
* Clamps the camera coordinates to make sure they don't go too far to the left, right, up, or down.
* Also snaps to 0,0 if it's close enough.
*/
function clampCamera() {
cam.coords.x = Math.min(cam.coords.x, (board.nrCols - 1) * (CELL_SIZE + CELL_PADDING));
cam.coords.x = Math.max(cam.coords.x, -(sectionCoords.board.width - CELL_SIZE));
cam.coords.y = Math.min(cam.coords.y, (board.nrRows - 1) * (CELL_SIZE + CELL_PADDING));
cam.coords.y = Math.max(cam.coords.y, -(sectionCoords.board.height - CELL_SIZE));
cam.zoom = Math.max(cam.zoom, 0.1);
cam.zoom = Math.min(cam.zoom, 5);
}
/**
* Debug function that automatically marks all levels as cleared.
* This does not save the player's progression.
*/
function clearAllLevels() {
for(var l = 0; l < progression.length; l++) {
progression[l] = true;
}
updateCanvas();
}
/**
* Clears the player's bookmark.
*/
function clearBookmark() {
localStorage.setItem('pikcrossBookmark', '');
}
/**
* Decodes a base64-encoded string into a board.
* @param {string} data Encoded board text.
* @param {number} context BOARD_DATA_CONTEXT_LEVEL to decode level data. BOARD_DATA_CONTEXT_BOOKMARK to decode bookmark data.
* @returns Board data. Returns null on error.
*/
function decodeBoard(data, context) {
let result = {
nrRows: 0,
nrCols: 0,
cells: [],
name: '',
nrRowHints: 0,
rowHints: [],
nrColHints: 0,
colHints: [],
levelNumber: 0,
levelCode: '',
}
let nrCellBits = context == BOARD_DATA_CONTEXT_LEVEL ? 1 : 2;
try {
let reader = new StrBitReader(atob(data));
result.nrRows = reader.readNumber();
result.nrCols = reader.readNumber();
for(var r = 0; r < result.nrRows; r++) {
result.cells.push([]);
for(var c = 0; c < result.nrCols; c++) {
result.cells[r].push(reader.readNumber(nrCellBits));
}
}
if(context == BOARD_DATA_CONTEXT_LEVEL) {
let nrNameChars = reader.readNumber();
for(var c = 0; c < nrNameChars; c++) {
result.name += String.fromCharCode(reader.readNumber());
}
} else {
result.nrRowHints = reader.readNumber();
result.nrColHints = reader.readNumber();
for(var r = 0; r < result.nrRowHints; r++) {
result.rowHints.push(reader.readNumber(1));
}
for(var c = 0; c < result.nrColHints; c++) {
result.colHints.push(reader.readNumber(1));
}
result.levelNumber = reader.readNumber();
let nrCodeChars = reader.readNumber();
for(var c = 0; c < nrCodeChars; c++) {
result.levelCode += String.fromCharCode(reader.readNumber());
}
}
return result;
} catch {
return null;
}
}
/**
* Draws the board game screen.
*/
function drawBoard() {
const boardX2 = sectionCoords.board.x + sectionCoords.board.width;
const boardY2 = sectionCoords.board.y + sectionCoords.board.height;
// Board, one cell at a time.
canvas.save();
canvas.beginPath();
canvas.rect(sectionCoords.board.x, sectionCoords.board.y, sectionCoords.board.width, sectionCoords.board.height);
canvas.clip();
drawSprite(gameSide == 0 ? SPRITES.HOCOTATE : SPRITES.KOPPAI, sectionCoords.board.x, sectionCoords.board.y, sectionCoords.board.width, sectionCoords.board.height);
canvas.fillStyle = COLOR_BOARD_BG;
canvas.fillRect(sectionCoords.board.x, sectionCoords.board.y, sectionCoords.board.width, sectionCoords.board.height);
for(var r = 0; r < board.nrRows; r++) {
for(var c = 0; c < board.nrCols; c++) {
let startX = sectionCoords.board.x + c * CELL_SIZE + c * CELL_PADDING - cam.coords.x;
let startY = sectionCoords.board.y + r * CELL_SIZE + r * CELL_PADDING - cam.coords.y;
let sprite = null;
if(board.cells[r][c] == CELL_BLANK) {
if(board.autoMarkedRows[r] || board.autoMarkedCols[c]) {
sprite = SPRITES.CELL_MARKED;
} else {
sprite = SPRITES.CELL_BLANK;
}
} else if(board.cells[r][c] == CELL_FILLED) {
sprite = SPRITES.CELL_FILLED;
} else {
sprite = SPRITES.CELL_MARKED;
}
drawSprite(sprite, startX, startY, CELL_SIZE, CELL_SIZE);
}
}
// Board fives grid.
canvas.lineWidth = 3;
for(var r = 5; r < board.nrRows; r += 5) {
let y = sectionCoords.board.y + r * CELL_SIZE + r * CELL_PADDING - cam.coords.y;
let minX = sectionCoords.board.x - cam.coords.x;
let maxX = sectionCoords.board.x + board.nrCols * CELL_SIZE + board.nrCols * CELL_PADDING - cam.coords.x;
canvas.beginPath();
canvas.moveTo(minX, y);
canvas.lineTo(maxX, y);
canvas.stroke();
}
for(var c = 5; c < board.nrCols; c += 5) {
let x = sectionCoords.board.x + c * CELL_SIZE + c * CELL_PADDING - cam.coords.x;
let minY = sectionCoords.board.y - cam.coords.y
let maxY = sectionCoords.board.y + board.nrRows * CELL_SIZE + board.nrRows * CELL_PADDING - cam.coords.y;
canvas.beginPath();
canvas.moveTo(x, minY);
canvas.lineTo(x, maxY);
canvas.stroke();
}
canvas.font = 'bold 12px sans';
canvas.fillStyle = '#888';
canvas.textAlign = 'right';
canvas.textBaseline = 'bottom';
for(var r = 5; r < board.nrRows; r += 5) {
let y = sectionCoords.board.y + r * CELL_SIZE + r * CELL_PADDING - cam.coords.y;
let maxX = sectionCoords.board.x + board.nrCols * CELL_SIZE + board.nrCols * CELL_PADDING - cam.coords.x;
maxX = Math.min(maxX, boardX2);
canvas.fillText(r, maxX - 1, y);
}
canvas.textAlign = 'right';
canvas.textBaseline = 'bottom';
for(var c = 5; c < board.nrCols; c += 5) {
let x = sectionCoords.board.x + c * CELL_SIZE + c * CELL_PADDING - cam.coords.x;
let maxY = sectionCoords.board.y + board.nrRows * CELL_SIZE + board.nrRows * CELL_PADDING - cam.coords.y;
maxY = Math.min(maxY, boardY2);
canvas.fillText(c, x - 1, maxY);
}
// Board border gradients.
if(cam.coords.x > 0) {
canvas.fillStyle = borderGradients.left;
canvas.fillRect(sectionCoords.board.x, sectionCoords.board.y - cam.coords.y, BORDER_GRADIENT_LENGTH, board.nrRows * (CELL_SIZE + CELL_PADDING));
}
if(cam.coords.x < -(sectionCoords.board.width - board.nrCols * (CELL_SIZE + CELL_PADDING))) {
canvas.fillStyle = borderGradients.right;
canvas.fillRect(boardX2 - BORDER_GRADIENT_LENGTH, sectionCoords.board.y - cam.coords.y, boardX2, board.nrRows * (CELL_SIZE + CELL_PADDING));
}
if(cam.coords.y > 0) {
canvas.fillStyle = borderGradients.up;
canvas.fillRect(sectionCoords.board.x - cam.coords.x, sectionCoords.board.y, board.nrCols * (CELL_SIZE + CELL_PADDING), BORDER_GRADIENT_LENGTH);
}
if(cam.coords.y < -(sectionCoords.board.height - board.nrRows * (CELL_SIZE + CELL_PADDING))) {
canvas.fillStyle = borderGradients.down;
canvas.fillRect(sectionCoords.board.x - cam.coords.x, boardY2 - BORDER_GRADIENT_LENGTH, board.nrCols * (CELL_SIZE + CELL_PADDING), boardY2);
}
canvas.restore();
// Miniature.
canvas.fillStyle = COLOR_BOARD_BG;
canvas.fillRect(sectionCoords.miniature.x, sectionCoords.miniature.y, sectionCoords.miniature.width, sectionCoords.miniature.height);
drawMiniature(board.nrRows, board.nrCols, board.cells, {x: 0, y: 0, width: 1, height: 1}, sectionCoords.miniature, COLOR_TEXT_GENERIC[gameSide]);
if(state == STATE_PLAYING) {
// Hints.
canvas.font = 'bold 24px sans';
// Row hints.
let cellWidth = CELL_SIZE;
let cellHeight = CELL_SIZE;
canvas.save();
canvas.beginPath();
canvas.rect(sectionCoords.rowBanner.x, sectionCoords.rowBanner.y, sectionCoords.rowBanner.width, sectionCoords.rowBanner.height);
canvas.clip();
canvas.fillStyle = COLOR_HINTS_BG;
canvas.fillRect(sectionCoords.rowBanner.x, sectionCoords.rowBanner.y, sectionCoords.rowBanner.width, sectionCoords.rowBanner.height);
for(r = 0; r < board.nrRows; r++) {
for(h = 0; h < board.rowHints[r].length; h++) {
let xIndexOffset = board.rowHints[r].length - h - 1;
let startX = sectionCoords.rowBanner.x + sectionCoords.rowBanner.width - cellWidth;
startX -= xIndexOffset * cellWidth + xIndexOffset * CELL_PADDING;
let startY = sectionCoords.rowBanner.y + r * cellWidth + r * CELL_PADDING - cam.coords.y;
let sprite = null;
if(board.rowHints[r][h].state == CELL_BLANK) {
sprite = SPRITES.CELL_BLANK;
} else {
sprite = SPRITES.CELL_FILLED;
}
drawSprite(sprite, startX, startY, cellWidth, cellHeight);
if(board.defiedRows[r]) {
canvas.lineWidth = 5;
canvas.strokeStyle = COLOR_HINT_DEFIED_TINT;
canvas.strokeRect(startX, startY, cellWidth, cellHeight);
canvas.fillStyle = COLOR_HINT_DEFIED;
} else {
canvas.fillStyle = COLOR_HINT[gameSide];
}
canvas.fillText(board.rowHints[r][h].nr, startX + cellWidth / 2, startY + cellHeight / 2 + 2);
}
}
canvas.restore();
// Column hints.
cellWidth = CELL_SIZE;
cellHeight = CELL_SIZE;
canvas.save();
canvas.beginPath();
canvas.rect(sectionCoords.colBanner.x, sectionCoords.colBanner.y, sectionCoords.colBanner.width, sectionCoords.colBanner.height);
canvas.clip();
canvas.fillStyle = COLOR_HINTS_BG;
canvas.fillRect(sectionCoords.colBanner.x, sectionCoords.colBanner.y, sectionCoords.colBanner.width, sectionCoords.colBanner.height);
for(c = 0; c < board.nrCols; c++) {
for(h = 0; h < board.colHints[c].length; h++) {
let yIndexOffset = board.colHints[c].length - h - 1;
let startX = sectionCoords.colBanner.x + c * cellWidth + c * CELL_PADDING - cam.coords.x;
let startY = sectionCoords.colBanner.y + sectionCoords.colBanner.height - cellHeight;
startY -= yIndexOffset * cellHeight + yIndexOffset * CELL_PADDING;
let sprite = null;
if(board.colHints[c][h].state == CELL_BLANK) {
sprite = SPRITES.CELL_BLANK;
} else {
sprite = SPRITES.CELL_FILLED;
}
drawSprite(sprite, startX, startY, cellWidth, cellHeight);
if(board.defiedCols[c]) {
canvas.lineWidth = 5;
canvas.strokeStyle = COLOR_HINT_DEFIED_TINT;
canvas.strokeRect(startX, startY, cellWidth, cellHeight);
canvas.fillStyle = COLOR_HINT_DEFIED;
} else {
canvas.fillStyle = COLOR_HINT[gameSide];
}
canvas.fillText(board.colHints[c][h].nr, startX + cellWidth / 2, startY + cellHeight / 2 + 2);
}
}
canvas.restore();
} else if (state == STATE_MAKING) {
// Maker grid tools.
canvas.font = 'bold ' + (CELL_SIZE - 2) + 'px sans';
canvas.textAlign = 'center';
canvas.textBaseline = 'middle';
// Row hints.
canvas.save();
canvas.beginPath();
canvas.rect(sectionCoords.rowBanner.x, sectionCoords.rowBanner.y, sectionCoords.rowBanner.width, sectionCoords.rowBanner.height);
canvas.clip();
canvas.fillStyle = COLOR_HINTS_BG;
canvas.fillRect(sectionCoords.rowBanner.x, sectionCoords.rowBanner.y, sectionCoords.rowBanner.width, sectionCoords.rowBanner.height);
for(r = 0; r < board.nrRows; r++) {
for(b = 0; b < 3; b++) {
let xIndexOffset = 3 - b - 1;
let startX = sectionCoords.rowBanner.x + sectionCoords.rowBanner.width - CELL_SIZE;
startX -= xIndexOffset * CELL_SIZE + xIndexOffset * CELL_PADDING;
let startY = sectionCoords.rowBanner.y + r * CELL_SIZE + r * CELL_PADDING - cam.coords.y;
let sprite = SPRITES.CELL_BLANK;
drawSprite(sprite, startX, startY, CELL_SIZE, CELL_SIZE);
let text = '';
switch(b) {
case 0:
canvas.fillStyle = COLOR_MAKER_RED;
text = 'x'
break;
case 1:
canvas.fillStyle = COLOR_MAKER_GREEN;
text = '↑'
break;
case 2:
canvas.fillStyle = COLOR_MAKER_GREEN;
text = '↓';
break;
}
canvas.fillText(text, startX + CELL_SIZE / 2, startY + CELL_SIZE / 2 + 2);
}
}
canvas.restore();
// Column hints.
canvas.save();
canvas.beginPath();
canvas.rect(sectionCoords.colBanner.x, sectionCoords.colBanner.y, sectionCoords.colBanner.width, sectionCoords.colBanner.height);
canvas.clip();
canvas.fillStyle = COLOR_HINTS_BG;
canvas.fillRect(sectionCoords.colBanner.x, sectionCoords.colBanner.y, sectionCoords.colBanner.width, sectionCoords.colBanner.height);
for(c = 0; c < board.nrCols; c++) {
for(b = 0; b < 3; b++) {
let yIndexOffset = 3 - b - 1;
let startX = sectionCoords.colBanner.x + c * CELL_SIZE + c * CELL_PADDING - cam.coords.x;
let startY = sectionCoords.colBanner.y + sectionCoords.colBanner.height - CELL_SIZE;
startY -= yIndexOffset * CELL_SIZE + yIndexOffset * CELL_PADDING;
let sprite = SPRITES.CELL_BLANK;
drawSprite(sprite, startX, startY, CELL_SIZE, CELL_SIZE);
let text = '';
switch(b) {
case 0:
canvas.fillStyle = COLOR_MAKER_RED;
text = 'x'
break;
case 1:
canvas.fillStyle = COLOR_MAKER_GREEN;
text = '←'
break;
case 2:
canvas.fillStyle = COLOR_MAKER_GREEN;
text = '→';
break;
}
canvas.fillText(text, startX + CELL_SIZE / 2, startY + CELL_SIZE / 2 + 2);
}
}
canvas.restore();
}
// Footer.
canvas.fillStyle = COLOR_FOOTER_BG;
canvas.fillRect(sectionCoords.footer.x, sectionCoords.footer.y, sectionCoords.footer.width, sectionCoords.footer.height);
let logoXY = calculateChildCoords(guiItemCoords.playingTitle, sectionCoords.footer);
let logoWH = calculateChildDimensions(guiItemCoords.playingTitle, logoXY, sectionCoords.footer);
let levelNumberStr = board.levelNumber == 0 ? 'Custom Level' : ('Level ' + board.levelNumber);
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.pause, sectionCoords.footer, '...', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
drawSprite(SPRITES.LOGO, logoXY.x, logoXY.y, logoWH.width, logoWH.height);
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.playingLevel, sectionCoords.footer, levelNumberStr, COLOR_TEXT_GENERIC[gameSide], 'bold 20px sans');
}
/**
* Draws a generic GUI item in specific coordinates, like a button, some text, an image, etc.
* @param {number} type Type of GUI item.
* @param {object} coords Coordinates of the object, relative to the parent.
* @param {object} parentCoords Coordinates of the parent section.
* @param {any} fg Contents of the foreground, if any.
* @param {any} fgStyle Foreground style, if any.
* @param {any} fgFont Foreground content font, if any.
* @param {any} bgStyle Background style. If undefined, the background won't be drawn.
*/
function drawGuiItem(type, coords, parentCoords, fg, fgStyle, fgFont, bgStyle) {
if(parentCoords == undefined) {
parentCoords = {
x: 0,
y: 0,
width: CANVAS.WIDTH,
height: CANVAS.HEIGHT
};
}
let finalXY = calculateChildCoords(coords, parentCoords);
let finalWH = calculateChildDimensions(coords, finalXY, parentCoords);
let finalCoords = {
x: finalXY.x,
y: finalXY.y,
width: finalWH.width,
height: finalWH.height
};
// Button shadow.
if(type == GUI_ITEM_BUTTON && bgStyle != undefined) {
canvas.fillStyle = COLOR_BUTTON_SHADOW[gameSide];
canvas.fillRect(finalCoords.x + 4, finalCoords.y + 4, finalCoords.width, finalCoords.height);
}
// Background.
if(bgStyle != undefined) {
canvas.fillStyle = bgStyle;
canvas.fillRect(finalCoords.x, finalCoords.y, finalCoords.width, finalCoords.height);
}
// Foreground.
if(fg != undefined) {
if(fgStyle != undefined) {
canvas.fillStyle = fgStyle;
}
if(fgFont != undefined) {
canvas.font = fgFont;
}
canvas.fillText(fg, finalCoords.x + finalCoords.width / 2, finalCoords.y + finalCoords.height / 2 + 2);
}
}
/**
* Draws the main menu.
*/
function drawMainMenu() {
// Header.
canvas.fillStyle = COLOR_FOOTER_BG;
canvas.fillRect(sectionCoords.mainMenuHeader.x + SECTION_PADDING, sectionCoords.mainMenuHeader.y + SECTION_PADDING, sectionCoords.mainMenuHeader.width - SECTION_PADDING * 2, sectionCoords.mainMenuHeader.height - SECTION_PADDING * 2);
let bookmarkData = getBookmarkData();
let logoXY = calculateChildCoords(guiItemCoords.mainMenuTitle, sectionCoords.mainMenuHeader);
let logoWH = calculateChildDimensions(guiItemCoords.mainMenuTitle, logoXY, sectionCoords.mainMenuHeader);
drawSprite(SPRITES.LOGO, logoXY.x, logoXY.y, logoWH.width, logoWH.height);
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.mainMenuContinue, sectionCoords.mainMenuHeader, 'Continue', bookmarkData != null ? COLOR_BUTTON_TEXT[gameSide] : COLOR_BUTTON_TEXT_DISABLED, '20px sans', COLOR_BUTTON_BG);
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.mainMenuInfo, sectionCoords.mainMenuHeader, 'Info', COLOR_BUTTON_TEXT[gameSide], '20px sans', COLOR_BUTTON_BG);
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.mainMenuMake, sectionCoords.mainMenuHeader, 'Make custom', COLOR_BUTTON_TEXT[gameSide], '20px sans', COLOR_BUTTON_BG);
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.mainMenuCustom, sectionCoords.mainMenuHeader, 'Play custom', COLOR_BUTTON_TEXT[gameSide], '20px sans', COLOR_BUTTON_BG);
if(firstSideCleared()) drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.mainMenuSide, sectionCoords.mainMenuHeader, 'Swap sides', COLOR_BUTTON_TEXT[gameSide], '20px sans', COLOR_BUTTON_BG);
// Level selection.
drawSprite(gameSide == 0 ? SPRITES.HOCOTATE : SPRITES.KOPPAI, sectionCoords.levelSelect.x + SECTION_PADDING, sectionCoords.levelSelect.y + SECTION_PADDING, sectionCoords.levelSelect.width - SECTION_PADDING * 2, sectionCoords.levelSelect.height - SECTION_PADDING * 2);
canvas.fillStyle = COLOR_BOARD_BG;
canvas.fillRect(sectionCoords.levelSelect.x + SECTION_PADDING, sectionCoords.levelSelect.y + SECTION_PADDING, sectionCoords.levelSelect.width - SECTION_PADDING * 2, sectionCoords.levelSelect.height - SECTION_PADDING * 2);
let buttonPadding = SECTION_PADDING * 2;
let buttonWidth = (sectionCoords.levelSelect.width - buttonPadding * 5) / 4;
let levelHeight = (sectionCoords.levelSelect.height - buttonPadding * 5) / 4;
let buttonHeight = levelHeight * 0.75;
for(var c = 0; c < 4; c++) {
for(var r = 0; r < 4; r++) {
let levelNumber = r * 4 + c + 1;
levelNumber += 16 * gameSide;
let cleared = progression[levelNumber - 1];
drawGuiItem(
GUI_ITEM_BUTTON,
{
x: buttonPadding + c * (buttonWidth + buttonPadding),
y: buttonPadding + r * (levelHeight + buttonPadding),
width: buttonWidth,
height: buttonHeight
},
sectionCoords.levelSelect,
'',
COLOR_BUTTON_TEXT[gameSide],
'bold 48px sans',
COLOR_BUTTON_BG
);
if(cleared) {
drawMiniature(
levelsData[levelNumber - 1].nrRows,
levelsData[levelNumber - 1].nrCols,
levelsData[levelNumber - 1].cells,
{
x: buttonPadding + c * (buttonWidth + buttonPadding) + 8,
y: buttonPadding + r * (levelHeight + buttonPadding) + 8,
width: buttonWidth - 16,
height: buttonHeight - 16
},
sectionCoords.levelSelect,
COLOR_BUTTON_TEXT[gameSide]
);
canvas.textAlign = 'left';
canvas.textBaseline = 'top';
drawGuiItem(
GUI_ITEM_TEXT,
{
x: buttonPadding + c * (buttonWidth + buttonPadding),
y: buttonPadding + r * (levelHeight + buttonPadding),
width: 10,
height: 5
},
sectionCoords.levelSelect,
levelsData[levelNumber - 1].nrRows + 'x' + levelsData[levelNumber - 1].nrCols,
COLOR_BUTTON_SHADOW[gameSide],
'9px sans'
);
}
let levelName = levelNumber + ': ' + (cleared ? levelsData[levelNumber - 1].name : '???');
canvas.textAlign = 'center';
canvas.textBaseline = 'middle';
drawGuiItem(
GUI_ITEM_TEXT,
{
x: buttonPadding + c * (buttonWidth + buttonPadding),
y: buttonPadding + r * (levelHeight + buttonPadding) + buttonHeight + 2,
width: buttonWidth,
height: (levelHeight - buttonHeight)
},
sectionCoords.levelSelect,
levelName,
COLOR_TEXT_GENERIC[gameSide],
'bold 14px sans'
);
}
}
}
/**
* Draws a miniature of the puzzle on-screen.
* @param {number} nrRows Number of rows to draw.
* @param {number} nrCols Number of columns to draw.
* @param {array} cells Cells to draw.
* @param {object} coords X, Y, width, and height of the miniature.
* @param {object} parentCoords Object with the parent coordinates.
* @param {color} color Color of each pixel.
*/
function drawMiniature(nrRows, nrCols, cells, coords, parentCoords, color) {
if(parentCoords == undefined) {
parentCoords = {
x: 0,
y: 0,
width: CANVAS.WIDTH,
height: CANVAS.HEIGHT
};
}
let finalXY = calculateChildCoords(coords, parentCoords);
let finalWH = calculateChildDimensions(coords, finalXY, parentCoords);
let finalCoords = {
x: finalXY.x,
y: finalXY.y,
width: finalWH.width,
height: finalWH.height
};
let miniatureCellNormalWidth = finalCoords.width / nrCols;
let miniatureCellNormalHeight = finalCoords.height / nrRows;
let miniatureCellSize = Math.min(miniatureCellNormalWidth, miniatureCellNormalHeight);
let miniatureFullWidth = miniatureCellSize * nrCols;
let miniatureFullHeight = miniatureCellSize * nrRows;
let miniatureStartX = finalCoords.x + (finalCoords.width - miniatureFullWidth) / 2;
let miniatureStartY = finalCoords.y + (finalCoords.height - miniatureFullHeight) / 2;
for(var r = 0; r < nrRows; r++) {
for(var c = 0; c < nrCols; c++) {
if(cells[r][c] != CELL_FILLED) {
continue;
}
let startX = Math.round(miniatureStartX + c * miniatureCellSize);
let startY = Math.round(miniatureStartY + r * miniatureCellSize);
canvas.fillStyle = color;
canvas.fillRect(
startX,
startY,
Math.ceil(miniatureCellSize),
Math.ceil(miniatureCellSize)
);
}
}
}
/**
* Draws the panel.
*/
function drawPanel() {
// Background.
canvas.fillStyle = 'rgba(0, 0, 0, 0.5)';
canvas.fillRect(0, 0, CANVAS.WIDTH, CANVAS.HEIGHT);
canvas.fillStyle = 'rgba(0, 0, 0, 0.5)';
canvas.fillRect(sectionCoords.panel.x + 8, sectionCoords.panel.y + 8, sectionCoords.panel.width, sectionCoords.panel.height);
canvas.fillStyle = COLOR_FOOTER_BG;
canvas.fillRect(sectionCoords.panel.x, sectionCoords.panel.y, sectionCoords.panel.width, sectionCoords.panel.height);
switch(panelState) {
case PANEL_STATE_PAUSE:
// Menu buttons.
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.continue, sectionCoords.panel, 'Continue', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.restart, sectionCoords.panel, 'Restart', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
if(state == STATE_MAKING) {
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.finish, sectionCoords.panel, 'Finish', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
}
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.quit, sectionCoords.panel, 'Quit', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
break;
case PANEL_STATE_CONGRATS:
// Congrats screen.
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.congratsHeader, sectionCoords.panel, (board.levelNumber == 0 ? 'CUSTOM LEVEL' : ('LEVEL ' + board.levelNumber)) + ' CLEAR!', COLOR_BUTTON_TEXT[gameSide], '36px sans');
drawMiniature(board.nrRows, board.nrCols, board.cells, guiItemCoords.congratsMiniature, sectionCoords.panel, COLOR_BUTTON_TEXT[gameSide]);
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.congratsLevelName, sectionCoords.panel, board.name, COLOR_BUTTON_TEXT[gameSide], 'italic 24px sans');
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
break;
case PANEL_STATE_MAKING_NAME:
// Asking the new puzzle's name.
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.newPuzzleNamePrompt, sectionCoords.panel, 'What\'s this puzzle\'s name?', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
break;
case PANEL_STATE_MAKING_CODE:
// Showing the new puzzle's code.
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.newPuzzleDoneText, sectionCoords.panel, 'Done! Copy this code and share it around!', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
break;
case PANEL_STATE_PLAYING_CODE:
// Asking a puzzle's code to play on.
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.customPuzzleText, sectionCoords.panel, 'Please paste the puzzle\'s code here.', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
break;
case PANEL_STATE_LOAD_ERROR:
// Warning the player there was an error while loading.
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError1, sectionCoords.panel, 'Error loading level!', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError2, sectionCoords.panel, 'Please make sure everything', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError3, sectionCoords.panel, 'is correct and try again.', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
break;
case PANEL_STATE_SAVE_ERROR:
// Warning the player there was an error while saving.
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError1, sectionCoords.panel, 'Error saving level!', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError2, sectionCoords.panel, 'You must have at least', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.loadError3, sectionCoords.panel, 'one filled cell!', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
break;
case PANEL_STATE_BOOKMARK_WARNING:
// Warning the player they have a bookmark when starting a level.
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.bookmarkWarning1, sectionCoords.panel, 'If you start a new puzzle, you\'ll', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.bookmarkWarning2, sectionCoords.panel, 'lose your old bookmark progress! In', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.bookmarkWarning3, sectionCoords.panel, 'the main menu, press "Continue" to', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.bookmarkWarning4, sectionCoords.panel, 'resume your bookmark, or press the', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.bookmarkWarning5, sectionCoords.panel, 'puzzle button again to start anyway.', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
break;
case PANEL_STATE_INFO:
// Game info.
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info1, sectionCoords.panel, '**TODO: INFO GOES HERE**', COLOR_BUTTON_BG, 'bold 20px sans');
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info2, sectionCoords.panel, '( Note added by Pikipedia:', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info3, sectionCoords.panel, 'Information on Pikcross, including', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info4, sectionCoords.panel, 'how to play, can be found here:', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info5, sectionCoords.panel, 'https://www.pikminwiki.com/Pikcross )', COLOR_BUTTON_BG, '20px sans');
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Ok', COLOR_BUTTON_TEXT[gameSide], '24px sans', COLOR_BUTTON_BG);
break;
}
}
/**
* Draws a sprite.
* @param object data Data about the sprite in the spritesheet, in the form {x, y, width, height}.
* @param number x Top-left X coordinate to draw on.
* @param number y Top-left Y coordinate to draw on.
* @param number w Width to draw at.
* @param number h Height to draw at.
*/
function drawSprite(data, x, y, w, h) {
canvas.drawImage(sprites, data.x, data.y, data.width, data.height, x, y, w, h);
}
/**
* Encodes a board and some more data into a base64-encoded string.
* @param {object} data Board data to encode.
* @param {number} context BOARD_DATA_CONTEXT_LEVEL to encode level data. BOARD_DATA_CONTEXT_BOOKMARK to encode bookmark data.
* @returns Encoded board text. Returns null on error.
*/
function encodeBoard(data, context) {
try {
let writer = new StrBitWriter();
let nrCellBits = context == BOARD_DATA_CONTEXT_LEVEL ? 1 : 2;
writer.writeNumber(data.nrRows);
writer.writeNumber(data.nrCols);
for(var r = 0; r < data.nrRows; r++) {
for(var c = 0; c < data.nrCols; c++) {
writer.writeNumber(data.cells[r][c], nrCellBits);
}
}
if(context == BOARD_DATA_CONTEXT_LEVEL) {
writer.writeNumber(data.name.length);
for(var c = 0; c < data.name.length; c++) {
writer.writeNumber(data.name.charCodeAt(c));
}
} else {
writer.writeNumber(data.nrRowHints);
writer.writeNumber(data.nrColHints);
for(var r = 0; r < data.nrRowHints; r++) {
writer.writeNumber(data.rowHints[r], 1);
}
for(var c = 0; c < data.nrColHints; c++) {
writer.writeNumber(data.colHints[c], 1);
}
writer.writeNumber(data.levelNumber);
writer.writeNumber(data.levelCode.length);
for(var c = 0; c < data.levelCode.length; c++) {
writer.writeNumber(data.levelCode.charCodeAt(c));
}
}
return btoa(writer.getStr());
} catch {
return null;
}
}
/**
* Given a list of cells, finds the first cell that is marked. It only checks inside of a given range.
* @param {array} cells List of cells to check in.
* @param {number} startIdx Index to start the check in, inclusive.
* @param {number} length Number of cells to check.
* @returns The index of the first marked cell, or -1 if none are marked.
*/
function findMarkedCellInList(cells, startIdx, length) {
for(var c = startIdx; c < startIdx + length; c++) {
if(c >= cells.length) return c;
if(cells[c] == CELL_MARKED) return c;
}
return -1;
}
/**
* Returns whether the first side (i.e. the first 16 puzzles) is cleared or not.
* @returns True if cleared, false otherwise.
*/
function firstSideCleared() {
for(var l = 0; l < 16; l++) {
if(!progression[l]) return false;
}
return true;
}
/**
* Returns which cell of the board is under the given screen coordinates.
* @param {object} coords Object with the coordinates.
* @returns The row and column, in an array.
*/
function getCellInCoords(coords) {
let x = coords.x - sectionCoords.board.x + cam.coords.x;
let y = coords.y - sectionCoords.board.y + cam.coords.y;
let col = Math.floor(x / (CELL_SIZE + CELL_PADDING));
let row = Math.floor(y / (CELL_SIZE + CELL_PADDING));
return [row, col];
}
/**
* Returns which column hint/maker button is under the given screen coordinates.
* @param {object} coords Object with the coordinates.
* @returns The column and button index numbers, in an array. Returns null if none.
*/
function getColumnBannerButtonInCoords(coords) {
let hintsClickCoords = {
x: coords.x - sectionCoords.colBanner.x + cam.coords.x,
y: coords.y - sectionCoords.colBanner.y
};
let col = Math.floor(hintsClickCoords.x / (CELL_SIZE + CELL_PADDING));
if(col < 0 || col >= board.nrCols) return null;
let button = Math.floor(hintsClickCoords.y / (CELL_SIZE + CELL_PADDING));
let maxButtons = Math.floor(sectionCoords.colBanner.height / (CELL_SIZE + CELL_PADDING));
let nrButtonsInCol = 0;
if(state == STATE_PLAYING) {
nrButtonsInCol = board.colHints[col].length;
} else if(state == STATE_MAKING) {
nrButtonsInCol = 3;
}
button -= maxButtons - nrButtonsInCol;
if(button < 0) return null;
if(state == STATE_PLAYING) {
if(button >= board.colHints[col].length) return null;
} else if(state == STATE_MAKING) {
if(button >= 3) return null;
}
return [col, button];
}
/**
* Returns an array with the combos the player has for the given line.
* @param {number} row Row number, if we're checking a row. null otherwise.
* @param {number} col Column number, if we're checking a column. null otherwise.
* @returns An array where each object is a combo.
*/
function getPlayerLineCombos(row, col) {
let playerCombos = [];
let curCombo = 0;
let curComboStart = 0;
if(row != null) {
for(var c = 0; c < board.nrCols; c++) {
if(board.cells[row][c] == CELL_FILLED) {
if(curCombo == 0) {
curComboStart = c;
}
curCombo++;
} else {
if(curCombo > 0) playerCombos.push({start: curComboStart, nr: curCombo});
curCombo = 0;
}
}
} else {
for(var r = 0; r < board.nrRows; r++) {
if(board.cells[r][col] == CELL_FILLED) {
if(curCombo == 0) {
curComboStart = r;
}
curCombo++;
} else {
if(curCombo > 0) playerCombos.push({start: curComboStart, nr: curCombo});
curCombo = 0;
}
}
}
if(curCombo > 0) playerCombos.push({start: curComboStart, nr: curCombo});
return playerCombos;
}
/**
* Returns which row hint/maker button is under the given screen coordinates.
* @param {object} coords Object with the coordinates.
* @returns The row and button index numbers, in an array. Returns null if none.
*/
function getRowBannerButtonInCoords(coords) {
let hintsClickCoords = {
x: coords.x - sectionCoords.rowBanner.x,
y: coords.y - sectionCoords.rowBanner.y + cam.coords.y
};
let row = Math.floor(hintsClickCoords.y / (CELL_SIZE + CELL_PADDING));
if(row < 0 || row >= board.nrRows) return null;
let button = Math.floor(hintsClickCoords.x / (CELL_SIZE + CELL_PADDING));
let maxButtons = Math.floor(sectionCoords.rowBanner.width / (CELL_SIZE + CELL_PADDING));
let nrButtonsInRow = 0;
if(state == STATE_PLAYING) {
nrButtonsInRow = board.rowHints[row].length;
} else if(state == STATE_MAKING) {
nrButtonsInRow = 3;
}
button -= maxButtons - nrButtonsInRow;
if(button < 0) return null;
if(state == STATE_PLAYING) {
if(button >= board.rowHints[row].length) return null;
} else if(state == STATE_MAKING) {
if(button >= 3) return null;
}
return [row, button];
}
/**
* Hides the input box HTML element in the middle of the canvas.
*/
function hideInputBox() {
inputEl.style.display = 'none';
}
/**
* Checks if a given point is inside a set of coordinates.
* @param point Point to check, in the format {x, y}.
* @param coords Coordinates to check, in the format {x, y, width, height}.
* @param parentCoords If not undefined, then the previous coordinates are relative to these ones.
*/
function isPointInCoords(point, coords, parentCoords) {
let finalCoords = {
x: coords.x,
y: coords.y,
width: coords.width,
height: coords.height
};
if(parentCoords != undefined) {
let finalXY = calculateChildCoords(coords, parentCoords);
let finalWH = calculateChildDimensions(coords, finalXY, parentCoords);
finalCoords = {
x: finalXY.x,
y: finalXY.y,
width: finalWH.width,
height: finalWH.height
};
}
return (
point.x >= finalCoords.x &&
point.y >= finalCoords.y &&
point.x <= finalCoords.x + finalCoords.width &&
point.y <= finalCoords.y + finalCoords.height
);
}
/**
* Loads the player's bookmark, if any, and returns its info.
* @returns null if no data exists, or an object with the data otherwise.
*/
function getBookmarkData() {
var bookmarkString = localStorage.getItem('pikcrossBookmark');
if(bookmarkString == null || bookmarkString == '') return null;
return decodeBoard(bookmarkString, BOARD_DATA_CONTEXT_BOOKMARK);
}
/**
* Loads a level and initializes the board game screen.
* @param {number} levelNumber Level number, or 0 for custom.
* @param {string} levelCode Level code.
* @param {number} stateContext What game state is this being loaded for?
* @returns True on success, false on failure.
*/
function loadLevel(levelNumber, levelCode, stateContext) {
if(stateContext == STATE_PLAYING && levelCode.length < 3) {
return false;
}
let nrRowBannerCells = 0;
let nrColBannerCells = 0;
// Cleanup.
cam.coords.x = 0;
cam.coords.y = 0;
board.name = 'Puzzle';
board.nrRows = 0;
board.nrCols = 0;
board.cells = [];
board.solution = [];
board.rowHints = [];
board.colHints = [];
board.autoMarkedRows = [];
board.autoMarkedCols = [];
board.defiedRows = [];
board.defiedCols = [];
board.levelNumber = levelNumber;
board.levelCode = levelCode;
if(stateContext == STATE_PLAYING) {
// Load board.
let levelData = decodeBoard(levelCode, BOARD_DATA_CONTEXT_LEVEL);
// Sanity check.
if(levelData == null) return false;
if(levelData.nrRows < MIN_COLS_OR_ROWS) return false;
if(levelData.nrRows > MAX_COLS_OR_ROWS) return false;
if(levelData.nrCols < MIN_COLS_OR_ROWS) return false;
if(levelData.nrCols > MAX_COLS_OR_ROWS) return false;
let hasFilledCells = false;
for(var r = 0; r < levelData.nrRows; r++) {
for(var c = 0; c < levelData.nrCols; c++) {
if(levelData.cells[r][c] == CELL_FILLED) {
hasFilledCells = true;
break;
}
}
if(hasFilledCells) break;
}
if(!hasFilledCells) return false;
if(levelData.name.length == 0) levelData.name = 'Puzzle';
board.nrRows = levelData.nrRows;
board.nrCols = levelData.nrCols;
board.solution = levelData.cells;
board.name = levelData.name;
// Hints.
for(var r = 0; r < board.nrRows; r++) {
board.rowHints.push([]);
let hintNr = 0;
for(var c = 0; c < board.nrCols; c++) {
if(board.solution[r][c] == CELL_FILLED) {
hintNr++;
} else {
if(hintNr > 0) {
board.rowHints[r].push({ state: CELL_BLANK, nr: hintNr });
}
hintNr = 0;
}
}
if(hintNr > 0) {
board.rowHints[r].push({ state: CELL_BLANK, nr: hintNr });
}
nrRowBannerCells = Math.max(nrRowBannerCells, board.rowHints[r].length);
}
for(var c = 0; c < board.nrCols; c++) {
board.colHints.push([]);
let hintNr = 0;
for(var r = 0; r < board.nrRows; r++) {
if(board.solution[r][c] == CELL_FILLED) {
hintNr++;
} else {
if(hintNr > 0) {
board.colHints[c].push({ state: CELL_BLANK, nr: hintNr });
}
hintNr = 0;
}
}
if(hintNr > 0) {
board.colHints[c].push({ state: CELL_BLANK, nr: hintNr });
}
nrColBannerCells = Math.max(nrColBannerCells, board.colHints[c].length);
}
} else if(stateContext == STATE_MAKING) {
// Create an empty board.
board.nrRows = 10;
board.nrCols = 10;
for(var r = 0; r < board.nrRows; r++) {
board.cells.push([]);
for(var c = 0; c < board.nrCols; c++) {
board.cells[r].push(CELL_BLANK);
}
}
// Maker grid tools.
nrRowBannerCells = 3;
nrColBannerCells = 3;
}
// Current cells state.
board.cells = [];
for(var r = 0; r < board.nrRows; r++) {
board.cells.push([]);
for(var c = 0; c < board.nrCols; c++) {
board.cells[r].push(CELL_BLANK);
}
}
for(var r = 0; r < board.nrRows; r++) {
board.autoMarkedRows.push(false);
board.defiedRows.push(false);
}
for(var c = 0; c < board.nrCols; c++) {
board.autoMarkedCols.push(false);
board.defiedCols.push(false);
}
// Coordinates.
sectionCoords.miniature.x = SECTION_PADDING;
sectionCoords.miniature.y = SECTION_PADDING;
sectionCoords.miniature.width = nrRowBannerCells * (CELL_SIZE + CELL_PADDING);
sectionCoords.miniature.height = nrColBannerCells * (CELL_SIZE + CELL_PADDING);
sectionCoords.footer.x = sectionCoords.miniature.x;
sectionCoords.footer.y = CANVAS.HEIGHT - SECTION_PADDING - 64;
sectionCoords.footer.width = CANVAS.WIDTH - SECTION_PADDING * 2;
sectionCoords.footer.height = 64;
sectionCoords.rowBanner.x = sectionCoords.miniature.x;
sectionCoords.rowBanner.y = sectionCoords.miniature.y + sectionCoords.miniature.height + SECTION_PADDING;
sectionCoords.rowBanner.width = sectionCoords.miniature.width;
sectionCoords.rowBanner.height = sectionCoords.footer.y - SECTION_PADDING - sectionCoords.rowBanner.y;
sectionCoords.colBanner.x = sectionCoords.miniature.x + sectionCoords.miniature.width + SECTION_PADDING;
sectionCoords.colBanner.y = sectionCoords.miniature.y;
sectionCoords.colBanner.width = CANVAS.WIDTH - SECTION_PADDING - sectionCoords.colBanner.x;
sectionCoords.colBanner.height = sectionCoords.miniature.height;
sectionCoords.board.x = sectionCoords.colBanner.x;
sectionCoords.board.y = sectionCoords.rowBanner.y;
sectionCoords.board.width = sectionCoords.colBanner.width;
sectionCoords.board.height = sectionCoords.rowBanner.height;
// Border gradients.
const boardX2 = sectionCoords.board.x + sectionCoords.board.width;
const boardY2 = sectionCoords.board.y + sectionCoords.board.height;
borderGradients.left = canvas.createLinearGradient(sectionCoords.board.x, 0, sectionCoords.board.x + BORDER_GRADIENT_LENGTH, 0),
borderGradients.left.addColorStop(1, 'rgba(0, 128, 0, 0.0)');
borderGradients.left.addColorStop(0, 'rgba(0, 128, 0, 0.5)');
borderGradients.right = canvas.createLinearGradient(boardX2, 0, boardX2 - BORDER_GRADIENT_LENGTH, 0),
borderGradients.right.addColorStop(1, 'rgba(0, 128, 0, 0.0)');
borderGradients.right.addColorStop(0, 'rgba(0, 128, 0, 0.5)');
borderGradients.up = canvas.createLinearGradient(0, sectionCoords.board.y, 0, sectionCoords.board.y + BORDER_GRADIENT_LENGTH),
borderGradients.up.addColorStop(1, 'rgba(0, 128, 0, 0.0)');
borderGradients.up.addColorStop(0, 'rgba(0, 128, 0, 0.5)');
borderGradients.down = canvas.createLinearGradient(0, boardY2, 0, boardY2 - BORDER_GRADIENT_LENGTH),
borderGradients.down.addColorStop(1, 'rgba(0, 128, 0, 0.0)');
borderGradients.down.addColorStop(0, 'rgba(0, 128, 0, 0.5)');
if(stateContext == STATE_PLAYING) {
for(var r = 0; r < board.nrRows; r++) {
updateRow(r);
}
for(var c = 0; c < board.nrCols; c++) {
updateColumn(c);
}
}
return true;
}
/**
* Loads the player's global progression.
*/
function loadProgression() {
let progressionStr = localStorage.getItem('pikcrossProgression');
if(progressionStr == null || progressionStr.length == 0) {
return;
}
let reader = new StrBitReader(progressionStr);
for(var l = 0; l < LEVELS.length; l++) {
progression[l] = (reader.readNumber(1) == 1);
}
}
/**
* Handler for when the player does an input down on the canvas.
* This happens regardless of it being a mouse button down press, or a mobile touch start.
* @param {number} button What button got pressed. 0 for left click/mobile touch, 2 for right click, other values for other buttons.
* @param {number} mobile True if it was a mobile touch, false otherwise.
*/
function onCanvasInputDown(button, mobile) {
for(var l = 0; l < loaded.length; l++) {
if(!loaded[l]) return;
}
if(transitionAnimTime > 0) return;
if(inPanel) {
onCanvasInputDownInPanel(button, mobile);
return;
}
switch(state) {
case STATE_MAIN_MENU:
onCanvasInputDownInMainMenu(button, mobile);
break;
case STATE_PLAYING:
case STATE_MAKING:
let changesMade = onCanvasInputDownInGameplay(button, mobile);
if(changesMade) {
input.dragging = true;
input.dragStart.x = input.screenCoords.x;
input.dragStart.y = input.screenCoords.y;
}
break;
}
}
/**
* Handler for when the player does an input down on the canvas, in the board game screen.
* This happens regardless of it being a mouse button down press, or a mobile touch start.
* @param {number} button What button got pressed. 0 for left click/mobile touch, 2 for right click, other values for other buttons.
* @param {number} mobile True if it was a mobile touch, false otherwise.
* @returns True if something happened, false otherwise.
*/
function onCanvasInputDownInGameplay(button, mobile) {
let changesMade = false;
// Figure out where the player clicked.
if(isPointInCoords(input.screenCoords, sectionCoords.board)) {
// Clicked on the board.
let idxs = getCellInCoords(input.screenCoords);
if(mobile) {
if(toggleCellFillAndMark(idxs[0], idxs[1])) {
changesMade = true;
}
} else if(button == 0) {
if(toggleCellFill(idxs[0], idxs[1])) {
changesMade = true;
}
} else if(button == 2 && state == STATE_PLAYING) {
if(toggleCellMark(idxs[0], idxs[1])) {
changesMade = true;
}
}
} else if(
state == STATE_PLAYING &&
isPointInCoords(input.screenCoords, sectionCoords.rowBanner)
) {
// Clicked on the row hints.
let idxs = getRowBannerButtonInCoords(input.screenCoords);
if(idxs != null && button == 0) {
if(toggleRowHint(idxs[0], idxs[1])) {
updateRow(idxs[0]);
saveBookmark();
changesMade = true;
}
}
} else if(
state == STATE_PLAYING &&
isPointInCoords(input.screenCoords, sectionCoords.colBanner)
) {
// Clicked on the column hints.
let idxs = getColumnBannerButtonInCoords(input.screenCoords);
if(idxs != null && button == 0) {
if(toggleColumnHint(idxs[0], idxs[1])) {
updateColumn(idxs[0]);
saveBookmark();
changesMade = true;
}
}
} else if(
state == STATE_MAKING &&
isPointInCoords(input.screenCoords, sectionCoords.rowBanner)
) {
// Clicked on the maker mode row buttons.
let idxs = getRowBannerButtonInCoords(input.screenCoords);
if(idxs != null && button == 0) {
if(idxs[1] == 0) {
// Delete row.
if(board.nrRows > MIN_COLS_OR_ROWS) {
board.cells.splice(idxs[0], 1);
board.nrRows--;
clampCamera();
changesMade = true;
}
} else if(idxs[1] == 1) {
// New row above.
if(board.nrRows < MAX_COLS_OR_ROWS) {
let newCol = [];
for(var c = 0; c < board.nrCols; c++) {
newCol.push(CELL_BLANK);
}
board.cells.splice(idxs[0], 0, newCol);
board.nrRows++;
changesMade = true;
}
} else if(idxs[1] == 2) {
// New row below.
if(board.nrRows < MAX_COLS_OR_ROWS) {
let newCol = [];
for(var c = 0; c < board.nrCols; c++) {
newCol.push(CELL_BLANK);
}
board.cells.splice(idxs[0] + 1, 0, newCol);
board.nrRows++;
changesMade = true;
}
}
}
} else if(
state == STATE_MAKING &&
isPointInCoords(input.screenCoords, sectionCoords.colBanner)
) {
// Clicked on the maker mode column buttons.
let idxs = getColumnBannerButtonInCoords(input.screenCoords);
if(idxs != null && button == 0) {
if(idxs[1] == 0) {
// Delete column.
if(board.nrCols > MIN_COLS_OR_ROWS) {
for(var r = 0; r < board.nrRows; r++) {
board.cells[r].splice(idxs[0], 1);
}
board.nrCols--;
clampCamera();
changesMade = true;
}
} else if(idxs[1] == 1) {
// New column above.
if(board.nrCols < MAX_COLS_OR_ROWS) {
for(var r = 0; r < board.nrRows; r++) {
board.cells[r].splice(idxs[0], 0, CELL_BLANK);
}
board.nrCols++;
changesMade = true;
}
} else if(idxs[1] == 2) {
// New column below.
if(board.nrCols < MAX_COLS_OR_ROWS) {
for(var r = 0; r < board.nrRows; r++) {
board.cells[r].splice(idxs[0] + 1, 0, CELL_BLANK);
}
board.nrCols++;
changesMade = true;
}
}
}
} else if(isPointInCoords(input.screenCoords, guiItemCoords.pause, sectionCoords.footer)) {
// Clicked on the pause button.
inPanel = true;
panelState = PANEL_STATE_PAUSE;
changesMade = true;
} else if(isPointInCoords(input.screenCoords, guiItemCoords.zoomIn, sectionCoords.footer)) {
// Clicked on the zoom in button.
cam.zoom += 0.2;
} else if(isPointInCoords(input.screenCoords, guiItemCoords.zoomOut, sectionCoords.footer)) {
// Clicked on the zoom out button.
cam.zoom -= 0.2;
}
if(!changesMade) {
input.dragPanning = true;
changesMade = true;
}
if(changesMade) {
updateCanvas();
}
return changesMade;
}
/**
* Handler for when the player does an input down on the canvas, in the main menu.
* This happens regardless of it being a mouse button down press, or a mobile touch start.
* @param {number} button What button got pressed. 0 for left click/mobile touch, 2 for right click, other values for other buttons.
* @param {number} mobile True if it was a mobile touch, false otherwise.
*/
function onCanvasInputDownInMainMenu(button, mobile) {
if(isPointInCoords(input.screenCoords, guiItemCoords.mainMenuContinue, sectionCoords.mainMenuHeader)) {
// Clicked on the continue button.
var bookmarkData = getBookmarkData();
if(bookmarkData == null) return;
if(loadLevel(bookmarkData.levelNumber, bookmarkData.levelCode, STATE_PLAYING)) {
board.cells = bookmarkData.cells;
let hintIdx = 0;
for(var r = 0; r < board.nrRows; r++) {
for(var h = 0; h < board.rowHints[r].length; h++) {
board.rowHints[r][h].state = bookmarkData.rowHints[hintIdx];
hintIdx++;
}
}
hintIdx = 0;
for(var c = 0; c < board.nrCols; c++) {
for(var h = 0; h < board.colHints[c].length; h++) {
board.colHints[c][h].state = bookmarkData.colHints[hintIdx];
hintIdx++;
}
}
for(c = 0; c < board.nrCols; c++) {
updateColumn(c);
}
for(r = 0; r < board.nrRows; r++) {
updateRow(r);
}
changeState(STATE_PLAYING);
} else {
panelState = PANEL_STATE_LOAD_ERROR;
}
} else if(isPointInCoords(input.screenCoords, guiItemCoords.mainMenuInfo, sectionCoords.mainMenuHeader)) {
// Clicked on the info button.
inPanel = true;
panelState = PANEL_STATE_INFO;
} else if(isPointInCoords(input.screenCoords, guiItemCoords.mainMenuMake, sectionCoords.mainMenuHeader)) {
// Clicked on the make custom button.
loadLevel(0, '', STATE_MAKING);
changeState(STATE_MAKING);
} else if(isPointInCoords(input.screenCoords, guiItemCoords.mainMenuCustom, sectionCoords.mainMenuHeader)) {
// Clicked on the play custom button.
let bookmarkData = getBookmarkData();
if(bookmarkData != null && !gaveBookmarkWarning) {
inPanel = true;
panelState = PANEL_STATE_BOOKMARK_WARNING;
gaveBookmarkWarning = true;
} else {
inPanel = true;
panelState = PANEL_STATE_PLAYING_CODE;
inputEl.value = '';
showInputBox(false);
}
} else if(firstSideCleared() && isPointInCoords(input.screenCoords, guiItemCoords.mainMenuSide, sectionCoords.mainMenuHeader)) {
// Clicked on the side swapping button.
gameSide = (gameSide == 0 ? 1 : 0);
updateCanvas();
} else {
// Check if the player clicked on one of the level buttons.
let buttonPadding = SECTION_PADDING * 2;
let buttonWidth = (sectionCoords.levelSelect.width - buttonPadding * 5) / 4;
let levelHeight = (sectionCoords.levelSelect.height - buttonPadding * 5) / 4;
let buttonHeight = levelHeight * 0.75;
var clickedLevelIdx = -1;
for(var c = 0; c < 4; c++) {
for(var r = 0; r < 4; r++) {
if(
isPointInCoords(
input.screenCoords,
{
x: buttonPadding + c * (buttonWidth + buttonPadding),
y: buttonPadding + r * (levelHeight + buttonPadding),
width: buttonWidth,
height: buttonHeight
},
sectionCoords.levelSelect
)
) {
clickedLevelIdx = r * 4 + c;
clickedLevelIdx += 16 * gameSide;
break;
}
}
if(clickedLevelIdx != -1) {
break;
}
}
if(clickedLevelIdx != -1) {
let bookmarkData = getBookmarkData();
if(bookmarkData != null && !gaveBookmarkWarning) {
inPanel = true;
panelState = PANEL_STATE_BOOKMARK_WARNING;
gaveBookmarkWarning = true;
} else {
if(loadLevel(clickedLevelIdx + 1, LEVELS[clickedLevelIdx], STATE_PLAYING)) {
changeState(STATE_PLAYING);
clearBookmark();
} else {
panelState = PANEL_STATE_LOAD_ERROR;
}
}
}
}
}
/**
* Handler for when the player does an input down on the canvas, in the panel.
* This happens regardless of it being a mouse button down press, or a mobile touch start.
* @param {number} button What button got pressed. 0 for left click/mobile touch, 2 for right click, other values for other buttons.
* @param {number} mobile True if it was a mobile touch, false otherwise.
*/
function onCanvasInputDownInPanel(button, mobile) {
// Figure out where the player clicked.
switch(panelState) {
case PANEL_STATE_PAUSE:
if(isPointInCoords(input.screenCoords, guiItemCoords.continue, sectionCoords.panel)) {
// Clicked on the continue button.
inPanel = false;
} else if(isPointInCoords(input.screenCoords, guiItemCoords.restart, sectionCoords.panel)) {
// Clicked on the restart button.
inPanel = false;
loadLevel(board.levelNumber, board.levelCode, state);
clearBookmark();
} else if(isPointInCoords(input.screenCoords, guiItemCoords.finish, sectionCoords.panel)) {
// Clicked on the finish button.
if(state == STATE_MAKING) {
let hasFilledCells = false;
for(var r = 0; r < board.nrRows; r++) {
for(var c = 0; c < board.nrCols; c++) {
if(board.cells[r][c] == CELL_FILLED) {
hasFilledCells = true;
break;
}
}
if(hasFilledCells) break;
}
if(hasFilledCells) {
panelState = PANEL_STATE_MAKING_NAME;
inputEl.value = board.name;
showInputBox(false, 20);
} else {
panelState = PANEL_STATE_SAVE_ERROR;
}
}
} else if(isPointInCoords(input.screenCoords, guiItemCoords.quit, sectionCoords.panel)) {
// Clicked on the quit button.
changeState(STATE_MAIN_MENU, function() { inPanel = false; });
} else {
return;
}
break;
case PANEL_STATE_CONGRATS:
if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) {
// Clicked on the ok button.
changeState(STATE_MAIN_MENU, function() { inPanel = false; });
}
break;
case PANEL_STATE_MAKING_NAME:
if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) {
// Clicked on the ok button.
acceptInputBox();
}
break;
case PANEL_STATE_MAKING_CODE:
if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) {
// Clicked on the ok button.
acceptInputBox();
}
break;
case PANEL_STATE_PLAYING_CODE:
if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) {
// Clicked on the ok button.
acceptInputBox();
}
break;
case PANEL_STATE_LOAD_ERROR:
if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) {
// Clicked on the ok button.
inPanel = false;
}
break;
case PANEL_STATE_SAVE_ERROR:
if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) {
// Clicked on the ok button.
inPanel = false;
}
break;
case PANEL_STATE_BOOKMARK_WARNING:
if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) {
// Clicked on the ok button.
inPanel = false;
}
break;
case PANEL_STATE_INFO:
if(isPointInCoords(input.screenCoords, guiItemCoords.ok, sectionCoords.panel)) {
// Clicked on the ok button.
inPanel = false;
}
break;
}
updateCanvas();
}
/**
* Handler for when the player does an input move on the canvas.
* This happens regardless of it being a mouse button down press, or a mobile touch start.
*/
function onCanvasInputMove() {
if(input.dragging && !input.dragPanning) {
let snappedMouse = { x: input.screenCoords.x, y: input.screenCoords.y };
let somethingChanged = false;
if(input.dragLockCoord.x) snappedMouse.x = input.dragStart.x;
if(input.dragLockCoord.y) snappedMouse.y = input.dragStart.y;
if(
isPointInCoords(input.dragStart, sectionCoords.board) &&
isPointInCoords(input.screenCoords, sectionCoords.board)
) {
let idxs = getCellInCoords(snappedMouse);
if(idxs != null) {
if(toggleCellFill(idxs[0], idxs[1])) {
somethingChanged = true;
}
}
} else if(
state == STATE_PLAYING &&
isPointInCoords(input.dragStart, sectionCoords.rowBanner) &&
isPointInCoords(input.screenCoords, sectionCoords.rowBanner)
) {
let idxs = getRowBannerButtonInCoords(snappedMouse);
if(idxs != null) {
if(toggleRowHint(idxs[0], idxs[1])) {
updateRow(idxs[0]);
somethingChanged = true;
}
}
} else if(
state == STATE_PLAYING &&
isPointInCoords(input.dragStart, sectionCoords.colBanner) &&
isPointInCoords(input.screenCoords, sectionCoords.colBanner)
) {
let idxs = getColumnBannerButtonInCoords(snappedMouse);
if(idxs != null) {
if(toggleColumnHint(idxs[0], idxs[1])) {
updateColumn(idxs[0]);
somethingChanged = true;
}
}
}
if(somethingChanged) {
if(!input.dragLockCoord.x && !input.dragLockCoord.y) {
// This means we changed something that wasn't the thing the drag start changed.
// We should lock the coordinates.
let axisSnappedMouse = snapCoordsToAxis(input.screenCoords, input.dragStart);
if(input.dragStart.x == axisSnappedMouse.x) {
input.dragLockCoord.x = true;
} else {
input.dragLockCoord.y = true;
}
}
updateCanvas();
}
}
}
/**
* Handler for when the player presses the mouse button down on the canvas.
* @param {event} event Event that triggered this callback.
*/
function onCanvasMouseDown(event) {
if(event.button == 1) {
event.preventDefault();
}
updateInputFromMouse(event);
onCanvasInputDown(event.button, false);
}
/**
* Handler for when the player moves the mouse cursor on the canvas.
* @param {event} event Event that triggered this callback.
*/
function onCanvasMouseMove(event) {
updateInputFromMouse(event);
onCanvasInputMove();
}
/**
* Handler for when the player scrolls the mouse wheel on the canvas.
* @param {event} event Event that triggered this callback.
*/
function onCanvasMouseWheel(event) {
for(var l = 0; l < loaded.length; l++) {
if(!loaded[l]) return;
}
if(transitionAnimTime > 0) return;
updateCanvas();
}
/**
* Handler for when the player moves a mobile touch on the canvas.
* @param {event} event Event that triggered this callback.
*/
function onCanvasTouchMove(event) {
event.preventDefault();
updateInputFromTouch(event);
onCanvasInputMove();
}
/**
* Handler for when the player starts a mobile touch on the canvas.
* @param {event} event Event that triggered this callback.
*/
function onCanvasTouchStart(event) {
event.preventDefault();
updateInputFromTouch(event);
onCanvasInputDown(0, true);
}
/**
* Handler for when the player does an input move on the page.
* This happens regardless of it being a mouse cursor move, or a mobile touch move.
*/
function onPageInputMove() {
if(input.dragging && input.dragPanning) {
cam.coords.x -= (input.screenCoords.x - input.dragStart.x);
cam.coords.y -= (input.screenCoords.y - input.dragStart.y);
input.dragStart.x = input.screenCoords.x;
input.dragStart.y = input.screenCoords.y;
clampCamera();
updateCanvas();
}
}
/**
* Handler for when the player does an input up on the page.
* This happens regardless of it being a mouse button up press, or a mobile touch end.
*/
function onPageInputUp() {
input.dragging = false;
input.dragAction = -1;
input.dragLockCoord.x = false;
input.dragLockCoord.y = false;
if(input.dragPanning) {
input.dragPanning = false;
if(Math.abs(cam.coords.x) < 10) {
cam.coords.x = 0;
}
if(Math.abs(cam.coords.y) < 10) {
cam.coords.y = 0;
}
}
updateCanvas();
}
/**
* Handler for when the player moves their mouse on the page.
* @param {event} event Event that triggered this callback.
*/
function onPageMouseMove(event) {
updateInputFromMouse(event);
onPageInputMove();
}
/**
* Handler for when the player releases their mouse button on the page.
* @param {event} event Event that triggered this callback.
*/
function onPageMouseUp(event) {
onPageInputUp();
}
/**
* Handler for when the player moves their mobile touch on the page.
* @param {event} event Event that triggered this callback.
*/
function onPageTouchMove(event) {
if(!input.dragging) event.preventDefault();
updateInputFromTouch(event);
onPageInputMove();
}
/**
* Handler for when the player releases their mobile touch on the page.
* @param {event} event Event that triggered this callback.
*/
function onPageTouchEnd(event) {
onPageInputUp();
}
/**
* Handler for when the global game timer ticks.
*/
function onTimer() {
if(transitionAnimTime > 0) {
transitionAnimTime -= 1 / 60;
if(transitionAnimTime <= 0) {
if(transitionPhase == 0) {
transitionPhase = 1;
transitionAnimTime = TRANSITION_DURATION;
state = transitionNewState;
if(transitionCodeAfter !== undefined) transitionCodeAfter();
} else {
transitionAnimTime = 0;
}
}
updateCanvas();
}
}
/**
* Checks whether a player's combo can ever be exactly the given size,
* provided that the player can still fill more cells next to it.
* Of course, if there are filled cells farther ahead that make the combo
* too big, this will return false.
* @param {object} combo Object with data about this combo.
* @param {number} goalSize Size to check for.
* @param {array} cells Array of cells in this line.
* @returns True if it can, false if not.
*/
function playerComboCanBeSize(combo, goalSize, cells) {
if(combo.nr > goalSize) return false;
let curSize = combo.nr;
for(var c = combo.start + size; c <= cells.length; c++) {
if(cells[c] == CELL_FILLED) {
// It must use this filled cell as part of its combo.
curSize++;
} else if(cells[c] == CELL_BLANK) {
// The player can stop here or keep going.
if(curSize == goalSize) return true;
curSize++;
} else if(c == cells.length || cells[c] == CELL_MARKED) {
// End of the line for this combo.
return curSize == goalSize;
}
}
return false;
}
/**
* Checks whether one of the player board's rows or columns defies the corresponding solution hint.
* @param {number} row Row number, if we're checking a row. null otherwise.
* @param {number} col Column number, if we're checking a column. null otherwise.
* @param {array} playerCombos Array with the player's combos for this column/row.
* @param {array} hints Array of hints for this column/row.
* @returns True if it defies, false otherwise.
*/
function playerLineDefiesHints(row, col, playerCombos, hints) {
// Setup.
let playerCells = [];
if(row != null) {
for(var c = 0; c < board.nrCols; c++) {
let cell = board.cells[row][c];
if(cell == CELL_BLANK && board.autoMarkedCols[c]) cell = CELL_MARKED;
playerCells.push(cell);
}
} else {
for(var r = 0; r < board.nrRows; r++) {
let cell = board.cells[r][col];
if(cell == CELL_BLANK && board.autoMarkedRows[r]) cell = CELL_MARKED;
playerCells.push(cell);
}
}
// Start by checking if any marked cell is in the way of the hints.
let curCellIdx = 0;
for(var h = 0; h < hints.length;) {
if(curCellIdx >= playerCells.length) {
// We've reached the end of the cells, and we still have hints to check. Too bad.
return true;
}
let markedCellIdx = findMarkedCellInList(playerCells, curCellIdx, hints[h].nr);
if(markedCellIdx == -1) {
// No marked cell. Move on to the next hint.
curCellIdx += hints[h].nr + 1;
h++;
} else {
// We've hit a marked cell. Let's move to after that cell and retry.
curCellIdx = markedCellIdx + 1;
}
}
if(playerCombos.length > hints.length) {
//return true;
} else if(playerCombos.length == hints.length) {
for(var c = 0; c < playerCombos.length; c++) {
if(playerCombos[c] > hints[c].nr) return true;
}
} else {
let highestPlayerCombo = 0;
let highestHintCombo = 0;
for(var c = 0; c < playerCombos.length; c++) {
highestPlayerCombo = Math.max(highestPlayerCombo, playerCombos[c]);
}
for(var h = 0; h < hints.length; h++) {
highestHintCombo = Math.max(highestHintCombo, hints[h].nr);
}
if(highestPlayerCombo > highestHintCombo) return true;
}
// All good!
return false;
/*
// Possible combos for each hint.
let possibleCombosPerHint = [];
for(var h = 0; h < hints.length; h++) {
let possibleCombos = [];
for(var c = 0; c < playerCombos.length; c++) {
if(playerComboCanBeSize(playerCombos[c], hints[h].nr, playerCells)) {
possibleCombos.push(playerCombos[c]);
}
}
possibleCombosPerHint[h] = possibleCombos;
}
// I don't know what to do with this :(
*/
}
/**
* Checks whether one of the player board's rows or columns matches the corresponding solution hint.
* This can be true even if the player marked the wrong cells.
* @param {array} playerCombos Array with the player's combos for this column/row.
* @param {array} hints Array of hints for this column/row.
* @returns True if it matches, false otherwise.
*/
function playerLineMatchesHints(playerCombos, hints) {
if(playerCombos.length != hints.length) return false;
for(var h = 0; h < hints.length; h++) {
if(hints[h].nr != playerCombos[h].nr) return false;
}
return true;
}
/**
* Populates the board with cells from the given code, in the puzzle making state.
* Debug function.
* @param {string} code Code.
* @returns True on success, false on failure.
*/
function populateFromCode(levelCode) {
if(state != STATE_MAKING) return false;
let levelData = decodeBoard(levelCode, BOARD_DATA_CONTEXT_LEVEL);
if(levelData == null) return false;
board.nrRows = levelData.nrRows;
board.nrCols = levelData.nrCols;
board.cells = levelData.cells;
board.name = levelData.name;
updateCanvas();
return true;
}
/**
* Saves the player's bookmark.
*/
function saveBookmark() {
if(state != STATE_PLAYING) return;
var boardData = {
nrRows: board.nrRows,
nrCols: board.nrCols,
cells: board.cells,
nrRowHints: 0,
nrColHints: 0,
rowHints: [],
colHints: [],
name: board.name,
levelNumber: board.levelNumber,
levelCode: board.levelCode
};
for(var r = 0; r < board.nrRows; r++) {
for(var h = 0; h < board.rowHints[r].length; h++) {
boardData.rowHints.push(board.rowHints[r][h].state);
}
}
boardData.nrRowHints = boardData.rowHints.length;
for(var c = 0; c < board.nrCols; c++) {
for(var h = 0; h < board.colHints[c].length; h++) {
boardData.colHints.push(board.colHints[c][h].state);
}
}
boardData.nrColHints = boardData.colHints.length;
let boardStr = encodeBoard(boardData, BOARD_DATA_CONTEXT_BOOKMARK);
localStorage.setItem('pikcrossBookmark', boardStr);
gaveBookmarkWarning = false;
}
/**
* Saves the player's global progression.
*/
function saveProgression() {
let writer = new StrBitWriter();
for(var l = 0; l < LEVELS.length; l++) {
writer.writeNumber(progression[l] ? 1 : 0, 1);
}
localStorage.setItem('pikcrossProgression', writer.getStr());
}
/**
* Sets up the whole game.
*/
function pikcrossSetup() {
// HTML setup.
let pikcrossDiv = document.getElementById('pikcross');
if(pikcrossDiv == null) return;
pikcrossDiv.style.position = 'relative';
pikcrossDiv.style.width = '800px';
pikcrossDiv.style.height = '800px';
canvasEl = document.createElement('canvas');
canvasEl.width = CANVAS.WIDTH;
canvasEl.height = CANVAS.HEIGHT;
pikcrossDiv.appendChild(canvasEl);
inputEl = document.createElement('input');
inputEl.type = 'text';
inputEl.style.left = '30%';
inputEl.style.width = '40%';
inputEl.style.top = '48%';
inputEl.style.height = '4%';
inputEl.style.position = 'absolute';
inputEl.style.backgroundColor = '#BDB';
inputEl.style.fontFamily = 'monospace';
inputEl.addEventListener('focus', function(e) { e.target.select(); });
inputEl.addEventListener('keyup', function(e) { if(e.key === 'Enter') acceptInputBox(); });
hideInputBox();
pikcrossDiv.appendChild(inputEl);
canvas = canvasEl.getContext('2d');
canvas.width = CANVAS.WIDTH;
canvas.height = CANVAS.HEIGHT;
canvas.textAlign = 'center';
canvas.textBaseline = 'middle';
// Listeners.
canvasEl.addEventListener('wheel', onCanvasMouseWheel);
canvasEl.addEventListener('mousedown', onCanvasMouseDown);
canvasEl.addEventListener('touchstart', onCanvasTouchStart);
canvasEl.addEventListener('mousemove', onCanvasMouseMove);
canvasEl.addEventListener('touchmove', onCanvasTouchMove);
canvasEl.addEventListener('contextmenu', function(e) { e.preventDefault(); }, false);
document.addEventListener('mousemove', onPageMouseMove);
document.addEventListener('touchmove', onPageTouchMove);
document.addEventListener('mouseup', onPageMouseUp);
document.addEventListener('touchend', onPageTouchEnd);
// Spritesheet.
sprites = new Image();
sprites.onload = function() {
loaded[LOAD_CONTENT_SPRITES] = true;
updateCanvas();
};
sprites.src = 'https://pikmin.wiki.gallery/images/c/c7/Pikcross_spritesheet.png';
// Player progression.
for(var l = 0; l < LEVELS.length; l++) {
progression.push(false);
}
loadProgression();
// Levels data.
for(var l = 0; l < LEVELS.length; l++) {
levelsData.push(decodeBoard(LEVELS[l], BOARD_DATA_CONTEXT_LEVEL));
}
// Global timer.
setInterval(onTimer, 1000 / 60);
loaded[LOAD_CONTENT_SETUP] = true;
}
/**
* Shows the input box HTML element in the middle of the canvas.
* It also selects the text.
* @param {boolean} readOnly Whether the box should be read only or not.
* @param {number} maxLength Maximum length for the input box. undefined for default.
*/
function showInputBox(readOnly, maxLength) {
inputEl.style.display = 'block';
inputEl.removeAttribute('maxlength');
inputEl.removeAttribute('readonly');
if(maxLength > 0) inputEl.maxLength = maxLength;
if(readOnly) inputEl.readOnly = true;
// This weird hack is necessary for some reason.
window.setTimeout(
function() {
inputEl.focus({ focusVisible: true });
inputEl.select();
},
0
);
}
/**
* Given a set of coordinates, it snaps them so they are in the same axis as the anchor coordinates.
* @param {object} coords Coordinates to snap.
* @param {object} anchor Coordinates to compare against.
* @returns An object with the snapped coordinates.
*/
function snapCoordsToAxis(coords, anchor) {
let h_diff = Math.abs(coords.x - anchor.x);
let v_diff = Math.abs(coords.y - anchor.y);
if(h_diff > v_diff) {
return {x: coords.x, y: anchor.y};
} else {
return {x: anchor.x, y: coords.y};
}
}
/**
* Solves the puzzle.
* Debug function.
*/
function solve() {
board.cells = board.solution;
for(c = 0; c < board.nrCols; c++) {
updateColumn(c);
}
for(r = 0; r < board.nrRows; r++) {
updateRow(r);
}
updateCanvas();
}
/**
* Toggles whether a cell is filled or blank, if possible.
* @param {number} row Row index number.
* @param {number} col Column index number.
* @returns True if the cell changed, false otherwise.
*/
function toggleCellFill(row, col) {
if(row < 0 || row >= board.nrRows) return false;
if(col < 0 || col >= board.nrCols) return false;
if(input.dragAction == -1) {
// Figure out what the player wants to do in this move.
if(board.cells[row][col] == CELL_FILLED) {
input.dragAction = CELL_BLANK;
} else {
input.dragAction = CELL_FILLED;
}
}
return changeCell(row, col, input.dragAction);
}
/**
* Toggles whether a cell is filled, marked, or blank, if possible.
* @param {number} row Row index number.
* @param {number} col Column index number.
* @returns True if the cell changed, false otherwise.
*/
function toggleCellFillAndMark(row, col) {
if(row < 0 || row >= board.nrRows) return false;
if(col < 0 || col >= board.nrCols) return false;
if(input.dragAction == -1) {
// Figure out what the player wants to do in this move.
if(board.cells[row][col] == CELL_FILLED) {
input.dragAction = CELL_MARKED;
} else if(board.cells[row][col] == CELL_MARKED) {
input.dragAction = CELL_BLANK;
} else {
input.dragAction = CELL_FILLED;
}
}
return changeCell(row, col, input.dragAction);
}
/**
* Toggles whether a cell is marked or blank, if possible.
* @param {number} row Row index number.
* @param {number} col Column index number.
* @returns True if the cell changed, false otherwise.
*/
function toggleCellMark(row, col) {
if(row < 0 || row >= board.nrRows) return false;
if(col < 0 || col >= board.nrCols) return false;
if(input.dragAction == -1) {
// Figure out what the player wants to do in this move.
if(board.cells[row][col] == CELL_MARKED) {
input.dragAction = CELL_BLANK;
} else {
input.dragAction = CELL_MARKED;
}
}
return changeCell(row, col, input.dragAction);
}
/**
* Toggles whether a column hint is marked or blank, if possible.
* @param {number} col Column index number.
* @param {number} hint Hint index number.
* @returns True if the hint changed, false otherwise.
*/
function toggleColumnHint(col, hint) {
if(input.dragAction == -1) {
// Figure out what the player wants to do in this move.
if(board.colHints[col][hint].state == CELL_FILLED) {
input.dragAction = CELL_BLANK;
} else {
input.dragAction = CELL_FILLED;
}
}
if(board.colHints[col][hint].state != input.dragAction) {
board.colHints[col][hint].state = input.dragAction;
return true;
}
return false;
}
/**
* Toggles whether a row hint is marked or blank, if possible.
* @param {number} row Row index number.
* @param {number} hint Hint index number.
* @returns True if the hint changed, false otherwise.
*/
function toggleRowHint(row, hint) {
if(input.dragAction == -1) {
// Figure out what the player wants to do in this move.
if(board.rowHints[row][hint].state == CELL_FILLED) {
input.dragAction = CELL_BLANK;
} else {
input.dragAction = CELL_FILLED;
}
}
if(board.rowHints[row][hint].state != input.dragAction) {
board.rowHints[row][hint].state = input.dragAction;
return true;
}
return false;
}
/**
* Updates the contents of the canvas.
*/
function updateCanvas() {
for(var l = 0; l < loaded.length; l++) {
if(!loaded[l]) return;
}
// Basic setup.
canvas.setTransform(1, 0, 0, 1, 0, 0);
canvas.fillStyle = COLOR_BG;
canvas.fillRect(0, 0, CANVAS.WIDTH, CANVAS.HEIGHT);
switch(state) {
case STATE_MAIN_MENU:
drawMainMenu();
break;
case STATE_PLAYING:
drawBoard();
break;
case STATE_MAKING:
drawBoard();
break;
}
if(inPanel) {
drawPanel();
}
if(transitionAnimTime > 0) {
canvas.fillStyle = 'black';
let fillRatio = 1 - (transitionAnimTime / TRANSITION_DURATION);
if(transitionPhase == 1) fillRatio = 1 - fillRatio;
fillRatio *= 0.5;
canvas.fillRect(0, 0, CANVAS.WIDTH, CANVAS.HEIGHT * fillRatio);
canvas.fillRect(0, 0, CANVAS.WIDTH * fillRatio, CANVAS.HEIGHT);
canvas.fillRect(CANVAS.WIDTH * (1 - fillRatio), 0, CANVAS.WIDTH, CANVAS.HEIGHT);
canvas.fillRect(0, CANVAS.HEIGHT * (1 - fillRatio), CANVAS.WIDTH, CANVAS.HEIGHT);
}
}
/**
* Updates the state of a given column.
* @param {number} col Column number to update.
*/
function updateColumn(col) {
let playerCombos = getPlayerLineCombos(null, col);
if(allHintsAreFilled(board.colHints[col]) && playerLineMatchesHints(playerCombos, board.colHints[col])) {
board.autoMarkedCols[col] = true;
} else {
board.autoMarkedCols[col] = false;
}
if(playerLineDefiesHints(null, col, playerCombos, board.colHints[col])) {
board.defiedCols[col] = true;
} else {
board.defiedCols[col] = false;
}
}
/**
* Updates the variables that hold the mouse state.
* @param {event} event The event that triggered this.
*/
function updateInputFromMouse(event) {
let br = canvasEl.getBoundingClientRect();
input.screenCoords.x = event.clientX - br.left;
input.screenCoords.y = event.clientY - br.top;
input.worldCoords.x = input.screenCoords.x;
input.worldCoords.y = input.screenCoords.y;
input.worldCoords.x -= CANVAS.WIDTH / 2;
input.worldCoords.y -= CANVAS.HEIGHT / 2;
//input.worldCoords.x /= cam.zoom;
//input.worldCoords.y /= cam.zoom;
input.worldCoords.x += cam.coords.x;
input.worldCoords.y += cam.coords.y;
}
/**
* Updates the variables that hold the mouse state, using data from a mobile touch event.
* @param {event} event The event that triggered this.
*/
function updateInputFromTouch(event) {
let br = canvasEl.getBoundingClientRect();
input.screenCoords.x = event.touches[0].clientX - br.left;
input.screenCoords.y = event.touches[0].clientY - br.top;
input.worldCoords.x = input.screenCoords.x;
input.worldCoords.y = input.screenCoords.y;
input.worldCoords.x -= CANVAS.WIDTH / 2;
input.worldCoords.y -= CANVAS.HEIGHT / 2;
//input.worldCoords.x /= cam.zoom;
//input.worldCoords.y /= cam.zoom;
input.worldCoords.x += cam.coords.x;
input.worldCoords.y += cam.coords.y;
}
/**
* Updates the state of a given row.
* @param {number} row Row number to update.
*/
function updateRow(row) {
let playerCombos = getPlayerLineCombos(row, null);
if(allHintsAreFilled(board.rowHints[row]) && playerLineMatchesHints(playerCombos, board.rowHints[row])) {
board.autoMarkedRows[row] = true;
} else {
board.autoMarkedRows[row] = false;
}
if(playerLineDefiesHints(row, null, playerCombos, board.rowHints[row])) {
board.defiedRows[row] = true;
} else {
board.defiedRows[row] = false;
}
}
/**
* Makes writing bits into a string (8-bit int array) easy.
*/
class StrBitWriter {
// Final string.
#str = '';
// Current working byte value.
#byteValue = 0;
// Current bit's index.
#bitIdx = 0;
/**
* Encodes a number into the string.
* @param {number} number Number to encode. 0 to 8 bits long.
* @param {number} nrBits Amount of bits the number takes. 8 by default.
*/
writeNumber(number, nrBits) {
if(nrBits === undefined) nrBits = 8;
if(this.#bitIdx + nrBits > 8) {
this.nextByte();
}
for(var b = 0; b < nrBits; b++) {
let bitValue = (number & (1 << b)) > 0;
this.#byteValue |= (bitValue << this.#bitIdx);
this.#bitIdx++;
}
if(this.#bitIdx >= 8) {
this.nextByte();
}
}
/**
* Finishes writing to the current byte and goes to the next one.
*/
nextByte() {
this.#str += String.fromCharCode(this.#byteValue);
this.#byteValue = 0;
this.#bitIdx = 0;
}
/**
* Obtains the finalized string (8-bit int array).
* @returns The finalized string.
*/
getStr() {
let str = this.#str;
if(this.#bitIdx > 0) {
str += String.fromCharCode(this.#byteValue);
}
return str;
}
/**
* Resets the state of the writer.
*/
reset() {
this.#str = '';
this.#byteValue = 0;
this.#bitIdx = 0;
}
}
/**
* Makes reading bits from a string (8-bit int array) easy.
*/
class StrBitReader {
// Full string.
#str = '';
// Current byte's index.
#byteIdx = 0;
// Current bit's index.
#bitIdx = 0;
/**
* Construcs a new instance.
* @param {string} str String (8-bit int array) with the bits to read from.
*/
constructor(str) {
this.#str = str;
}
/**
* Reads a number from the next bits in the string.
* @param {number} nrBits Amount of bits the number takes. 8 by default.
* @returns The read number.
*/
readNumber(nrBits) {
if(nrBits === undefined) nrBits = 8;
if(this.#bitIdx + nrBits > 8) {
this.nextByte();
}
let number = 0;
let byteValue = this.#str.charCodeAt(this.#byteIdx);
for(var b = 0; b < nrBits; b++) {
let bitMask = (1 << this.#bitIdx);
let bitValue = (byteValue & bitMask) > 0;
number |= (bitValue << b);
this.#bitIdx++;
}
if(this.#bitIdx >= 8) {
this.nextByte();
}
return number;
}
/**
* Finishes reading the current byte and goes to the next one.
*/
nextByte() {
this.#byteIdx++;
this.#bitIdx = 0;
}
/**
* Resets the state of the reader.
*/
reset() {
this.#str = '';
this.#byteIdx = 0;
this.#bitIdx = 0;
}
}
/**
* Runs the game.
*/
pikcrossSetup();