/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ship class for mergeable ships
var Ship = Container.expand(function () {
var self = Container.call(this);
// Level of the ship (1-6)
self.level = 1;
self.gridX = 0;
self.gridY = 0;
self.isDragging = false;
// Attach ship asset
var shipSprite = self.attachAsset('ship' + self.level, {
anchorX: 0.5,
anchorY: 0.5
});
// Update ship sprite to match level
self.setLevel = function (level) {
self.level = level;
self.removeChild(shipSprite);
var newSprite = self.attachAsset('ship' + level, {
anchorX: 0.5,
anchorY: 0.5
});
shipSprite = newSprite;
};
// Animate merge
self.animateMerge = function () {
tween(self, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
}
});
};
// Animate spawn
self.animateSpawn = function () {
self.scaleX = 0.2;
self.scaleY = 0.2;
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 180,
easing: tween.easeOut
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x0e1621
});
/****
* Game Code
****/
// --- Game Constants ---
// Pirate ship assets for different levels (1-6)
// Sea background image (replace 'sea_bg_id' with your actual asset id)
var GRID_SIZE = 5;
var CELL_SIZE = 230;
var GRID_MARGIN = 30;
var BOARD_WIDTH = GRID_SIZE * CELL_SIZE;
var BOARD_HEIGHT = GRID_SIZE * CELL_SIZE;
var BOARD_X = Math.floor((2048 - BOARD_WIDTH) / 2);
var BOARD_Y = Math.floor((2732 - BOARD_HEIGHT) / 2) + 60; // leave top margin for GUI
// --- Game State ---
var grid = []; // 2D array [y][x] of Ship or null
var ships = []; // All ship objects
var draggingShip = null;
var dragStartGrid = null;
var dragOffsetX = 0;
var dragOffsetY = 0;
var isMerging = false;
var score = 0;
var scoreTxt = null;
// --- GUI ---
scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFE066
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Sea Background ---
var seaBg = LK.getAsset('sea_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
game.addChildAt(seaBg, 0); // Add as the very first child so it's behind everything
// --- Board Background ---
for (var y = 0; y < GRID_SIZE; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
var cell = LK.getAsset('cell', {
anchorX: 0.5,
anchorY: 0.5,
x: BOARD_X + x * CELL_SIZE + CELL_SIZE / 2,
y: BOARD_Y + y * CELL_SIZE + CELL_SIZE / 2
});
game.addChild(cell);
}
}
// --- Grid Initialization ---
for (var y = 0; y < GRID_SIZE; y++) {
grid[y] = [];
for (var x = 0; x < GRID_SIZE; x++) {
grid[y][x] = null;
}
}
// --- Helper Functions ---
function getCellPos(x, y) {
return {
x: BOARD_X + x * CELL_SIZE + CELL_SIZE / 2,
y: BOARD_Y + y * CELL_SIZE + CELL_SIZE / 2
};
}
function isInsideGrid(x, y) {
return x >= 0 && x < GRID_SIZE && y >= 0 && y < GRID_SIZE;
}
function getGridFromPos(px, py) {
var gx = Math.floor((px - BOARD_X) / CELL_SIZE);
var gy = Math.floor((py - BOARD_Y) / CELL_SIZE);
if (isInsideGrid(gx, gy)) {
return {
x: gx,
y: gy
};
}
return null;
}
function spawnRandomShip() {
// Find all empty cells
var empties = [];
for (var y = 0; y < GRID_SIZE; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
if (!grid[y][x]) empties.push({
x: x,
y: y
});
}
}
if (empties.length === 0) return false;
var idx = Math.floor(Math.random() * empties.length);
var cell = empties[idx];
var level = Math.random() < 0.85 ? 1 : 2; // 85% level 1, 15% level 2
var ship = new Ship();
ship.setLevel(level);
ship.gridX = cell.x;
ship.gridY = cell.y;
var pos = getCellPos(cell.x, cell.y);
ship.x = pos.x;
ship.y = pos.y;
ship.animateSpawn();
grid[cell.y][cell.x] = ship;
ships.push(ship);
game.addChild(ship);
return true;
}
function canMerge(shipA, shipB) {
return shipA && shipB && shipA.level === shipB.level && shipA !== shipB;
}
// Find all connected ships of the same level (DFS)
function findConnectedSameLevelShips(startShip) {
var visited = {};
var stack = [startShip];
var result = [];
while (stack.length > 0) {
var ship = stack.pop();
if (!ship) continue; // Defensive: skip null/undefined ships
var key = ship.gridX + "," + ship.gridY;
if (visited[key]) continue;
visited[key] = true;
result.push(ship);
// Check 4 neighbors
var dirs = [[0, 1], [1, 0], [-1, 0], [0, -1]];
for (var d = 0; d < dirs.length; d++) {
var nx = ship.gridX + dirs[d][0];
var ny = ship.gridY + dirs[d][1];
if (isInsideGrid(nx, ny)) {
var neighbor = grid[ny][nx];
if (neighbor && neighbor.level === startShip.level && !visited[nx + "," + ny]) {
stack.push(neighbor);
}
}
}
}
return result;
}
// Merge 3 or more ships of the same level into one higher-level ship
function mergeThreeOrMoreShips(shipsToMerge) {
if (shipsToMerge.length < 3) return;
// Pick the first ship as the merge target
var target = shipsToMerge[0];
var newLevel = target.level + 1;
if (newLevel > 6) newLevel = 6;
target.setLevel(newLevel);
target.animateMerge();
// Remove the other ships
for (var i = 1; i < shipsToMerge.length; i++) {
var ship = shipsToMerge[i];
grid[ship.gridY][ship.gridX] = null;
var idx = ships.indexOf(ship);
if (idx !== -1) ships.splice(idx, 1);
ship.destroy();
}
// Update grid for target
grid[target.gridY][target.gridX] = target;
// If merged to max level, sink surrounding ships and give bonus
if (newLevel === 6) {
var dirs = [[0, 1], [1, 0], [-1, 0], [0, -1], [1, 1], [-1, 1], [1, -1], [-1, -1]];
var sunk = 0;
for (var d = 0; d < dirs.length; d++) {
var nx = target.gridX + dirs[d][0];
var ny = target.gridY + dirs[d][1];
if (isInsideGrid(nx, ny) && grid[ny][nx]) {
var s = grid[ny][nx];
grid[ny][nx] = null;
var idx2 = ships.indexOf(s);
if (idx2 !== -1) ships.splice(idx2, 1);
s.destroy();
sunk++;
}
}
// Bonus points for sinking
var bonus = 1000 * sunk;
score += bonus;
}
// Update score
var addScore = Math.pow(2, newLevel + 1) * 10 * shipsToMerge.length;
score += addScore;
scoreTxt.setText(score);
}
function hasMovesAvailable() {
// If any empty cell or any mergeable neighbor, return true
for (var y = 0; y < GRID_SIZE; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
var ship = grid[y][x];
if (!ship) return true;
// Check neighbors
var dirs = [[0, 1], [1, 0], [-1, 0], [0, -1]];
for (var d = 0; d < dirs.length; d++) {
var nx = x + dirs[d][0];
var ny = y + dirs[d][1];
if (isInsideGrid(nx, ny) && grid[ny][nx] && grid[ny][nx].level === ship.level) {
return true;
}
}
}
}
return false;
}
// --- Initial Ships ---
// No ships on the board at game start
// After initial spawn, check for any 3+ merges and resolve them
// (No need, board is empty)
// --- Drag & Drop Logic ---
game.down = function (x, y, obj) {
if (isMerging) return;
// Find ship at this position
var gridPos = getGridFromPos(x, y);
if (!gridPos) return;
var ship = grid[gridPos.y][gridPos.x];
if (!ship) return;
draggingShip = ship;
dragStartGrid = {
x: ship.gridX,
y: ship.gridY
};
dragOffsetX = x - ship.x;
dragOffsetY = y - ship.y;
ship.isDragging = true;
// Bring to front
game.removeChild(ship);
game.addChild(ship);
// Animate
tween(ship, {
scaleX: 1.15,
scaleY: 1.15
}, {
duration: 100,
easing: tween.easeOut
});
};
game.move = function (x, y, obj) {
if (!draggingShip || isMerging) return;
// Move ship with finger
draggingShip.x = x - dragOffsetX;
draggingShip.y = y - dragOffsetY;
};
game.up = function (x, y, obj) {
if (!draggingShip || isMerging) return;
// Snap to nearest cell
var gridPos = getGridFromPos(draggingShip.x, draggingShip.y);
if (!gridPos) {
// Out of bounds, return to original
var orig = getCellPos(dragStartGrid.x, dragStartGrid.y);
tween(draggingShip, {
x: orig.x,
y: orig.y,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
draggingShip.isDragging = false;
draggingShip = null;
return;
}
var targetShip = grid[gridPos.y][gridPos.x];
if (targetShip && canMerge(draggingShip, targetShip)) {
// Merge!
isMerging = true;
var pos = getCellPos(gridPos.x, gridPos.y);
tween(draggingShip, {
x: pos.x,
y: pos.y
}, {
duration: 100,
easing: tween.linear,
onFinish: function onFinish() {
mergeShips(draggingShip, targetShip);
// Move merged ship to cell
targetShip.gridX = gridPos.x;
targetShip.gridY = gridPos.y;
grid[dragStartGrid.y][dragStartGrid.x] = null;
grid[gridPos.y][gridPos.x] = targetShip;
isMerging = false;
draggingShip = null;
// After merge, check for 3+ merge at the merged cell
var connected = findConnectedSameLevelShips(targetShip);
while (connected.length >= 3) {
mergeThreeOrMoreShips(connected);
// After merge, check again at the same cell for new possible merges (chain reaction)
connected = findConnectedSameLevelShips(grid[targetShip.gridY][targetShip.gridX]);
}
// After merge, spawn new ship
if (!spawnRandomShip()) {
// No space, check for moves
if (!hasMovesAvailable()) {
LK.showGameOver();
}
}
}
});
} else if (!targetShip) {
// Move to empty cell
var pos = getCellPos(gridPos.x, gridPos.y);
tween(draggingShip, {
x: pos.x,
y: pos.y,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn,
onFinish: function onFinish() {
// Update grid
grid[dragStartGrid.y][dragStartGrid.x] = null;
grid[gridPos.y][gridPos.x] = draggingShip;
draggingShip.gridX = gridPos.x;
draggingShip.gridY = gridPos.y;
draggingShip.isDragging = false;
draggingShip = null;
// After move, check for 3+ merge at the new cell
var connected = findConnectedSameLevelShips(draggingShip);
while (connected.length >= 3) {
mergeThreeOrMoreShips(connected);
// After merge, check again at the same cell for new possible merges (chain reaction)
connected = findConnectedSameLevelShips(grid[draggingShip.gridY][draggingShip.gridX]);
}
// After move, spawn new ship
if (!spawnRandomShip()) {
if (!hasMovesAvailable()) {
LK.showGameOver();
}
}
}
});
} else {
// Occupied, can't merge, return to original
var orig = getCellPos(dragStartGrid.x, dragStartGrid.y);
tween(draggingShip, {
x: orig.x,
y: orig.y,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
draggingShip.isDragging = false;
draggingShip = null;
}
};
// --- Bottom Bar for Ship Selection ---
var unlockedLevels = [1]; // Always unlocked at start
var bottomBarShips = [];
var bottomBarY = 2732 - 180;
var bottomBarHeight = 180;
var bottomBarPadding = 40;
var bottomBarX = 0;
var bottomBarWidth = 2048;
var bottomBarContainer = new Container();
game.addChild(bottomBarContainer);
function updateBottomBar() {
// Remove old
for (var i = 0; i < bottomBarShips.length; i++) {
bottomBarShips[i].destroy();
}
bottomBarShips = [];
bottomBarContainer.removeChildren();
// Always show up to 3 lowest unlocked levels in the bar
var barLevels = unlockedLevels.slice(0, 3);
var n = barLevels.length;
var spacing = (bottomBarWidth - 2 * bottomBarPadding) / Math.max(1, n);
for (var i = 0; i < n; i++) {
var level = barLevels[i];
var ship = new Ship();
ship.setLevel(level);
ship.x = bottomBarPadding + spacing * i + spacing / 2;
ship.y = bottomBarY + bottomBarHeight / 2;
ship.scaleX = 0.8;
ship.scaleY = 0.8;
ship.isBottomBar = true;
ship.bottomBarIndex = i;
bottomBarContainer.addChild(ship);
bottomBarShips.push(ship);
}
}
updateBottomBar();
// --- Drag & Drop from Bottom Bar ---
var draggingFromBar = null;
var draggingBarShip = null;
var draggingBarLevel = null;
var draggingBarSprite = null;
game.down = function (x, y, obj) {
if (isMerging) return;
// Check if pressed on a bottom bar ship
for (var i = 0; i < bottomBarShips.length; i++) {
var ship = bottomBarShips[i];
var dx = x - ship.x;
var dy = y - ship.y;
var r = 90;
if (dx * dx + dy * dy < r * r) {
// Start dragging a new ship from bar
draggingFromBar = true;
draggingBarLevel = ship.level;
draggingBarSprite = new Ship();
draggingBarSprite.setLevel(ship.level);
draggingBarSprite.x = x;
draggingBarSprite.y = y;
draggingBarSprite.scaleX = 1.1;
draggingBarSprite.scaleY = 1.1;
draggingBarSprite.isDragging = true;
game.addChild(draggingBarSprite);
return;
}
}
// Otherwise, normal board drag
var gridPos = getGridFromPos(x, y);
if (!gridPos) return;
var ship = grid[gridPos.y][gridPos.x];
if (!ship) return;
// Prevent moving ships already placed on the board
return;
};
game.move = function (x, y, obj) {
if (isMerging) return;
if (draggingFromBar && draggingBarSprite) {
draggingBarSprite.x = x;
draggingBarSprite.y = y;
return;
}
if (!draggingShip) return;
draggingShip.x = x - dragOffsetX;
draggingShip.y = y - dragOffsetY;
};
game.up = function (x, y, obj) {
if (isMerging) return;
if (draggingFromBar && draggingBarSprite) {
// Try to place on board
var gridPos = getGridFromPos(x, y);
if (gridPos && !grid[gridPos.y][gridPos.x]) {
// Place new ship
var ship = draggingBarSprite;
ship.isDragging = false;
ship.gridX = gridPos.x;
ship.gridY = gridPos.y;
var pos = getCellPos(gridPos.x, gridPos.y);
tween(ship, {
x: pos.x,
y: pos.y,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn,
onFinish: function onFinish() {
grid[gridPos.y][gridPos.x] = ship;
ships.push(ship);
// After place, check for 3+ merge at the new cell
var connected = findConnectedSameLevelShips(ship);
while (connected.length >= 3) {
mergeThreeOrMoreShips(connected);
// After merge, check again at the same cell for new possible merges (chain reaction)
connected = findConnectedSameLevelShips(grid[ship.gridY][ship.gridX]);
}
// Replace the used ship in the bottom bar with a new random unlocked ship (if more than 1 unlocked)
if (unlockedLevels.length > 1) {
// Find the index of the used ship in the bar
var usedBarIndex = -1;
for (var i = 0; i < bottomBarShips.length; i++) {
if (bottomBarShips[i].level === draggingBarLevel) {
usedBarIndex = i;
break;
}
}
if (usedBarIndex !== -1) {
// Pick a random unlocked level (not the one just used, if possible)
var possibleLevels = unlockedLevels.slice();
if (possibleLevels.length > 1) {
var idx = possibleLevels.indexOf(draggingBarLevel);
if (idx !== -1) possibleLevels.splice(idx, 1);
}
var newLevel = possibleLevels[Math.floor(Math.random() * possibleLevels.length)];
// Replace the ship in the bar
bottomBarShips[usedBarIndex].destroy();
var shipBar = new Ship();
shipBar.setLevel(newLevel);
var n = bottomBarShips.length;
var spacing = (bottomBarWidth - 2 * bottomBarPadding) / Math.max(1, n);
shipBar.x = bottomBarPadding + spacing * usedBarIndex + spacing / 2;
shipBar.y = bottomBarY + bottomBarHeight / 2;
shipBar.scaleX = 0.8;
shipBar.scaleY = 0.8;
shipBar.isBottomBar = true;
shipBar.bottomBarIndex = usedBarIndex;
bottomBarContainer.addChild(shipBar);
bottomBarShips[usedBarIndex] = shipBar;
}
}
// After move, spawn new ship (if desired, or not)
if (!hasMovesAvailable()) {
LK.showGameOver();
}
}
});
} else {
// Not placed, destroy
draggingBarSprite.destroy();
}
draggingFromBar = false;
draggingBarSprite = null;
draggingBarLevel = null;
return;
}
if (!draggingShip) return;
// Snap to nearest cell
var gridPos = getGridFromPos(draggingShip.x, draggingShip.y);
if (!gridPos) {
// Out of bounds, return to original
var orig = getCellPos(dragStartGrid.x, dragStartGrid.y);
tween(draggingShip, {
x: orig.x,
y: orig.y,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
draggingShip.isDragging = false;
draggingShip = null;
return;
}
var targetShip = grid[gridPos.y][gridPos.x];
if (targetShip && canMerge(draggingShip, targetShip)) {
// Merge!
isMerging = true;
var pos = getCellPos(gridPos.x, gridPos.y);
tween(draggingShip, {
x: pos.x,
y: pos.y
}, {
duration: 100,
easing: tween.linear,
onFinish: function onFinish() {
mergeShips(draggingShip, targetShip);
// Move merged ship to cell
targetShip.gridX = gridPos.x;
targetShip.gridY = gridPos.y;
grid[dragStartGrid.y][dragStartGrid.x] = null;
grid[gridPos.y][gridPos.x] = targetShip;
isMerging = false;
draggingShip = null;
// After merge, check for 3+ merge at the merged cell
var connected = findConnectedSameLevelShips(targetShip);
while (connected.length >= 3) {
mergeThreeOrMoreShips(connected);
// After merge, check again at the same cell for new possible merges (chain reaction)
connected = findConnectedSameLevelShips(grid[targetShip.gridY][targetShip.gridX]);
}
// After merge, spawn new ship
if (!spawnRandomShip()) {
// No space, check for moves
if (!hasMovesAvailable()) {
LK.showGameOver();
}
}
}
});
} else if (!targetShip) {
// Move to empty cell
var pos = getCellPos(gridPos.x, gridPos.y);
tween(draggingShip, {
x: pos.x,
y: pos.y,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn,
onFinish: function onFinish() {
// Update grid
grid[dragStartGrid.y][dragStartGrid.x] = null;
grid[gridPos.y][gridPos.x] = draggingShip;
draggingShip.gridX = gridPos.x;
draggingShip.gridY = gridPos.y;
draggingShip.isDragging = false;
draggingShip = null;
// After move, check for 3+ merge at the new cell
var connected = findConnectedSameLevelShips(draggingShip);
while (connected.length >= 3) {
mergeThreeOrMoreShips(connected);
// After merge, check again at the same cell for new possible merges (chain reaction)
connected = findConnectedSameLevelShips(grid[draggingShip.gridY][draggingShip.gridX]);
}
// After move, spawn new ship
if (!spawnRandomShip()) {
if (!hasMovesAvailable()) {
LK.showGameOver();
}
}
}
});
} else {
// Occupied, can't merge, return to original
var orig = getCellPos(dragStartGrid.x, dragStartGrid.y);
tween(draggingShip, {
x: orig.x,
y: orig.y,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
draggingShip.isDragging = false;
draggingShip = null;
}
};
// --- Game Update (not used for logic, but could be for future animations) ---
game.update = function () {
// No per-frame logic needed for MVP
};
// --- Unlock new ship levels when merged ---
var _old_mergeThreeOrMoreShips = mergeThreeOrMoreShips;
mergeThreeOrMoreShips = function mergeThreeOrMoreShips(shipsToMerge) {
if (shipsToMerge.length < 3) return;
var oldLevel = shipsToMerge[0].level;
_old_mergeThreeOrMoreShips(shipsToMerge);
var newLevel = oldLevel + 1;
if (newLevel > 6) newLevel = 6;
if (unlockedLevels.indexOf(newLevel) === -1) {
unlockedLevels.push(newLevel);
unlockedLevels.sort(function (a, b) {
return a - b;
});
updateBottomBar();
}
};
// Merge two ships of the same level into one higher-level ship at the target's position
function mergeShips(shipA, shipB) {
if (!canMerge(shipA, shipB)) return;
// Find all connected same-level ships (including shipA and shipB)
var connected = findConnectedSameLevelShips(shipB);
// If not enough to merge, fallback to just merging two
if (connected.length < 2) connected = [shipA, shipB];
// Always merge into shipB (the target cell)
var newLevel = shipB.level + 1;
if (newLevel > 6) newLevel = 6;
shipB.setLevel(newLevel);
shipB.animateMerge();
// Remove all other ships in connected except shipB
for (var i = 0; i < connected.length; i++) {
var s = connected[i];
if (s !== shipB) {
grid[s.gridY][s.gridX] = null;
var idx = ships.indexOf(s);
if (idx !== -1) ships.splice(idx, 1);
s.destroy();
}
}
// Update grid for shipB
grid[shipB.gridY][shipB.gridX] = shipB;
// Update score for this merge
var addScore = Math.pow(2, newLevel + 1) * 10 * connected.length;
score += addScore;
scoreTxt.setText(score);
} /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ship class for mergeable ships
var Ship = Container.expand(function () {
var self = Container.call(this);
// Level of the ship (1-6)
self.level = 1;
self.gridX = 0;
self.gridY = 0;
self.isDragging = false;
// Attach ship asset
var shipSprite = self.attachAsset('ship' + self.level, {
anchorX: 0.5,
anchorY: 0.5
});
// Update ship sprite to match level
self.setLevel = function (level) {
self.level = level;
self.removeChild(shipSprite);
var newSprite = self.attachAsset('ship' + level, {
anchorX: 0.5,
anchorY: 0.5
});
shipSprite = newSprite;
};
// Animate merge
self.animateMerge = function () {
tween(self, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
}
});
};
// Animate spawn
self.animateSpawn = function () {
self.scaleX = 0.2;
self.scaleY = 0.2;
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 180,
easing: tween.easeOut
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x0e1621
});
/****
* Game Code
****/
// --- Game Constants ---
// Pirate ship assets for different levels (1-6)
// Sea background image (replace 'sea_bg_id' with your actual asset id)
var GRID_SIZE = 5;
var CELL_SIZE = 230;
var GRID_MARGIN = 30;
var BOARD_WIDTH = GRID_SIZE * CELL_SIZE;
var BOARD_HEIGHT = GRID_SIZE * CELL_SIZE;
var BOARD_X = Math.floor((2048 - BOARD_WIDTH) / 2);
var BOARD_Y = Math.floor((2732 - BOARD_HEIGHT) / 2) + 60; // leave top margin for GUI
// --- Game State ---
var grid = []; // 2D array [y][x] of Ship or null
var ships = []; // All ship objects
var draggingShip = null;
var dragStartGrid = null;
var dragOffsetX = 0;
var dragOffsetY = 0;
var isMerging = false;
var score = 0;
var scoreTxt = null;
// --- GUI ---
scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFE066
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Sea Background ---
var seaBg = LK.getAsset('sea_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
game.addChildAt(seaBg, 0); // Add as the very first child so it's behind everything
// --- Board Background ---
for (var y = 0; y < GRID_SIZE; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
var cell = LK.getAsset('cell', {
anchorX: 0.5,
anchorY: 0.5,
x: BOARD_X + x * CELL_SIZE + CELL_SIZE / 2,
y: BOARD_Y + y * CELL_SIZE + CELL_SIZE / 2
});
game.addChild(cell);
}
}
// --- Grid Initialization ---
for (var y = 0; y < GRID_SIZE; y++) {
grid[y] = [];
for (var x = 0; x < GRID_SIZE; x++) {
grid[y][x] = null;
}
}
// --- Helper Functions ---
function getCellPos(x, y) {
return {
x: BOARD_X + x * CELL_SIZE + CELL_SIZE / 2,
y: BOARD_Y + y * CELL_SIZE + CELL_SIZE / 2
};
}
function isInsideGrid(x, y) {
return x >= 0 && x < GRID_SIZE && y >= 0 && y < GRID_SIZE;
}
function getGridFromPos(px, py) {
var gx = Math.floor((px - BOARD_X) / CELL_SIZE);
var gy = Math.floor((py - BOARD_Y) / CELL_SIZE);
if (isInsideGrid(gx, gy)) {
return {
x: gx,
y: gy
};
}
return null;
}
function spawnRandomShip() {
// Find all empty cells
var empties = [];
for (var y = 0; y < GRID_SIZE; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
if (!grid[y][x]) empties.push({
x: x,
y: y
});
}
}
if (empties.length === 0) return false;
var idx = Math.floor(Math.random() * empties.length);
var cell = empties[idx];
var level = Math.random() < 0.85 ? 1 : 2; // 85% level 1, 15% level 2
var ship = new Ship();
ship.setLevel(level);
ship.gridX = cell.x;
ship.gridY = cell.y;
var pos = getCellPos(cell.x, cell.y);
ship.x = pos.x;
ship.y = pos.y;
ship.animateSpawn();
grid[cell.y][cell.x] = ship;
ships.push(ship);
game.addChild(ship);
return true;
}
function canMerge(shipA, shipB) {
return shipA && shipB && shipA.level === shipB.level && shipA !== shipB;
}
// Find all connected ships of the same level (DFS)
function findConnectedSameLevelShips(startShip) {
var visited = {};
var stack = [startShip];
var result = [];
while (stack.length > 0) {
var ship = stack.pop();
if (!ship) continue; // Defensive: skip null/undefined ships
var key = ship.gridX + "," + ship.gridY;
if (visited[key]) continue;
visited[key] = true;
result.push(ship);
// Check 4 neighbors
var dirs = [[0, 1], [1, 0], [-1, 0], [0, -1]];
for (var d = 0; d < dirs.length; d++) {
var nx = ship.gridX + dirs[d][0];
var ny = ship.gridY + dirs[d][1];
if (isInsideGrid(nx, ny)) {
var neighbor = grid[ny][nx];
if (neighbor && neighbor.level === startShip.level && !visited[nx + "," + ny]) {
stack.push(neighbor);
}
}
}
}
return result;
}
// Merge 3 or more ships of the same level into one higher-level ship
function mergeThreeOrMoreShips(shipsToMerge) {
if (shipsToMerge.length < 3) return;
// Pick the first ship as the merge target
var target = shipsToMerge[0];
var newLevel = target.level + 1;
if (newLevel > 6) newLevel = 6;
target.setLevel(newLevel);
target.animateMerge();
// Remove the other ships
for (var i = 1; i < shipsToMerge.length; i++) {
var ship = shipsToMerge[i];
grid[ship.gridY][ship.gridX] = null;
var idx = ships.indexOf(ship);
if (idx !== -1) ships.splice(idx, 1);
ship.destroy();
}
// Update grid for target
grid[target.gridY][target.gridX] = target;
// If merged to max level, sink surrounding ships and give bonus
if (newLevel === 6) {
var dirs = [[0, 1], [1, 0], [-1, 0], [0, -1], [1, 1], [-1, 1], [1, -1], [-1, -1]];
var sunk = 0;
for (var d = 0; d < dirs.length; d++) {
var nx = target.gridX + dirs[d][0];
var ny = target.gridY + dirs[d][1];
if (isInsideGrid(nx, ny) && grid[ny][nx]) {
var s = grid[ny][nx];
grid[ny][nx] = null;
var idx2 = ships.indexOf(s);
if (idx2 !== -1) ships.splice(idx2, 1);
s.destroy();
sunk++;
}
}
// Bonus points for sinking
var bonus = 1000 * sunk;
score += bonus;
}
// Update score
var addScore = Math.pow(2, newLevel + 1) * 10 * shipsToMerge.length;
score += addScore;
scoreTxt.setText(score);
}
function hasMovesAvailable() {
// If any empty cell or any mergeable neighbor, return true
for (var y = 0; y < GRID_SIZE; y++) {
for (var x = 0; x < GRID_SIZE; x++) {
var ship = grid[y][x];
if (!ship) return true;
// Check neighbors
var dirs = [[0, 1], [1, 0], [-1, 0], [0, -1]];
for (var d = 0; d < dirs.length; d++) {
var nx = x + dirs[d][0];
var ny = y + dirs[d][1];
if (isInsideGrid(nx, ny) && grid[ny][nx] && grid[ny][nx].level === ship.level) {
return true;
}
}
}
}
return false;
}
// --- Initial Ships ---
// No ships on the board at game start
// After initial spawn, check for any 3+ merges and resolve them
// (No need, board is empty)
// --- Drag & Drop Logic ---
game.down = function (x, y, obj) {
if (isMerging) return;
// Find ship at this position
var gridPos = getGridFromPos(x, y);
if (!gridPos) return;
var ship = grid[gridPos.y][gridPos.x];
if (!ship) return;
draggingShip = ship;
dragStartGrid = {
x: ship.gridX,
y: ship.gridY
};
dragOffsetX = x - ship.x;
dragOffsetY = y - ship.y;
ship.isDragging = true;
// Bring to front
game.removeChild(ship);
game.addChild(ship);
// Animate
tween(ship, {
scaleX: 1.15,
scaleY: 1.15
}, {
duration: 100,
easing: tween.easeOut
});
};
game.move = function (x, y, obj) {
if (!draggingShip || isMerging) return;
// Move ship with finger
draggingShip.x = x - dragOffsetX;
draggingShip.y = y - dragOffsetY;
};
game.up = function (x, y, obj) {
if (!draggingShip || isMerging) return;
// Snap to nearest cell
var gridPos = getGridFromPos(draggingShip.x, draggingShip.y);
if (!gridPos) {
// Out of bounds, return to original
var orig = getCellPos(dragStartGrid.x, dragStartGrid.y);
tween(draggingShip, {
x: orig.x,
y: orig.y,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
draggingShip.isDragging = false;
draggingShip = null;
return;
}
var targetShip = grid[gridPos.y][gridPos.x];
if (targetShip && canMerge(draggingShip, targetShip)) {
// Merge!
isMerging = true;
var pos = getCellPos(gridPos.x, gridPos.y);
tween(draggingShip, {
x: pos.x,
y: pos.y
}, {
duration: 100,
easing: tween.linear,
onFinish: function onFinish() {
mergeShips(draggingShip, targetShip);
// Move merged ship to cell
targetShip.gridX = gridPos.x;
targetShip.gridY = gridPos.y;
grid[dragStartGrid.y][dragStartGrid.x] = null;
grid[gridPos.y][gridPos.x] = targetShip;
isMerging = false;
draggingShip = null;
// After merge, check for 3+ merge at the merged cell
var connected = findConnectedSameLevelShips(targetShip);
while (connected.length >= 3) {
mergeThreeOrMoreShips(connected);
// After merge, check again at the same cell for new possible merges (chain reaction)
connected = findConnectedSameLevelShips(grid[targetShip.gridY][targetShip.gridX]);
}
// After merge, spawn new ship
if (!spawnRandomShip()) {
// No space, check for moves
if (!hasMovesAvailable()) {
LK.showGameOver();
}
}
}
});
} else if (!targetShip) {
// Move to empty cell
var pos = getCellPos(gridPos.x, gridPos.y);
tween(draggingShip, {
x: pos.x,
y: pos.y,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn,
onFinish: function onFinish() {
// Update grid
grid[dragStartGrid.y][dragStartGrid.x] = null;
grid[gridPos.y][gridPos.x] = draggingShip;
draggingShip.gridX = gridPos.x;
draggingShip.gridY = gridPos.y;
draggingShip.isDragging = false;
draggingShip = null;
// After move, check for 3+ merge at the new cell
var connected = findConnectedSameLevelShips(draggingShip);
while (connected.length >= 3) {
mergeThreeOrMoreShips(connected);
// After merge, check again at the same cell for new possible merges (chain reaction)
connected = findConnectedSameLevelShips(grid[draggingShip.gridY][draggingShip.gridX]);
}
// After move, spawn new ship
if (!spawnRandomShip()) {
if (!hasMovesAvailable()) {
LK.showGameOver();
}
}
}
});
} else {
// Occupied, can't merge, return to original
var orig = getCellPos(dragStartGrid.x, dragStartGrid.y);
tween(draggingShip, {
x: orig.x,
y: orig.y,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
draggingShip.isDragging = false;
draggingShip = null;
}
};
// --- Bottom Bar for Ship Selection ---
var unlockedLevels = [1]; // Always unlocked at start
var bottomBarShips = [];
var bottomBarY = 2732 - 180;
var bottomBarHeight = 180;
var bottomBarPadding = 40;
var bottomBarX = 0;
var bottomBarWidth = 2048;
var bottomBarContainer = new Container();
game.addChild(bottomBarContainer);
function updateBottomBar() {
// Remove old
for (var i = 0; i < bottomBarShips.length; i++) {
bottomBarShips[i].destroy();
}
bottomBarShips = [];
bottomBarContainer.removeChildren();
// Always show up to 3 lowest unlocked levels in the bar
var barLevels = unlockedLevels.slice(0, 3);
var n = barLevels.length;
var spacing = (bottomBarWidth - 2 * bottomBarPadding) / Math.max(1, n);
for (var i = 0; i < n; i++) {
var level = barLevels[i];
var ship = new Ship();
ship.setLevel(level);
ship.x = bottomBarPadding + spacing * i + spacing / 2;
ship.y = bottomBarY + bottomBarHeight / 2;
ship.scaleX = 0.8;
ship.scaleY = 0.8;
ship.isBottomBar = true;
ship.bottomBarIndex = i;
bottomBarContainer.addChild(ship);
bottomBarShips.push(ship);
}
}
updateBottomBar();
// --- Drag & Drop from Bottom Bar ---
var draggingFromBar = null;
var draggingBarShip = null;
var draggingBarLevel = null;
var draggingBarSprite = null;
game.down = function (x, y, obj) {
if (isMerging) return;
// Check if pressed on a bottom bar ship
for (var i = 0; i < bottomBarShips.length; i++) {
var ship = bottomBarShips[i];
var dx = x - ship.x;
var dy = y - ship.y;
var r = 90;
if (dx * dx + dy * dy < r * r) {
// Start dragging a new ship from bar
draggingFromBar = true;
draggingBarLevel = ship.level;
draggingBarSprite = new Ship();
draggingBarSprite.setLevel(ship.level);
draggingBarSprite.x = x;
draggingBarSprite.y = y;
draggingBarSprite.scaleX = 1.1;
draggingBarSprite.scaleY = 1.1;
draggingBarSprite.isDragging = true;
game.addChild(draggingBarSprite);
return;
}
}
// Otherwise, normal board drag
var gridPos = getGridFromPos(x, y);
if (!gridPos) return;
var ship = grid[gridPos.y][gridPos.x];
if (!ship) return;
// Prevent moving ships already placed on the board
return;
};
game.move = function (x, y, obj) {
if (isMerging) return;
if (draggingFromBar && draggingBarSprite) {
draggingBarSprite.x = x;
draggingBarSprite.y = y;
return;
}
if (!draggingShip) return;
draggingShip.x = x - dragOffsetX;
draggingShip.y = y - dragOffsetY;
};
game.up = function (x, y, obj) {
if (isMerging) return;
if (draggingFromBar && draggingBarSprite) {
// Try to place on board
var gridPos = getGridFromPos(x, y);
if (gridPos && !grid[gridPos.y][gridPos.x]) {
// Place new ship
var ship = draggingBarSprite;
ship.isDragging = false;
ship.gridX = gridPos.x;
ship.gridY = gridPos.y;
var pos = getCellPos(gridPos.x, gridPos.y);
tween(ship, {
x: pos.x,
y: pos.y,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn,
onFinish: function onFinish() {
grid[gridPos.y][gridPos.x] = ship;
ships.push(ship);
// After place, check for 3+ merge at the new cell
var connected = findConnectedSameLevelShips(ship);
while (connected.length >= 3) {
mergeThreeOrMoreShips(connected);
// After merge, check again at the same cell for new possible merges (chain reaction)
connected = findConnectedSameLevelShips(grid[ship.gridY][ship.gridX]);
}
// Replace the used ship in the bottom bar with a new random unlocked ship (if more than 1 unlocked)
if (unlockedLevels.length > 1) {
// Find the index of the used ship in the bar
var usedBarIndex = -1;
for (var i = 0; i < bottomBarShips.length; i++) {
if (bottomBarShips[i].level === draggingBarLevel) {
usedBarIndex = i;
break;
}
}
if (usedBarIndex !== -1) {
// Pick a random unlocked level (not the one just used, if possible)
var possibleLevels = unlockedLevels.slice();
if (possibleLevels.length > 1) {
var idx = possibleLevels.indexOf(draggingBarLevel);
if (idx !== -1) possibleLevels.splice(idx, 1);
}
var newLevel = possibleLevels[Math.floor(Math.random() * possibleLevels.length)];
// Replace the ship in the bar
bottomBarShips[usedBarIndex].destroy();
var shipBar = new Ship();
shipBar.setLevel(newLevel);
var n = bottomBarShips.length;
var spacing = (bottomBarWidth - 2 * bottomBarPadding) / Math.max(1, n);
shipBar.x = bottomBarPadding + spacing * usedBarIndex + spacing / 2;
shipBar.y = bottomBarY + bottomBarHeight / 2;
shipBar.scaleX = 0.8;
shipBar.scaleY = 0.8;
shipBar.isBottomBar = true;
shipBar.bottomBarIndex = usedBarIndex;
bottomBarContainer.addChild(shipBar);
bottomBarShips[usedBarIndex] = shipBar;
}
}
// After move, spawn new ship (if desired, or not)
if (!hasMovesAvailable()) {
LK.showGameOver();
}
}
});
} else {
// Not placed, destroy
draggingBarSprite.destroy();
}
draggingFromBar = false;
draggingBarSprite = null;
draggingBarLevel = null;
return;
}
if (!draggingShip) return;
// Snap to nearest cell
var gridPos = getGridFromPos(draggingShip.x, draggingShip.y);
if (!gridPos) {
// Out of bounds, return to original
var orig = getCellPos(dragStartGrid.x, dragStartGrid.y);
tween(draggingShip, {
x: orig.x,
y: orig.y,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
draggingShip.isDragging = false;
draggingShip = null;
return;
}
var targetShip = grid[gridPos.y][gridPos.x];
if (targetShip && canMerge(draggingShip, targetShip)) {
// Merge!
isMerging = true;
var pos = getCellPos(gridPos.x, gridPos.y);
tween(draggingShip, {
x: pos.x,
y: pos.y
}, {
duration: 100,
easing: tween.linear,
onFinish: function onFinish() {
mergeShips(draggingShip, targetShip);
// Move merged ship to cell
targetShip.gridX = gridPos.x;
targetShip.gridY = gridPos.y;
grid[dragStartGrid.y][dragStartGrid.x] = null;
grid[gridPos.y][gridPos.x] = targetShip;
isMerging = false;
draggingShip = null;
// After merge, check for 3+ merge at the merged cell
var connected = findConnectedSameLevelShips(targetShip);
while (connected.length >= 3) {
mergeThreeOrMoreShips(connected);
// After merge, check again at the same cell for new possible merges (chain reaction)
connected = findConnectedSameLevelShips(grid[targetShip.gridY][targetShip.gridX]);
}
// After merge, spawn new ship
if (!spawnRandomShip()) {
// No space, check for moves
if (!hasMovesAvailable()) {
LK.showGameOver();
}
}
}
});
} else if (!targetShip) {
// Move to empty cell
var pos = getCellPos(gridPos.x, gridPos.y);
tween(draggingShip, {
x: pos.x,
y: pos.y,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn,
onFinish: function onFinish() {
// Update grid
grid[dragStartGrid.y][dragStartGrid.x] = null;
grid[gridPos.y][gridPos.x] = draggingShip;
draggingShip.gridX = gridPos.x;
draggingShip.gridY = gridPos.y;
draggingShip.isDragging = false;
draggingShip = null;
// After move, check for 3+ merge at the new cell
var connected = findConnectedSameLevelShips(draggingShip);
while (connected.length >= 3) {
mergeThreeOrMoreShips(connected);
// After merge, check again at the same cell for new possible merges (chain reaction)
connected = findConnectedSameLevelShips(grid[draggingShip.gridY][draggingShip.gridX]);
}
// After move, spawn new ship
if (!spawnRandomShip()) {
if (!hasMovesAvailable()) {
LK.showGameOver();
}
}
}
});
} else {
// Occupied, can't merge, return to original
var orig = getCellPos(dragStartGrid.x, dragStartGrid.y);
tween(draggingShip, {
x: orig.x,
y: orig.y,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
draggingShip.isDragging = false;
draggingShip = null;
}
};
// --- Game Update (not used for logic, but could be for future animations) ---
game.update = function () {
// No per-frame logic needed for MVP
};
// --- Unlock new ship levels when merged ---
var _old_mergeThreeOrMoreShips = mergeThreeOrMoreShips;
mergeThreeOrMoreShips = function mergeThreeOrMoreShips(shipsToMerge) {
if (shipsToMerge.length < 3) return;
var oldLevel = shipsToMerge[0].level;
_old_mergeThreeOrMoreShips(shipsToMerge);
var newLevel = oldLevel + 1;
if (newLevel > 6) newLevel = 6;
if (unlockedLevels.indexOf(newLevel) === -1) {
unlockedLevels.push(newLevel);
unlockedLevels.sort(function (a, b) {
return a - b;
});
updateBottomBar();
}
};
// Merge two ships of the same level into one higher-level ship at the target's position
function mergeShips(shipA, shipB) {
if (!canMerge(shipA, shipB)) return;
// Find all connected same-level ships (including shipA and shipB)
var connected = findConnectedSameLevelShips(shipB);
// If not enough to merge, fallback to just merging two
if (connected.length < 2) connected = [shipA, shipB];
// Always merge into shipB (the target cell)
var newLevel = shipB.level + 1;
if (newLevel > 6) newLevel = 6;
shipB.setLevel(newLevel);
shipB.animateMerge();
// Remove all other ships in connected except shipB
for (var i = 0; i < connected.length; i++) {
var s = connected[i];
if (s !== shipB) {
grid[s.gridY][s.gridX] = null;
var idx = ships.indexOf(s);
if (idx !== -1) ships.splice(idx, 1);
s.destroy();
}
}
// Update grid for shipB
grid[shipB.gridY][shipB.gridX] = shipB;
// Update score for this merge
var addScore = Math.pow(2, newLevel + 1) * 10 * connected.length;
score += addScore;
scoreTxt.setText(score);
}
deniz arka plan. In-Game asset. 2d. High contrast. No shadows
3d piksar tarzı şeffaf kare. In-Game asset. 2d. High contrast. No shadows
3d pixar tarzı korsan gemisi beyaz renk. In-Game asset. 2d. High contrast. No shadows
beyaz bölgeler sarı olsun
beyaz kısımlar açık yeşil olsun
beyaz kısımlar mavi olsun
mavi kısımlar koyu mor olsun
mor kısımlar siyah olsun