MediaWiki:Pikcross.js: Difference between revisions
From the Super Mario Wiki, the Mario encyclopedia
Jump to navigationJump to search
mNo edit summary |
mNo edit summary |
||
Line 2,688: | Line 2,688: | ||
*/ | */ | ||
function onPageTouchEnd(event) { | function onPageTouchEnd(event) { | ||
onPageInputUp(); | onPageInputUp(); | ||
} | } |
Revision as of 13:41, December 23, 2023
// PLEASE do not modify or tamper with this script for your
// own Javascript-based Picross needs without permission!
// Message Espyo on pikminwiki.com instead before doing so.
// If you're here in the name of archival or just personalbar
// amusement in running this locally, however, go nuts.
// Just put in a good word to them, will ya? Most of the actual
// coding work was theirs, after all.
// 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, 51)';
// Color for the board's background.
const COLOR_BOARD_BG = 'rgba(34, 68, 68, 0.6)';
// Color for the hints' background.
const COLOR_HINTS_BG = 'rgb(51, 85, 85)';
// Color for the footer's background.
const COLOR_FOOTER_BG = 'rgb(68, 102, 102)';
// Colors for the hint text.
const COLOR_HINT = ['#262', '#383'];
// 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 = '#BCC';
// Colors for a button's shadow.
const COLOR_BUTTON_SHADOW = ['rgba(0, 64, 64, 0.5)', 'rgba(64, 64, 0, 0.5)'];
// Colors for buttons' text.
const COLOR_BUTTON_TEXT = ['#262', '#383'];
// Color for buttons' text while disabled.
const COLOR_BUTTON_TEXT_DISABLED = '#888';
// Color for a blank cell.
const COLOR_BLANK = '#EFF';
// Color for a filled cell.
const COLOR_FILLED = '#699';
// Color for a marked cell.
const COLOR_MARKED = '#DEE';
// 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 = ['#CEC', '#BEB'];
// 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.
// Yes, the level editor actually was... THE LEVEL EDITOR FOR THE ORIGINAL APPLET! Dun dun DUNNN!~
const LEVELS = [
// Mushroom
'BQXuf0IACE11c2hyb29t',
// Barrel
'BQVu5uoABkJhcnJlbA==',
// Mario
'Cgr4+P+/y6FF5iPB+HMOBU1hcmlv',
// Yoshi Egg
'CgowIMEMMxLpJJthhOABCVlvc2hpIEVnZw==',
// King Totomesu
'Dw/+HwGQ/48XqPpXIXz0AwiBf1+A4J8QkP//MjgPAA1LaW5nIFRvdG9tZXN1',
// Power Jump Badge
'Dw/gPxggDBAGCP7DBTFBTZATxAXhQdj/ZzRisWAwABBQb3dlciBKdW1wIEJhZGdl',
// Gold Ghost
'Dw8AACAA+AFeAK/Av+GA/eh/4n6Bf8A7AA4AAAAAAApHb2xkIEdob3N0',
// Beanstar
'Dw/AAbAB2ADH4dWbKk+QnXKMGAYMjgMpgZzAe8AYAAhCZWFuc3Rhcg==',
// Propeller Mushroom
'Dw+egLBfX9D/B2NAQBBAHHDuO/ubj48pg5SAIIAPABJQcm9wZWxsZXIgTXVzaHJvb20=',
// Mini Mario
'Dw/AARgDggJBQf9jU5BC+B86B9mH8oR5AtOASMA/AApNaW5pIE1hcmlv',
// Weird Mushroom
'Dw/gARwDH0KGI4DhfwADgAHAAGAAMAAsABYADQADAA5XZWlyZCBNdXNocm9vbQ==',
// Pi'illo
'Dw8AAOecjMl9Z0GroNZfm7P5T+GQIEjXZRwDAAAAAAdQaSdpbGxv',
// Cappy
'Dw8APAAJ+AeSBLGFJII2QZuYJJodMjj+DwFSdNFFAAVDYXBweQ==',
// "Hachisuke"
'Dw+D4Lijqord84inv/z48R/feQQY/g/9hXmKgY7/AQlIYWNoaXN1a2U=',
// Poochy
'Dw8AHsw8WV+kLcQm/GP4w8DID/An/Av9rD+Fz4FTAAZQb29jaHk=',
// ~Cool Secret Obscure Reference!~
'Dw8AQP6/+C/4l/ypfkQfnAPAAe6Ac8AZIAwoB+QBAAM/Pz8=',
// START OF B-SIDE PUZZLES
// Issue #201!
'ChT/A/YBU4BJpKREH0qkkPSKMPA9+Ar8D/7/C0lzc3VlICMyMDEh',
// Donkey Kong
'DxSAPwD8B2DOYEN4GRuJExk4gDHgwDGAmfyn6W4qR1yEKoIDGMB/AAtEb25rZXkgS29uZw==',
// Bowser
'FBT/7/5/5P8z/udwPwTxmIg/cfARCDGBIAoQQAYBmBABEFMAeWdy/J8xP4zw8CT/J/N/AgZCb3dzZXI=',
// Kamek
'FBSAPwAGPPwAbDCAAoyvHf8m4kdiPD7qIxzvAeCYAIQBQBg8giIkyAfBwB8M/uCjM88T/AVLYW1law==',
// Mario's Picross (picrossception?!)
'FBQAH4ANBjSgwAA+xBkkYoCS//n3fEuNUiQVJtrzoUUfGnjBgCcCWOZDnPxHDNsCgSs4xg9NYXJpbydzIFBpY3Jvc3M=',
// Tumble
'FBQADgAQA4BNANQY4DICLyNwxwL5LfA4AVcfMOUBAw5wl4FpJ/g5gmYkkIQBQQgQhAC+BwZUdW1ibGU=',
// Shine Sprite
'FBQADgCgAAAOwEFwFArFo3jQcQEKCmDAAJIIGNJDsEE4yQMKCmDVAKoK0H8B4xgACgBAAAxTaGluZSBTcHJpdGU=',
// Wario's Adventure
'FBQAAADwDsC9APMOCMRBnDP0x/LzNhE6k6G/cjwqDWKEP4b4P5BUAUk1EP8CAiDA/wEAABFXYXJpbydzIEFkdmVudHVyZQ==',
// Funky Kong
'FBSAHwD0AuB5APoFMIaAARi0/0INL2hggQEQMICAAxj4/4FVGbiCgf8XcIAAHgTAPwDwAApGdW5reSBLb25n',
// Princess Peach
'FBRABQCqAGAdAG8CjMBAAAgGwCHnEEyUgGYJaOYAAQ4YMIOYIBCMAc8TSDKBGhJcIUEmCw5QcmluY2VzcyBQZWFjaA==',
// Miiverse
'FBQAAADwAIAZAAwDeOnBBjZGKSZoQYIQZAxjRCBiBGKjUFwKpcc2Tm0r1LbCbz+AHwAAAAhNaWl2ZXJzZQ==',
// Biddybud (since you're here; we've had a tab of this image for months because the idea of a Dedicated Biddybud Tab was funny to us. this is the chekov's gun of our year.)
'FBQAAAAAAMA/AAMMCABBgCMEeCKAR/pxZD5AQgYkQWAS5WfSf3n+Of+Pvl/IA0QAgAMAAAhCaWRkeWJ1ZA==',
// Talking Flower
'FBSAGQB0A7DZgJgRhBkiAECCHyT8Q8I/JPxDBA+CABAwwOD8c7LQZAxjiRkZkIAGBoafHw5UYWxraW5nIEZsb3dlcg==',
// Hotel Mario
'FBQA7gHYIUAGBCRAIG0H83kIA8LIcUKSJGdZbI6HAGQMB+rw4B7+b8J9Q0gC+B/hQHAHCAtIb3RlbCBNYXJpbw==',
// Shroom Spotlight
'FBSAAAYIUOCDAjL0IL/pM70n4TsM3wPk1q80qZqRqhOpejGoF/56AfgXAPADAC4AQAEACBBTaHJvb20gU3BvdGxpZ2h0',
// The 3 Koppaites.
'FBQAAAD4AMAaAFQBwBgAVAHAGgD4AIAIAAQBQBDgAz5jUBUFY/kfFAVjY1DlAz4AAAAAAAM/Pz8=',
];
// 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: 97, y: 0, width: 199, height: 46,
},
// 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;
const 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 (let 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 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 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 (let r = 0; r < board.nrRows; r++) {
for (let c = 0; c < board.nrCols; c++) {
let 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 = undefined) {
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 (let 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 (let r = 0; r < result.nrRows; r++) {
result.cells.push([]);
for (let c = 0; c < result.nrCols; c++) {
result.cells[r].push(reader.readNumber(nrCellBits));
}
}
if (context === BOARD_DATA_CONTEXT_LEVEL) {
let nrNameChars = reader.readNumber();
for (let c = 0; c < nrNameChars; c++) {
result.name += String.fromCharCode(reader.readNumber());
}
} else {
result.nrRowHints = reader.readNumber();
result.nrColHints = reader.readNumber();
for (let r = 0; r < result.nrRowHints; r++) {
result.rowHints.push(reader.readNumber(1));
}
for (let c = 0; c < result.nrColHints; c++) {
result.colHints.push(reader.readNumber(1));
}
result.levelNumber = reader.readNumber();
let nrCodeChars = reader.readNumber();
for (let 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 (let r = 0; r < board.nrRows; r++) {
for (let 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 (let 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 (let 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 (let 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 (let 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 (let r = 0; r < board.nrRows; r++) {
for (let 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 (let c = 0; c < board.nrCols; c++) {
for (let 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 (let r = 0; r < board.nrRows; r++) {
for (let 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 (let c = 0; c < board.nrCols; c++) {
for (let 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 {string} fg Contents of the foreground, if any.
* @param {string} fgStyle Foreground style, if any.
* @param {string} fgFont Foreground content font, if any.
* @param {string} bgStyle Background style. If undefined, the background won't be drawn.
*/
function drawGuiItem(type, coords, parentCoords, fg = undefined, fgStyle = undefined, fgFont = undefined, bgStyle = undefined) {
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, 'About', 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 (let c = 0; c < 4; c++) {
for (let 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 {string} 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 (let r = 0; r < nrRows; r++) {
for (let 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_TEXT_GENERIC[gameSide], '36px sans');
drawMiniature(board.nrRows, board.nrCols, board.cells, guiItemCoords.congratsMiniature, sectionCoords.panel, COLOR_TEXT_GENERIC[gameSide]);
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.congratsLevelName, sectionCoords.panel, board.name, COLOR_TEXT_GENERIC[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, 'SHROOM PICROSS CREDITS', COLOR_BUTTON_BG, 'bold 20px sans');
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info2, sectionCoords.panel, 'Original Game Script - Espyo for Pikipedia', COLOR_BUTTON_BG, '18px sans');
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info3, sectionCoords.panel, 'Modification & Puzzles - Camwoodstock & Tori', COLOR_BUTTON_BG, '18px sans');
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info4, sectionCoords.panel, 'Additional GFX - Cookie Clicker, Gabumon', COLOR_BUTTON_BG, '18px sans');
drawGuiItem(GUI_ITEM_TEXT, guiItemCoords.info5, sectionCoords.panel, 'Special Thanks - TPG, VG Resource, "Gramma K."', COLOR_BUTTON_BG, '18px sans');
drawGuiItem(GUI_ITEM_BUTTON, guiItemCoords.ok, sectionCoords.panel, 'Thanks for play! ♥', 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 (let r = 0; r < data.nrRows; r++) {
for (let 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 (let c = 0; c < data.name.length; c++) {
writer.writeNumber(data.name.charCodeAt(c));
}
} else {
writer.writeNumber(data.nrRowHints);
writer.writeNumber(data.nrColHints);
for (let r = 0; r < data.nrRowHints; r++) {
writer.writeNumber(data.rowHints[r], 1);
}
for (let c = 0; c < data.nrColHints; c++) {
writer.writeNumber(data.colHints[c], 1);
}
writer.writeNumber(data.levelNumber);
writer.writeNumber(data.levelCode.length);
for (let 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 (let 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 (let 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|null} row Row number, if we're checking a row. null otherwise.
* @param {number|null} 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 (let 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 (let 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.
*/
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() {
const 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 (let r = 0; r < levelData.nrRows; r++) {
for (let 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 (let r = 0; r < board.nrRows; r++) {
board.rowHints.push([]);
let hintNr = 0;
for (let 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 (let c = 0; c < board.nrCols; c++) {
board.colHints.push([]);
let hintNr = 0;
for (let 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 (let r = 0; r < board.nrRows; r++) {
board.cells.push([]);
for (let 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 (let r = 0; r < board.nrRows; r++) {
board.cells.push([]);
for (let c = 0; c < board.nrCols; c++) {
board.cells[r].push(CELL_BLANK);
}
}
for (let r = 0; r < board.nrRows; r++) {
board.autoMarkedRows.push(false);
board.defiedRows.push(false);
}
for (let 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 (let r = 0; r < board.nrRows; r++) {
updateRow(r);
}
for (let 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 (let 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 {boolean} touch True if it was a touch event, false otherwise.
*/
function onCanvasInputDown(button, touch) {
for (let l = 0; l < loaded.length; l++) {
if (!loaded[l]) return;
}
if (transitionAnimTime > 0) return;
if (inPanel) {
onCanvasInputDownInPanel();
return;
}
switch (state) {
case STATE_MAIN_MENU:
onCanvasInputDownInMainMenu();
break;
case STATE_PLAYING:
case STATE_MAKING:
let changesMade = onCanvasInputDownInGameplay(button, touch);
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 {boolean} touch True if it was a touch, false otherwise.
* @returns True if something happened, false otherwise.
*/
function onCanvasInputDownInGameplay(button, touch) {
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 (touch) {
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 (let 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 (let 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 (let 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 (let 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 (let 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 && !touch) {
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.
*/
function onCanvasInputDownInMainMenu() {
if (isPointInCoords(input.screenCoords, guiItemCoords.mainMenuContinue, sectionCoords.mainMenuHeader)) {
// Clicked on the continue button.
const bookmarkData = getBookmarkData();
if (bookmarkData == null) return;
if (loadLevel(bookmarkData.levelNumber, bookmarkData.levelCode, STATE_PLAYING)) {
board.cells = bookmarkData.cells;
let hintIdx = 0;
for (let r = 0; r < board.nrRows; r++) {
for (let h = 0; h < board.rowHints[r].length; h++) {
board.rowHints[r][h].state = bookmarkData.rowHints[hintIdx];
hintIdx++;
}
}
hintIdx = 0;
for (let c = 0; c < board.nrCols; c++) {
for (let h = 0; h < board.colHints[c].length; h++) {
board.colHints[c][h].state = bookmarkData.colHints[hintIdx];
hintIdx++;
}
}
for (let c = 0; c < board.nrCols; c++) {
updateColumn(c);
}
for (let 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;
let clickedLevelIdx = -1;
for (let c = 0; c < 4; c++) {
for (let 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.
*/
function onCanvasInputDownInPanel() {
// 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 (let r = 0; r < board.nrRows; r++) {
for (let 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 {MouseEvent} 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 {MouseEvent} event Event that triggered this callback.
*/
function onCanvasMouseMove(event) {
updateInputFromMouse(event);
onCanvasInputMove();
}
/**
* Handler for when the player moves a mobile touch on the canvas.
* @param {TouchEvent} event Event that triggered this callback.
*/
function onCanvasTouchMove(event) {
updateInputFromTouch(event);
onCanvasInputMove();
}
/**
* Handler for when the player starts a mobile touch on the canvas.
* @param {TouchEvent} event Event that triggered this callback.
*/
function onCanvasTouchStart(event) {
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 {MouseEvent} event Event that triggered this callback.
*/
function onPageMouseMove(event) {
updateInputFromMouse(event);
onPageInputMove();
}
/**
* Handler for when the player releases their mouse button on the page.
*/
function onPageMouseUp() {
onPageInputUp();
}
/**
* Handler for when the player moves their mobile touch on the page.
* @param {TouchEvent} event Event that triggered this callback.
*/
function onPageTouchMove(event) {
updateInputFromTouch(event);
onPageInputMove();
}
/**
* Handler for when the player releases their mobile touch on the page.
* @param {TouchEvent} 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 one of the player board's rows or columns defies the corresponding solution hint.
* @param {number|null} row Row number, if we're checking a row. null otherwise.
* @param {number|null} 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 (let 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 (let 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 (let 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 (let c = 0; c < playerCombos.length; c++) {
if (playerCombos[c] > hints[c].nr) return true;
}
} else {
let highestPlayerCombo = 0;
let highestHintCombo = 0;
for (let c = 0; c < playerCombos.length; c++) {
highestPlayerCombo = Math.max(highestPlayerCombo, playerCombos[c]);
}
for (let h = 0; h < hints.length; h++) {
highestHintCombo = Math.max(highestHintCombo, hints[h].nr);
}
if (highestPlayerCombo > highestHintCombo) return true;
}
// All good!
return false;
}
/**
* 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 (let 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} levelCode 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;
const 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 (let r = 0; r < board.nrRows; r++) {
for (let h = 0; h < board.rowHints[r].length; h++) {
boardData.rowHints.push(board.rowHints[r][h].state);
}
}
boardData.nrRowHints = boardData.rowHints.length;
for (let c = 0; c < board.nrCols; c++) {
for (let 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 (let 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 = CANVAS.WIDTH + 'px';
pikcrossDiv.style.height = CANVAS.HEIGHT + 'px';
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('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: [[File:Pikcross spritesheet.png]]
sprites = new Image();
sprites.onload = function () {
loaded[LOAD_CONTENT_SPRITES] = true;
updateCanvas();
};
sprites.src = 'https://mario.wiki.gallery/images/c/c7/Pikcross_spritesheet.png';
// Player progression.
for (let l = 0; l < LEVELS.length; l++) {
progression.push(false);
}
loadProgression();
// Levels data.
for (let 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 = undefined) {
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 (let c = 0; c < board.nrCols; c++) {
updateColumn(c);
}
for (let 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 (let 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);
board.autoMarkedCols[col] = allHintsAreFilled(board.colHints[col]) && playerLineMatchesHints(playerCombos, board.colHints[col]);
board.defiedCols[col] = playerLineDefiesHints(null, col, playerCombos, board.colHints[col]);
}
/**
* Updates the variables that hold the mouse state.
* @param {MouseEvent} 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 {TouchEvent} 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);
board.autoMarkedRows[row] = allHintsAreFilled(board.rowHints[row]) && playerLineMatchesHints(playerCombos, board.rowHints[row]);
board.defiedRows[row] = playerLineDefiesHints(row, null, playerCombos, board.rowHints[row]);
}
/**
* 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 = 8) {
if (this.#bitIdx + nrBits > 8) {
this.nextByte();
}
for (let 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;
/**
* Constructs 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 = 8) {
if (this.#bitIdx + nrBits > 8) {
this.nextByte();
}
let number = 0;
let byteValue = this.#str.charCodeAt(this.#byteIdx);
for (let 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();