1244 lines
31 KiB
JavaScript
1244 lines
31 KiB
JavaScript
'use strict';
|
|
|
|
var useLocalStorage = (typeof localStorage !== 'undefined');
|
|
|
|
/**
|
|
* if true, allows placing any card in the Flower slot, and dragons are always movable.
|
|
* @type {Boolean}
|
|
*/
|
|
var DEBUG = false;
|
|
|
|
/**
|
|
* if true, the alternate stylesheet is loaded even if images load correctly.
|
|
* @type {Boolean}
|
|
*/
|
|
var DEBUG_STYLE = false;
|
|
|
|
/**
|
|
* Time in milliseconds cards take moving around
|
|
* @type {Number}
|
|
*/
|
|
var CARD_ANIMATION_SPEED = 100;
|
|
|
|
/**
|
|
* Gap in pixels between cards when fanned out.
|
|
* @type {Number}
|
|
*/
|
|
var CARD_STACK_GAP = 30;
|
|
|
|
/**
|
|
* The seed of the game being played right now.
|
|
*/
|
|
var currentSeed;
|
|
|
|
var bambooWhiteToGreen = 'sepia(100%) saturate(10000%) hue-rotate(63deg) brightness(.35)';
|
|
|
|
var SUITS = {
|
|
BAMBOO: {
|
|
order: 1,
|
|
color: '#17714e',
|
|
prefix_large: 'bamboo',
|
|
small: 'bamboo',
|
|
fixAssetsFilter: bambooWhiteToGreen, // apply a color the the bamboo/green-dragon images since they changed to white.
|
|
},
|
|
CHARACTERS: {
|
|
order: 2,
|
|
color: '#000000',
|
|
prefix_large: 'char',
|
|
small: 'characters'
|
|
},
|
|
COINS: {
|
|
order: 3,
|
|
color: '#ae2810',
|
|
prefix_large: 'coins',
|
|
small: 'coins'
|
|
}
|
|
};
|
|
|
|
var SPECIAL = {
|
|
DRAGON_GREEN: {
|
|
order: 1,
|
|
large: 'dragon_green',
|
|
small: 'dragon_green',
|
|
equivalentSuit: 'bamboo',
|
|
fixAssetsFilter: bambooWhiteToGreen, // apply a color the the bamboo/green-dragon images since they changed to white.
|
|
},
|
|
DRAGON_RED: {
|
|
order: 2,
|
|
large: 'dragon_red',
|
|
small: 'dragon_red',
|
|
equivalentSuit: 'coins',
|
|
},
|
|
DRAGON_WHITE: {
|
|
order: 3,
|
|
large: 'dragon_white',
|
|
small: 'dragon_white',
|
|
equivalentSuit: 'characters',
|
|
},
|
|
FLOWER: {
|
|
order: 4,
|
|
large: 'flower',
|
|
small: 'flower',
|
|
equivalentSuit: 'flower',
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Number of each type of dragon to create.
|
|
* @type {Number}
|
|
*/
|
|
var DRAGON_COUNT = 4;
|
|
|
|
/**
|
|
* Height in px of the tray slots.
|
|
* @type {Number}
|
|
*/
|
|
var SLOT_TALL = 500;
|
|
|
|
/**
|
|
* Contains groupings of slots, which will have an "element" property added.
|
|
* @type {Object}
|
|
*/
|
|
var SLOTS = {
|
|
SPARE: [
|
|
{
|
|
type: 'spare',
|
|
top: 18,
|
|
left: 46
|
|
},
|
|
{
|
|
type: 'spare',
|
|
top: 18,
|
|
left: 198
|
|
},
|
|
{
|
|
type: 'spare',
|
|
top: 18,
|
|
left: 350
|
|
}
|
|
],
|
|
FLOWER: [
|
|
{
|
|
type: 'flower',
|
|
top: 18,
|
|
left: 614
|
|
}
|
|
],
|
|
OUT: [
|
|
{
|
|
type: 'out',
|
|
top: 18,
|
|
left: 806
|
|
},
|
|
{
|
|
type: 'out',
|
|
top: 18,
|
|
left: 958
|
|
},
|
|
{
|
|
type: 'out',
|
|
top: 18,
|
|
left: 1110
|
|
}
|
|
],
|
|
TRAY: [
|
|
{
|
|
type: 'tray',
|
|
fan: true,
|
|
top: 282,
|
|
left: 46,
|
|
height: SLOT_TALL
|
|
},
|
|
{
|
|
type: 'tray',
|
|
fan: true,
|
|
top: 282,
|
|
left: 198,
|
|
height: SLOT_TALL
|
|
},
|
|
{
|
|
type: 'tray',
|
|
fan: true,
|
|
top: 282,
|
|
left: 350,
|
|
height: SLOT_TALL
|
|
},
|
|
{
|
|
type: 'tray',
|
|
fan: true,
|
|
top: 282,
|
|
left: 502,
|
|
height: SLOT_TALL
|
|
},
|
|
{
|
|
type: 'tray',
|
|
fan: true,
|
|
top: 282,
|
|
left: 654,
|
|
height: SLOT_TALL
|
|
},
|
|
{
|
|
type: 'tray',
|
|
fan: true,
|
|
top: 282,
|
|
left: 806,
|
|
height: SLOT_TALL
|
|
},
|
|
{
|
|
type: 'tray',
|
|
fan: true,
|
|
top: 282,
|
|
left: 958,
|
|
height: SLOT_TALL
|
|
},
|
|
{
|
|
type: 'tray',
|
|
fan: true,
|
|
top: 282,
|
|
left: 1110,
|
|
height: SLOT_TALL
|
|
}
|
|
]
|
|
};
|
|
|
|
// Audio element for OST
|
|
var music = null;
|
|
|
|
jQuery.fn.visible = function () {
|
|
return this.css('visibility', 'visible');
|
|
};
|
|
|
|
jQuery.fn.invisible = function () {
|
|
return this.css('visibility', 'hidden');
|
|
};
|
|
|
|
jQuery.fn.visibilityToggle = function () {
|
|
return this.css('visibility', function (i, visibility) {
|
|
return (visibility == 'visible') ? 'hidden' : 'visible';
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a card of the given value and suit.
|
|
* @param {Integer} value
|
|
* @param {SUIT} suit
|
|
* @return {Card} {element: HTMLElement, value: Integer, suit: SUIT}
|
|
*/
|
|
function createCard(value, suit) {
|
|
var smallImg = 'solitaire/small_icons/' + suit.small + '.png';
|
|
var largeImg = 'solitaire/large_icons/' + suit.prefix_large + '_' + value + '.png';
|
|
var card = $('<div class="card card-numbered nickardson card-' + suit.small + ' card-' + value + '">' +
|
|
'<div class="card-count-a"></div>' +
|
|
'<div class="card-count-b"></div>' +
|
|
'<div class="card-mini-logo-a"></div>' +
|
|
'<div class="card-mini-logo-b"></div>' +
|
|
'<div class="card-logo"></div>' +
|
|
'</div>');
|
|
|
|
card.css('color', suit.color);
|
|
card.find('.card-count-a,.card-count-b').text(value);
|
|
card.find('.card-mini-logo-a,.card-mini-logo-b')
|
|
.css({
|
|
'background-image': 'url(' + smallImg + ')',
|
|
'filter': suit.fixAssetsFilter
|
|
});
|
|
card.find('.card-logo')
|
|
.css({
|
|
'background-image': 'url(' + largeImg + ')',
|
|
'filter': suit.fixAssetsFilter
|
|
});
|
|
|
|
var c = {
|
|
element: card,
|
|
value: value,
|
|
suit: suit
|
|
};
|
|
card.data('card', c);
|
|
return c;
|
|
}
|
|
|
|
/**
|
|
* Creates a 'special' card of the given type.
|
|
* @param {SPECIAL} special Card type definitition from the SPECIAL table.
|
|
* @return {Card}
|
|
*/
|
|
function createSpecialCard(special) {
|
|
var smallImg = 'solitaire/small_icons/' + special.small + '.png';
|
|
var largeImg = 'solitaire/large_icons/' + special.large + '.png';
|
|
var card = $('<div class="card card-special card-' + special.equivalentSuit + '">' +
|
|
'<div class="card-logo-a"></div>' +
|
|
'<div class="card-logo-b"></div>' +
|
|
'<div class="card-logo"></div>' +
|
|
'</div>');
|
|
|
|
card.find('.card-logo-a,.card-logo-b')
|
|
.css({
|
|
'background-image': 'url(' + smallImg + ')',
|
|
'filter': special.fixAssetsFilter
|
|
});
|
|
card.find('.card-logo')
|
|
.css({
|
|
'background-image': 'url(' + largeImg + ')',
|
|
'filter': special.fixAssetsFilter
|
|
});
|
|
|
|
var c = {
|
|
element: card,
|
|
special: special
|
|
};
|
|
card.data('card', c);
|
|
return c;
|
|
}
|
|
|
|
/**
|
|
* Places a card at the given level on the given slot.
|
|
* @param {Card} card The card to place.
|
|
* @param {Subslot} slot The destination slot
|
|
* @param {Integer} depth How high up the card is. Higher values are further up the stack. When fanned out, higher values are displayed closer to the bottom.
|
|
*/
|
|
function insertCard(card, slot, depth) {
|
|
if (card.slot !== undefined) {
|
|
card.slot.cards.splice(card.slot.cards.indexOf(card), 1); // remove the card from the previous slot
|
|
}
|
|
// add the card to a new slot.
|
|
slot.cards.splice(depth, 0, card);
|
|
card.slot = slot;
|
|
|
|
var e = $(slot.element);
|
|
|
|
var ce = card.element.detach();
|
|
if (depth === 0) {
|
|
e.prepend(ce);
|
|
} else {
|
|
var target = e.find('.card:nth-child(' + depth + ')');
|
|
if (target.length !== 0) {
|
|
target.after(ce);
|
|
} else {
|
|
e.append(ce);
|
|
}
|
|
}
|
|
|
|
var h = 0;
|
|
if (slot.fan === true) {
|
|
h = depth * CARD_STACK_GAP;
|
|
}
|
|
card.element.css({
|
|
'top': h + 'px',
|
|
'left': 0
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Randomize array element order in-place.
|
|
* Using Durstenfeld shuffle algorithm.
|
|
* @param {Array[?]} array Array to shuffle in-place
|
|
*/
|
|
function shuffleArray(array) {
|
|
for (var i = array.length - 1; i > 0; i--) {
|
|
var j = Math.floor(Math.random() * (i + 1));
|
|
var temp = array[i];
|
|
array[i] = array[j];
|
|
array[j] = temp;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates the elements for the slots, and prepares them to accept cards.
|
|
* @param {SLOTS} slots The SLOTS object, a set of arrays, each containing slots.
|
|
* @param {HTMLElement} board The element slots are parented to.
|
|
*/
|
|
function populateSlots(slots, board) {
|
|
// Create the slots
|
|
for (var slotname in slots) {
|
|
if (slots.hasOwnProperty(slotname)) {
|
|
var list = slots[slotname];
|
|
for (var i = 0; i < list.length; i++) {
|
|
var slot = $('<div class="slot"></div>').css({
|
|
top: list[i].top,
|
|
left: list[i].left
|
|
}).addClass('slot-' + list[i].type);
|
|
if (list[i].height !== undefined) {
|
|
slot.css('height', list[i].height);
|
|
}
|
|
slot.appendTo(board);
|
|
list[i].cards = [];
|
|
list[i].element = slot;
|
|
slot.data('slot', list[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates all the cards in a full deck.
|
|
* @return {Array[Card]}
|
|
*/
|
|
function makeDeck() {
|
|
var cards = [];
|
|
var s; // 0-2 suit index.
|
|
var suit; // the actual suit object
|
|
for (var value = 1; value <= 9; value++) {
|
|
for (s = 0; s < 3; s++) {
|
|
switch (s) {
|
|
case 0:
|
|
suit = SUITS.BAMBOO;
|
|
break;
|
|
case 1:
|
|
suit = SUITS.CHARACTERS;
|
|
break;
|
|
case 2:
|
|
suit = SUITS.COINS;
|
|
break;
|
|
}
|
|
cards.push(createCard(value, suit));
|
|
}
|
|
}
|
|
|
|
for (s = 0; s < 3; s++) {
|
|
for (var i = 0; i < DRAGON_COUNT; i++) {
|
|
switch (s) {
|
|
case 0:
|
|
suit = SPECIAL.DRAGON_GREEN;
|
|
break;
|
|
case 1:
|
|
suit = SPECIAL.DRAGON_WHITE;
|
|
break;
|
|
case 2:
|
|
suit = SPECIAL.DRAGON_RED;
|
|
break;
|
|
}
|
|
cards.push(createSpecialCard(suit));
|
|
}
|
|
}
|
|
|
|
cards.push(createSpecialCard(SPECIAL.FLOWER));
|
|
return cards;
|
|
}
|
|
|
|
/**
|
|
* Places the given cards onto the given board in a top-to-bottom, left-to-right fashion.
|
|
* @param {Array[Card]} cards List of cards to place
|
|
* @param {HTMLElement} board Parent element for the cards.
|
|
* @param {SLOT} traySet An array of individual slots, (ex: SLOTS.TRAY)
|
|
*/
|
|
function placeCardsInTray(cards, board, traySet) {
|
|
var row = 0;
|
|
var col = 0;
|
|
for (var i = 0; i < cards.length; i++) {
|
|
var card = cards[i];
|
|
card.element.appendTo(board);
|
|
insertCard(card, traySet[col], row);
|
|
|
|
row++;
|
|
if (row >= 5) {
|
|
row = 0;
|
|
col++;
|
|
}
|
|
}
|
|
onFieldUpdated();
|
|
}
|
|
|
|
/**
|
|
* Makes sure there are no vertical gaps between cards in the tray, by moving them towards the top to fill the gap.
|
|
*/
|
|
function balanceCards() {
|
|
for (var i = 0; i < SLOTS.TRAY.length; i++) {
|
|
for (var y = 0; y < SLOTS.TRAY[i].cards.length; y++) {
|
|
insertCard(SLOTS.TRAY[i].cards[y], SLOTS.TRAY[i], y);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets all special cards of the given type.
|
|
* @param {SPECIAL} type
|
|
* @return {Array[Card]}
|
|
*/
|
|
function getSpecialCards(type) {
|
|
var list = [];
|
|
for (var slotName in SLOTS) {
|
|
if (SLOTS.hasOwnProperty(slotName)) {
|
|
for (var i = 0; i < SLOTS[slotName].length; i++) {
|
|
var stack = SLOTS[slotName][i].cards;
|
|
for (var j = 0; j < stack.length; j++) {
|
|
var card = stack[j];
|
|
if (card.special == type) {
|
|
list.push(card);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
/**
|
|
* Gets all special cards of the given type which are on the top of their respective stack.
|
|
* @param {SPECIAL} type
|
|
* @return {Array[Card]}
|
|
*/
|
|
function getTopSpecialCards(type) {
|
|
var list = [];
|
|
for (var slotName in SLOTS) {
|
|
if (SLOTS.hasOwnProperty(slotName)) {
|
|
for (var i = 0; i < SLOTS[slotName].length; i++) {
|
|
var stack = SLOTS[slotName][i].cards;
|
|
if (stack.length === 0) {
|
|
continue;
|
|
}
|
|
var card = stack[stack.length - 1];
|
|
if (card.special == type) {
|
|
list.push(card);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
/**
|
|
* Gets the card with the given value and suit, if any.
|
|
* @param {Integer} value Card value
|
|
* @param {SUIT} suit Card suit
|
|
* @return {Card} The found card, or undefined.
|
|
*/
|
|
function getCard(value, suit) {
|
|
for (var slotName in SLOTS) {
|
|
if (SLOTS.hasOwnProperty(slotName)) {
|
|
for (var i = 0; i < SLOTS[slotName].length; i++) {
|
|
var stack = SLOTS[slotName][i].cards;
|
|
for (var j = 0; j < stack.length; j++) {
|
|
if (stack[j].value === value && stack[j].suit == suit) {
|
|
return stack[j];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets whether the dragons of the given type are all on the top of their respective stack, and a slot is open for them to go to.
|
|
* @param {SPECIAL} type
|
|
* @return {Boolean} Whether all dragon cards of that type are able to be moved, and there is an open slot.
|
|
*/
|
|
function isDragonReady(type) {
|
|
if (!DEBUG) {
|
|
var allAvailable = getTopSpecialCards(type).length == DRAGON_COUNT;
|
|
var spaceOpen = false;
|
|
|
|
for (var i = 0; i < SLOTS.SPARE.length; i++) {
|
|
if (SLOTS.SPARE[i].cards.length === 0 || SLOTS.SPARE[i].cards[0].special === type) {
|
|
spaceOpen = true;
|
|
}
|
|
}
|
|
|
|
return allAvailable && spaceOpen;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
var DRAGON_BTNS = [{
|
|
type: SPECIAL.DRAGON_RED,
|
|
selector: '#btn_dragon_red',
|
|
imgNone: 'solitaire/button_red_up.png',
|
|
imgReady: 'solitaire/button_red_active.png',
|
|
imgComplete: 'solitaire/button_red_down.png',
|
|
},
|
|
{
|
|
type: SPECIAL.DRAGON_GREEN,
|
|
selector: '#btn_dragon_green',
|
|
imgNone: 'solitaire/button_green_up.png',
|
|
imgReady: 'solitaire/button_green_active.png',
|
|
imgComplete: 'solitaire/button_green_down.png',
|
|
},
|
|
{
|
|
type: SPECIAL.DRAGON_WHITE,
|
|
selector: '#btn_dragon_white',
|
|
imgNone: 'solitaire/button_white_up.png',
|
|
imgReady: 'solitaire/button_white_active.png',
|
|
imgComplete: 'solitaire/button_white_down.png',
|
|
}
|
|
];
|
|
|
|
/**
|
|
* To be called when cards are done moving. Handles setting up the UI for dragons, etc.
|
|
*/
|
|
function onFieldUpdated() {
|
|
var i;
|
|
// prepare buttons if dragons are available
|
|
for (i = 0; i < DRAGON_BTNS.length; i++) {
|
|
var btn = DRAGON_BTNS[i];
|
|
if ($(btn.selector).data('complete') !== true) {
|
|
if (isDragonReady(btn.type)) {
|
|
$(btn.selector).css('background-image', 'url(\'' + btn.imgReady + '\')').data('active', true);
|
|
} else {
|
|
$(btn.selector).css('background-image', 'url(\'' + btn.imgNone + '\')').data('active', false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// move cards to the out tray when possible.
|
|
// it is movable when there are no cards that can be placed on that card, and the destination is 1 less than this card.
|
|
// this means that, for a BAMBOO 5, there must be no 4s anywhere in the tray or spare slots.
|
|
|
|
// build a list of cards which are on the top of their stacks, and are potentially eligible to be automatically moved.
|
|
var movableTops = [];
|
|
var cards;
|
|
var card;
|
|
for (i = 0; i < SLOTS.TRAY.length; i++) {
|
|
cards = SLOTS.TRAY[i].cards;
|
|
if (cards.length > 0) {
|
|
card = cards[cards.length - 1];
|
|
if (card.value || card.special === SPECIAL.FLOWER) {
|
|
movableTops.push(card);
|
|
}
|
|
}
|
|
}
|
|
for (i = 0; i < SLOTS.SPARE.length; i++) {
|
|
cards = SLOTS.SPARE[i].cards;
|
|
if (cards.length > 0) {
|
|
card = cards[cards.length - 1];
|
|
if (card.value || card.special === SPECIAL.FLOWER) {
|
|
movableTops.push(card);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < movableTops.length; i++) {
|
|
var canOut = true;
|
|
var outSlot = undefined;
|
|
|
|
card = movableTops[i];
|
|
if (card.special == SPECIAL.FLOWER) {
|
|
// flower can always move to flower slot.
|
|
outSlot = SLOTS.FLOWER[0];
|
|
} else if (card.value > 1) {
|
|
// output only if the card of the same suit with -1 value is in the out tray,
|
|
// AND if all cards with different suits and -2 value are in the out tray.
|
|
for (var suit in SUITS) {
|
|
var cardAbove;
|
|
|
|
if (card.suit === SUITS[suit]) {
|
|
// For checking the same suit, check if the value above has been placed.
|
|
cardAbove = getCard(card.value - 1, SUITS[suit]);
|
|
} else {
|
|
// For a different suit, the reasoning is more complex.
|
|
// The top card itself is free to move up if no other card would have a reason to be placed on it.
|
|
// Cards placable on it would have a value -1 from the top card.
|
|
// If the slot -1 from THAT card (-2 from the movable top card) is filled,
|
|
// then the moment the -1 card is revealed it will be moved, so the top card has no reason to consider that card as a reason to stay.
|
|
|
|
// The 2 card doesn't care about the cards in other suits with -2 values.
|
|
if (card.value === 2) {
|
|
continue;
|
|
}
|
|
|
|
cardAbove = getCard(card.value - 2, SUITS[suit]);
|
|
}
|
|
|
|
if (cardAbove) {
|
|
if (cardAbove.slot.type != 'out') {
|
|
canOut = false;
|
|
break;
|
|
} else {
|
|
// card-1 is in the out slot, save that location.
|
|
if (cardAbove.suit == card.suit) {
|
|
outSlot = cardAbove.slot;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// output this '1' valued card to the first empty 'out' slot
|
|
for (var j = 0; j < SLOTS.OUT.length; j++) {
|
|
if (SLOTS.OUT[j].cards.length === 0) {
|
|
outSlot = SLOTS.OUT[j];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (canOut && outSlot) {
|
|
tweenCard(card, outSlot, outSlot.cards.length);
|
|
setTimeout(onFieldUpdated, CARD_ANIMATION_SPEED);
|
|
// don't move any more top cards in this iteration, next will be moved after this card finishes.
|
|
break;
|
|
}
|
|
}
|
|
|
|
// no more top cards to move, is the field clear?
|
|
var allGood = true;
|
|
for (i = 0; i < SLOTS.TRAY.length; i++) {
|
|
if (SLOTS.TRAY[i].cards.length !== 0) {
|
|
allGood = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (allGood) {
|
|
if (!isInVictory) {
|
|
localStorage.shenzhen_win_count++;
|
|
updateWinCount();
|
|
}
|
|
isInVictory = true;
|
|
// wait for any possible animations to finish.
|
|
setTimeout(function () {
|
|
victoryScreen();
|
|
}, CARD_ANIMATION_SPEED);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves a card to a slot and position, then smoothly animates the transition.
|
|
* @param {Card} card The card to move
|
|
* @param {SLOT} slot The destination slot for the card
|
|
* @param {Integer} depth The position in the slot for the card
|
|
* @param {Function} callback Called when the animation is complete with the arguments (card, slot, depth)
|
|
*/
|
|
function tweenCard(card, slot, depth, callback) {
|
|
// remember the original position, move the card to determine the final position, then reset to original and interpolate between them.
|
|
|
|
var oldOffset = card.element.offset();
|
|
insertCard(card, slot, depth);
|
|
var newOffset = card.element.offset();
|
|
|
|
var dY = newOffset.top - oldOffset.top,
|
|
dX = newOffset.left - oldOffset.left;
|
|
var finalY = parseInt(card.element.css('top')),
|
|
finalX = parseInt(card.element.css('left'));
|
|
|
|
card.element.css({
|
|
top: finalY - dY,
|
|
left: finalX - dX,
|
|
'z-index': 99
|
|
});
|
|
|
|
card.element.animate({
|
|
top: finalY,
|
|
left: finalX
|
|
}, CARD_ANIMATION_SPEED, 'swing', function () {
|
|
card.element.css('z-index', '');
|
|
|
|
if (typeof callback === 'function') {
|
|
callback(card, slot, depth);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Intended as a parameter for tweenCard.
|
|
* Applies a backing to the card. Also adds special "dragon" backings if the win count is right.
|
|
* @param {Card} card The card
|
|
* @param {SLOT} slot ignored
|
|
* @param {Integer} depth ignored
|
|
*/
|
|
function applyCardBacking(card, _slot, _depth) {
|
|
card.element.addClass('card-reverse');
|
|
|
|
// special backing
|
|
if (useLocalStorage) {
|
|
if (localStorage.shenzhen_win_count >= 100) {
|
|
card.element.addClass('grand_dragon');
|
|
}
|
|
if (localStorage.shenzhen_win_count >= 200) {
|
|
card.element.addClass('grand_dragon_2');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates an jQuery action function for a button from DRAGON_BTNS
|
|
* @param {DRAGON_BTNS element} b The button description
|
|
* @return {Function}
|
|
*/
|
|
function dragonBtnListener(b) {
|
|
return function () {
|
|
if ($(this).data('active') === true) {
|
|
var i;
|
|
var list = getSpecialCards(b.type);
|
|
|
|
var openSlot;
|
|
for (i = 0; i < SLOTS.SPARE.length; i++) {
|
|
var set = SLOTS.SPARE[i].cards;
|
|
// TODO: if any spare slot already has this dragon, go to that one instead.
|
|
if (set.length >= DRAGON_COUNT && set[0].special == b.type) {
|
|
return false;
|
|
}
|
|
|
|
if (set.length === 0 || set[0].special == b.type && set.length < DRAGON_COUNT) {
|
|
openSlot = SLOTS.SPARE[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (list.length > 0 && openSlot !== undefined) {
|
|
for (i = 0; i < list.length; i++) {
|
|
tweenCard(list[i], openSlot, openSlot.cards.length, applyCardBacking);
|
|
}
|
|
$(b.selector).css('background-image', 'url(\'' + b.imgComplete + '\')').data('complete', true);
|
|
balanceCards();
|
|
onFieldUpdated();
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a function which either highlights or unhighlights dragons as specified by the button.
|
|
* @param {DRAGON_BTNS item} b Button specification
|
|
* @param {Boolean} isEnter If true, highlights the items. If false, removes the highlights.
|
|
* @return {Function} The created function
|
|
*/
|
|
function dragonEnterLeaveListener(b, isEnter) {
|
|
return function () {
|
|
if (dragging) {
|
|
return;
|
|
}
|
|
var cards = getSpecialCards(b.type);
|
|
|
|
for (var i = 0; i < cards.length; i++) {
|
|
if (isEnter) {
|
|
$(cards[i].element).addClass('card-highlight');
|
|
} else {
|
|
$(cards[i].element).removeClass('card-highlight');
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
for (var i = 0; i < DRAGON_BTNS.length; i++) {
|
|
var btn = DRAGON_BTNS[i];
|
|
$(btn.selector).click(dragonBtnListener(btn));
|
|
$(btn.selector).mouseenter(dragonEnterLeaveListener(btn, true));
|
|
$(btn.selector).mouseleave(dragonEnterLeaveListener(btn, false));
|
|
}
|
|
|
|
/**
|
|
* Finds whether the given stack is valid to be picked up,
|
|
* IE:
|
|
* If the stack is a single card.
|
|
* OR From the bottom-most card up, each is a numbered card, decreases by in value by 1, and is not the same color as the previous.
|
|
* @param {Array[Card]} stack A list of cards, with the first element being the "bottom-most" card.
|
|
* @param {SLOT} sourceSlot The type of slot this comes from
|
|
* @return {Boolean} Whether the stack can be picked up
|
|
*/
|
|
function canPickUpStack(stack, sourceSlot) {
|
|
if (sourceSlot.type == 'tray') {
|
|
if (stack.length == 1) {
|
|
return true;
|
|
} else {
|
|
for (var i = 1; i < stack.length; i++) {
|
|
var prev = stack[i - 1],
|
|
curr = stack[i];
|
|
if (prev.value && curr.value && prev.value == curr.value + 1 && prev.suit != curr.suit) {
|
|
continue;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
} else if (sourceSlot.type == 'spare') {
|
|
// once all dragons are stacked in there, you can't move it.
|
|
return sourceSlot.cards.length != DRAGON_COUNT;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds whether the given stack can be put on the given destination.
|
|
* The stack is assumed to be valid to pick up.
|
|
*
|
|
* The destination must be a numbered one. The stack bottom must be numbered, or undefined.
|
|
* The destination must be numbered, one more than the stack bottom, and not the same suit.
|
|
* @param {Array[Card]} stack The list of cards which are picked up.
|
|
* @param {SLOT} destSlot
|
|
* @param {Card} dest The card which the lowest of the stack will rest upon.
|
|
* @return {Boolean} Whether the stack can be placed on it.
|
|
*/
|
|
function canPlaceStack(stack, destSlot, dest) {
|
|
if (destSlot.type == 'tray') {
|
|
if (stack.length === 0 || dest === undefined) {
|
|
return true;
|
|
} else {
|
|
if (stack[0].value && dest.value) {
|
|
return (stack[0].value + 1 == dest.value) && (stack[0].suit != dest.suit);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
} else if (destSlot.type == 'flower') {
|
|
// only flower allowed in flower slot, except during debug
|
|
return stack[0].special === SPECIAL.FLOWER || DEBUG === true;
|
|
} else if (destSlot.type == 'spare') {
|
|
// only 1 card manually placed in spare slot
|
|
return destSlot.cards.length === 0 && stack.length == 1;
|
|
} else if (destSlot.type == 'out') {
|
|
// a single numbered card
|
|
if (stack.length === 1 && stack[0].value) {
|
|
if (dest === undefined) {
|
|
// if empty, must be value '1' card.
|
|
return stack[0].value == 1;
|
|
} else {
|
|
// otherwise, must be the next value
|
|
return stack[0].value == dest.value + 1 && stack[0].suit == dest.suit;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sorts the cards in a consistent order.
|
|
* Modifies the given array.
|
|
* @param {Array[Card]} cards The array of cards to be sorted.
|
|
*/
|
|
function sortCards(cards) {
|
|
cards.sort(function (a, b) {
|
|
var aHas = typeof a.value !== 'undefined';
|
|
var bHas = typeof b.value !== 'undefined';
|
|
if (aHas && bHas) {
|
|
if (a.value == b.value) {
|
|
return a.suit.order - b.suit.order;
|
|
} else {
|
|
return a.value - b.value;
|
|
}
|
|
} else {
|
|
if (aHas) {
|
|
return -1;
|
|
} else if (bHas) {
|
|
return 1;
|
|
} else {
|
|
return a.special.order - b.special.order;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sets up a new game with randomly placed cards.
|
|
* @param {Array[Card]} cards List of cards which will be placed.
|
|
* @param {HTMLElement} board The container for the cards.
|
|
* @param {Object} seed (optional) The random seed for shuffling the deck. If omitted, the time is used.
|
|
*/
|
|
function startNewGame(cards, board, seed) {
|
|
clearInterval(looper);
|
|
looper = undefined;
|
|
isInVictory = false;
|
|
|
|
// TODO: start cards face down in the flower slot, then move them into place.
|
|
|
|
sortCards(cards);
|
|
|
|
var truSeed = seed;
|
|
// use time-based seed if there is no seed, or is an empty string.
|
|
if (seed === undefined || (typeof seed === 'string' && seed.length === 0)) {
|
|
truSeed = new Date().getTime();
|
|
}
|
|
// if input is a numeric string, convert to an integer ("123" and 123 behave differently)
|
|
if (!isNaN(parseInt(truSeed, 10))) {
|
|
truSeed = parseInt(truSeed, 10);
|
|
}
|
|
|
|
currentSeed = truSeed;
|
|
|
|
Math.seedrandom(truSeed);
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log('Game id:', truSeed);
|
|
|
|
shuffleArray(cards); // shuffle cards
|
|
|
|
$('.card').finish().removeClass('card-reverse');
|
|
$('.btn-dragon').data('complete', false);
|
|
placeCardsInTray(cards, board, SLOTS.TRAY); // place cards
|
|
$('.card').visible();
|
|
}
|
|
|
|
function updateWinCount() {
|
|
if (useLocalStorage) {
|
|
$('#win_count').text(localStorage.shenzhen_win_count);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether the victory screen is currently running.
|
|
* @type {Boolean}
|
|
*/
|
|
var isInVictory = false;
|
|
var looper; // the interval identifier for the cards dropping in the victory screen.
|
|
/**
|
|
* Runs the victory screen, where cards drop down the screen.
|
|
*/
|
|
function victoryScreen() {
|
|
var cards = [];
|
|
|
|
var stax = $('.slot-spare,.slot-flower,.slot-out');
|
|
var foundThisRun = false;
|
|
do {
|
|
foundThisRun = false;
|
|
for (var i = 0; i < stax.length; i++) {
|
|
var childs = $(stax[i]).children();
|
|
var searchI = $(stax[i]).data('search-i') === undefined ? (childs.length - 1) : $(stax[i]).data('search-i');
|
|
if (searchI >= 0) {
|
|
cards.push(childs[searchI]);
|
|
foundThisRun = true;
|
|
$(stax[i]).data('search-i', searchI - 1);
|
|
}
|
|
}
|
|
} while (foundThisRun);
|
|
stax.removeData('search-i');
|
|
|
|
var row = 0;
|
|
|
|
// each iteration, over time, take the first card and shunt it down.
|
|
if (looper !== undefined) {
|
|
clearInterval(looper);
|
|
}
|
|
looper = setInterval(function () {
|
|
$(cards[row]).animate({
|
|
top: parseInt($(cards[row]).css('top')) + 1000
|
|
}, 1000);
|
|
|
|
row++;
|
|
if (row >= cards.length) {
|
|
clearInterval(looper);
|
|
looper = undefined;
|
|
isInVictory = false;
|
|
}
|
|
}, 50);
|
|
}
|
|
|
|
/**
|
|
* Loads the alternate stylesheet for when the images are missing.
|
|
*/
|
|
function loadAltStyle() {
|
|
$('head').append('<link rel="stylesheet" type="text/css" href="css/noimages.css">');
|
|
}
|
|
|
|
function setColorblindMode(isColorblindMode) {
|
|
var stylesheetId = 'colorblind-styles';
|
|
|
|
if (isColorblindMode) {
|
|
if (!$('#colorblind-styles').length) {
|
|
$('head').append('<link id="' + stylesheetId + '" rel="stylesheet" type="text/css" href="css/colorblind.css">');
|
|
}
|
|
} else {
|
|
$('#' + stylesheetId).remove();
|
|
}
|
|
|
|
if (useLocalStorage) {
|
|
localStorage.shenzhen_colorblind = isColorblindMode;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a stack of all cards including and stacked on top of the given card.
|
|
* @param {HTMLElement} cardElement The element for the card.
|
|
* @return {Array[Card]} An array of cards
|
|
*/
|
|
function getStackFromCardElement(cardElement) {
|
|
var card = $(cardElement).data('card');
|
|
|
|
var cardIndex = card.slot.cards.indexOf(card),
|
|
cardLength = card.slot.cards.length;
|
|
|
|
var stack = [];
|
|
for (var i = cardIndex; i < cardLength; i++) {
|
|
stack.push(card.slot.cards[i]);
|
|
}
|
|
|
|
return stack;
|
|
}
|
|
|
|
var cards;
|
|
var dragging = false;
|
|
$(document).ready(function () {
|
|
|
|
if (useLocalStorage) {
|
|
if (localStorage.shenzhen_win_count === undefined) {
|
|
localStorage.shenzhen_win_count = 0;
|
|
}
|
|
}
|
|
updateWinCount();
|
|
|
|
var board = $('#cards');
|
|
populateSlots(SLOTS, board);
|
|
|
|
cards = makeDeck();
|
|
|
|
// if there is a hash in the url upon load, load that as the seed.
|
|
startNewGame(cards, board, location.hash.replace('#', ''));
|
|
|
|
$('#newGame').click(function () {
|
|
// Replace the current state with the previous game
|
|
if (!location.hash) {
|
|
history.replaceState('', document.title, window.location.pathname + window.location.search + '#' + currentSeed);
|
|
}
|
|
// clear the hash from the url.
|
|
history.pushState('', document.title, window.location.pathname + window.location.search);
|
|
|
|
startNewGame(cards, board);
|
|
});
|
|
|
|
$('#retryGame').click(function () {
|
|
if (currentSeed !== null) {
|
|
if (location.hash.replace('#', '') === currentSeed.toString()) {
|
|
startNewGame(cards, board, location.hash.replace('#', ''));
|
|
} else {
|
|
location.hash = currentSeed;
|
|
// Triggers hashchange
|
|
}
|
|
}
|
|
});
|
|
|
|
addEventListener("hashchange", function () {
|
|
if (location.hash.replace('#', '')) {
|
|
startNewGame(cards, board, location.hash.replace('#', ''));
|
|
}
|
|
});
|
|
|
|
$('#playMusicButton').click(function () {
|
|
music.play();
|
|
if (music.currentTime > 0 && music.currentTime < 5) {
|
|
music.currentTime = 0;
|
|
}
|
|
$('#playMusicButton').hide();
|
|
$('#pauseMusicButton').show();
|
|
});
|
|
|
|
$('#pauseMusicButton').click(function () {
|
|
music.pause();
|
|
$('#playMusicButton').show();
|
|
$('#pauseMusicButton').hide();
|
|
});
|
|
|
|
$('#toggleColorblind').change(function (event) {
|
|
setColorblindMode(event.target.checked);
|
|
});
|
|
|
|
// When the alt style
|
|
if (useLocalStorage) {
|
|
if (localStorage.shenzhen_colorblind !== undefined && JSON.parse(localStorage.shenzhen_colorblind) === true) {
|
|
$('#toggleColorblind').prop('checked', true);
|
|
setColorblindMode(true);
|
|
}
|
|
}
|
|
|
|
// Make the cards interactable
|
|
$('.slot').droppable({
|
|
drop: function (event, ui) {
|
|
// drop is contingent on "accept", so this is a valid stack.
|
|
var stack = getStackFromCardElement(ui.draggable);
|
|
var slot = $(this).data('slot');
|
|
|
|
// insert all from stack into the bottom of the slot.
|
|
for (var i = 0; i < stack.length; i++) {
|
|
insertCard(stack[i], slot, slot.cards.length);
|
|
}
|
|
onFieldUpdated();
|
|
},
|
|
accept: function (draggable) {
|
|
var stack = getStackFromCardElement(draggable);
|
|
var slot = $(this).data('slot');
|
|
|
|
return canPlaceStack(stack, slot, slot.cards[slot.cards.length - 1]);
|
|
},
|
|
tolerance: 'pointer'
|
|
});
|
|
|
|
$('.card').draggable({
|
|
'cursor': 'url(assets/cursor_normal.png) 40 40, default',
|
|
'revert': 'invalid',
|
|
'revertDuration': CARD_ANIMATION_SPEED,
|
|
helper: function () {
|
|
var cardSet = $('<div></div>');
|
|
cardSet.css({
|
|
'z-index': 100,
|
|
'display': 'inline'
|
|
});
|
|
|
|
var card = $(this).data('card');
|
|
var cardIndex = card.slot.cards.indexOf(card),
|
|
cardLength = card.slot.cards.length;
|
|
|
|
for (var i = cardIndex, height = 0; i < cardLength; i++, height++) {
|
|
var e = card.slot.cards[i].element.clone();
|
|
e.css({
|
|
top: height * CARD_STACK_GAP,
|
|
left: '',
|
|
});
|
|
cardSet.append(e);
|
|
}
|
|
return cardSet;
|
|
},
|
|
start: function (event, _ui) {
|
|
var card = $(this).data('card');
|
|
|
|
var stack = [];
|
|
var cardIndex = card.slot.cards.indexOf(card),
|
|
cardLength = card.slot.cards.length;
|
|
var i;
|
|
for (i = cardIndex; i < cardLength; i++) {
|
|
stack.push(card.slot.cards[i]);
|
|
}
|
|
|
|
if (!card.element.is(':animated') && canPickUpStack(stack, card.slot)) {
|
|
for (i = 0; i < stack.length; i++) {
|
|
stack[i].element.invisible();
|
|
}
|
|
dragging = true;
|
|
} else {
|
|
event.stopPropagation();
|
|
event.stopImmediatePropagation();
|
|
event.preventDefault();
|
|
}
|
|
},
|
|
stop: function (_event, _ui) {
|
|
dragging = false;
|
|
var card = $(this).data('card');
|
|
|
|
var cardIndex = card.slot.cards.indexOf(card),
|
|
cardLength = card.slot.cards.length;
|
|
for (var i = cardIndex; i < cardLength; i++) {
|
|
card.slot.cards[i].element.visible();
|
|
}
|
|
}
|
|
});
|
|
|
|
// detect failed image load
|
|
var triggeredWarning = false;
|
|
|
|
// prepare for a canary check for if we have images
|
|
$('#canary').on('error', function (_data, _handler) {
|
|
if (!triggeredWarning) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn('Couldn\'t load an image from the original game. If you own SHENZHEN I/O, copy the game\'s "Content/textures/solitaire" folder into the "solitaire" directory of the cloned repository.');
|
|
|
|
loadAltStyle();
|
|
}
|
|
triggeredWarning = true;
|
|
});
|
|
|
|
// start the canary check
|
|
$('#canary').attr('src', 'solitaire/button_red_up.png');
|
|
|
|
music = new Audio("solitaire/Solitaire.ogg");
|
|
music.loop = true;
|
|
$(music).on('canplay', function () {
|
|
$('#playMusicButton').show();
|
|
$(music).off('canplay');
|
|
})
|
|
$(music).on('error', function (_data, _handler) {
|
|
console.warn('Couldn\'t load music from the original game. If you own SHENZHEN I/O, copy "Content/music/Solitaire.ogg" from the game into the "solitaire" directory of the cloned repository.');
|
|
});
|
|
|
|
$('html').keydown(function () { }); // UI breakpoint for debugging in Chrome
|
|
|
|
if (DEBUG_STYLE) {
|
|
loadAltStyle();
|
|
}
|
|
|
|
}); |