/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Crystal = Container.expand(function () {
var self = Container.call(this);
var crystal = self.attachAsset('crystal', {
anchorX: 0.5,
anchorY: 0.5
});
crystal.rotation = Math.PI / 4; // 45 degrees to make diamond shape
// Create larger invisible hitbox for better touch interaction
var hitbox = self.attachAsset('crystal', {
anchorX: 0.5,
anchorY: 0.5,
width: 180,
height: 180
});
hitbox.alpha = 0; // Make invisible
hitbox.interactive = true; // Ensure it can receive touch events
self.sparkleTimer = 0;
self.isDragging = false;
self.originalX = 0;
self.originalY = 0;
self.isInPot = false;
self.lastIsInPot = false;
self.down = function (x, y, obj) {
self.isDragging = true;
draggedCrystal = self;
self.originalX = self.x;
self.originalY = self.y;
// Bring crystal to front and scale it up slightly
tween(self, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.9
}, {
duration: 200,
easing: tween.easeOut
});
// Add visual feedback on touch
tween(crystal, {
tint: 0xFFFFFF
}, {
duration: 100,
easing: tween.easeOut
});
};
self.up = function (x, y, obj) {
self.isDragging = false;
draggedCrystal = null;
// Restore original tint when releasing
tween(crystal, {
tint: crystal.originalTint || 0xFFFFFF
}, {
duration: 100,
easing: tween.easeOut
});
// Check if crystal is dropped in pot
if (self.isInPot) {
// Get crystal color from tint
var crystalColor = self.children[0].tint;
// Add to crystals in pot tracking
crystalsInPot.push(crystalColor);
// Update available movements based on crystal color
if (crystalColor === 0x8B4513) {
// Brown crystal
availableMovements.push({
type: 'brown',
downSteps: 2,
leftSteps: 1
});
} else if (crystalColor === 0xff0000) {
// Red crystal
availableMovements.push({
type: 'red',
upSteps: 1,
rightSteps: 3
});
} else if (crystalColor === 0x008080) {
// Teal crystal
availableMovements.push({
type: 'teal',
leftSteps: 2,
upSteps: 2
});
} else if (crystalColor === 0x800080) {
// Purple crystal
availableMovements.push({
type: 'purple',
rightSteps: 1,
downSteps: 1
});
}
// Crystal successfully added to pot - destroy it with effect
tween(self, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
// Remove from crystals array
for (var i = crystals.length - 1; i >= 0; i--) {
if (crystals[i] === self) {
crystals.splice(i, 1);
break;
}
}
}
});
} else {
// Return to original position if not dropped in pot
tween(self, {
x: self.originalX,
y: self.originalY,
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 400,
easing: tween.easeOut
});
}
};
self.update = function () {
// Check if crystal is in pot
var potCenterX = 2048 / 2;
var potCenterY = 2150;
var potOuterRadius = 300;
var potHeightRadius = 250;
self.lastIsInPot = self.isInPot;
// Check if crystal is inside pot area
var normalizedX = (self.x - potCenterX) / (potOuterRadius - 60);
var normalizedY = (self.y - potCenterY) / (potHeightRadius - 60);
var ellipticalDistance = Math.sqrt(normalizedX * normalizedX + normalizedY * normalizedY);
self.isInPot = ellipticalDistance <= 1.0;
// Visual feedback when entering pot
if (!self.lastIsInPot && self.isInPot && self.isDragging) {
// Just entered pot - add glow effect
tween(crystal, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
} else if (self.lastIsInPot && !self.isInPot && self.isDragging) {
// Just left pot - restore original color
tween(crystal, {
tint: crystal.originalTint || 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
}
self.sparkleTimer++;
if (self.sparkleTimer >= 120) {
// Sparkle every 2 seconds
self.sparkleTimer = 0;
tween(crystal, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.7
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(crystal, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
});
}
};
return self;
});
var GridCell = Container.expand(function () {
var self = Container.call(this);
var border = self.attachAsset('gridBorder', {
anchorX: 0.5,
anchorY: 0.5
});
var cell = self.attachAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
var Player = Container.expand(function () {
var self = Container.call(this);
var playerBorder = self.attachAsset('playerBorder', {
anchorX: 0.5,
anchorY: 0.5
});
var playerInner = self.attachAsset('playerInner', {
anchorX: 0.5,
anchorY: 0.5
});
self.gridX = 5; // Center position (0-indexed)
self.gridY = 5; // Center position (0-indexed)
self.isFlashing = false;
self.flashTimer = 0;
self.update = function () {
// Handle flashing animation
self.flashTimer++;
if (self.flashTimer >= 30) {
// Flash every 30 frames (0.5 seconds at 60fps)
self.flashTimer = 0;
self.isFlashing = !self.isFlashing;
playerBorder.alpha = self.isFlashing ? 0.3 : 1.0;
playerInner.alpha = self.isFlashing ? 0.3 : 1.0;
}
};
self.moveToGridPosition = function () {
var cellSize = 164;
var gridStartX = 2048 / 2 - 11 * cellSize / 2 + cellSize / 2;
var gridStartY = 2732 / 2 - 11 * cellSize / 2 + cellSize / 2;
self.x = gridStartX + self.gridX * cellSize;
self.y = gridStartY + self.gridY * cellSize;
};
return self;
});
var Pot = Container.expand(function () {
var self = Container.call(this);
var potBase = self.attachAsset('pot', {
anchorX: 0.5,
anchorY: 0.5
});
var water = self.attachAsset('water', {
anchorX: 0.5,
anchorY: 0.5,
width: 680,
// Match rim width (700 - 20 for border effect)
height: 120 // Much thinner to fit only in rim area
});
water.y = -200; // Position at same level as rim
water.alpha = 0.8; // Make water semi-transparent
var rim = self.attachAsset('potRim', {
anchorX: 0.5,
anchorY: 0.5
});
rim.y = -200;
return self;
});
var Shelf = Container.expand(function () {
var self = Container.call(this);
var shelfBoard = self.attachAsset('shelf', {
anchorX: 0.5,
anchorY: 0.5
});
var leftSupport = self.attachAsset('shelfSupport', {
anchorX: 0.5,
anchorY: 0
});
leftSupport.x = -700;
leftSupport.y = 40;
var rightSupport = self.attachAsset('shelfSupport', {
anchorX: 0.5,
anchorY: 0
});
rightSupport.x = 700;
rightSupport.y = 40;
return self;
});
var Spoon = Container.expand(function () {
var self = Container.call(this);
var handle = self.attachAsset('spoonHandle', {
anchorX: 0.5,
anchorY: 1.0
});
var bowl = self.attachAsset('spoonBowl', {
anchorX: 0.5,
anchorY: 0.5
});
bowl.y = -360;
self.isDragging = false;
self.isInPot = false;
self.lastIsInPot = false;
self.isCollidingWithPot = false;
self.lastIsCollidingWithPot = false;
self.isStirring = false;
self.stirAngle = 0;
self.originalBowlTint = bowl.tint;
self.down = function (x, y, obj) {
self.isDragging = true;
draggedSpoon = self;
// Flip spoon upside down when dragging starts
tween(self, {
rotation: Math.PI
}, {
duration: 200,
easing: tween.easeOut
});
};
self.up = function (x, y, obj) {
self.isDragging = false;
self.isStirring = false;
draggedSpoon = null;
// Flip spoon back to normal when drag ends
tween(self, {
rotation: 0
}, {
duration: 200,
easing: tween.easeOut
});
};
self.update = function () {
// Check if spoon bowl is in pot
var potCenterX = 2048 / 2;
var potCenterY = 2150;
var bowlWorldX = self.x;
var bowlWorldY = self.y - 360; // Bowl offset
var handleWorldX = self.x;
var handleWorldY = self.y;
// Update collision detection state
self.lastIsCollidingWithPot = self.isCollidingWithPot;
// Collision detection - check both spoon bowl and handle against pot outline
var potOuterRadius = 300; // Pot ellipse outer boundary (width/2 = 600/2)
var potHeightRadius = 250; // Pot ellipse height boundary (height/2 = 500/2)
var potLineThickness = 10; // Thickness of the pot outline
// Check bowl collision with pot outline
var bowlNormalizedX = (bowlWorldX - potCenterX) / potOuterRadius;
var bowlNormalizedY = (bowlWorldY - potCenterY) / potHeightRadius;
var bowlEllipticalDistance = Math.sqrt(bowlNormalizedX * bowlNormalizedX + bowlNormalizedY * bowlNormalizedY);
// Account for bowl size (60 pixel radius = 120/2)
var bowlRadiusFactor = 60 / Math.min(potOuterRadius, potHeightRadius);
var bowlInnerBoundary = Math.max(0, 1.0 - potLineThickness / Math.min(potOuterRadius, potHeightRadius) - bowlRadiusFactor);
var bowlOuterBoundary = 1.0 + bowlRadiusFactor;
var bowlCollidingWithPot = bowlEllipticalDistance >= bowlInnerBoundary && bowlEllipticalDistance <= bowlOuterBoundary;
// Check handle collision with pot outline (sample points along handle)
var handleCollidingWithPot = false;
for (var i = 0; i <= 10; i++) {
var handleSampleY = handleWorldY - i * 36; // Sample along 360 pixel handle length
var handleNormalizedX = (handleWorldX - potCenterX) / potOuterRadius;
var handleNormalizedY = (handleSampleY - potCenterY) / potHeightRadius;
var handleEllipticalDistance = Math.sqrt(handleNormalizedX * handleNormalizedX + handleNormalizedY * handleNormalizedY);
// Account for handle width (20 pixel radius = 40/2)
var handleRadiusFactor = 20 / Math.min(potOuterRadius, potHeightRadius);
var handleInnerBoundary = Math.max(0, 1.0 - potLineThickness / Math.min(potOuterRadius, potHeightRadius) - handleRadiusFactor);
var handleOuterBoundary = 1.0 + handleRadiusFactor;
if (handleEllipticalDistance >= handleInnerBoundary && handleEllipticalDistance <= handleOuterBoundary) {
handleCollidingWithPot = true;
break;
}
}
self.isCollidingWithPot = bowlCollidingWithPot || handleCollidingWithPot;
// Update pot state - check if bowl is inside the pot ellipse
self.lastIsInPot = self.isInPot;
var inPotNormalizedX = (bowlWorldX - potCenterX) / (potOuterRadius - 60); // Account for bowl size
var inPotNormalizedY = (bowlWorldY - potCenterY) / (potHeightRadius - 60); // Account for bowl size
var inPotEllipticalDistance = Math.sqrt(inPotNormalizedX * inPotNormalizedX + inPotNormalizedY * inPotNormalizedY);
self.isInPot = inPotEllipticalDistance <= 1.0; // Inside pot for stirring
// Handle collision feedback
if (!self.lastIsCollidingWithPot && self.isCollidingWithPot) {
// Just started colliding with pot edge
tween(self, {
scaleX: 0.95,
scaleY: 0.95
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 150,
easing: tween.easeIn
});
}
});
}
// Visual feedback when entering/leaving pot
if (!self.lastIsInPot && self.isInPot) {
// Just entered pot - tint spoon darker
tween(bowl, {
tint: 0x4A2C17
}, {
duration: 300
});
tween(handle, {
tint: 0x4A2C17
}, {
duration: 300
});
} else if (self.lastIsInPot && !self.isInPot) {
// Just left pot - restore original color
tween(bowl, {
tint: 0x654321
}, {
duration: 300
});
tween(handle, {
tint: 0x654321
}, {
duration: 300
});
}
// Handle depth when in pot
if (self.isInPot) {
// Make bowl appear to dip into pot using elliptical distance
var depthFactor = Math.max(0, 1 - inPotEllipticalDistance);
bowl.y = -360 + depthFactor * 60; // Bowl sinks up to 60 pixels
// Adjust handle visibility based on depth
handle.alpha = Math.max(0.3, 1 - depthFactor * 0.7);
} else {
// Restore normal position when outside pot
bowl.y = -360;
handle.alpha = 1.0;
}
};
return self;
});
var Table = Container.expand(function () {
var self = Container.call(this);
var tabletop = self.attachAsset('table', {
anchorX: 0.5,
anchorY: 0.5
});
var leg1 = self.attachAsset('tableLeg1', {
anchorX: 0.5,
anchorY: 0
});
leg1.x = -500;
leg1.y = 40;
var leg2 = self.attachAsset('tableLeg2', {
anchorX: 0.5,
anchorY: 0
});
leg2.x = 500;
leg2.y = 40;
var leg3 = self.attachAsset('tableLeg3', {
anchorX: 0.5,
anchorY: 0
});
leg3.x = -500;
leg3.y = 40;
var leg4 = self.attachAsset('tableLeg4', {
anchorX: 0.5,
anchorY: 0
});
leg4.x = 500;
leg4.y = 40;
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x111111
});
/****
* Game Code
****/
var gridSize = 11;
var cellSize = 164;
var gridCells = [];
var player;
// Game state management
var gridScreen;
var cursorScreen;
// Crystal movement tracking
var crystalsInPot = [];
var availableMovements = [];
// Create screen containers positioned side by side
gridScreen = new Container();
cursorScreen = new Container();
// Position grid screen on the right half aligned with alchemy lab screen
gridScreen.x = 512; // Shift right by quarter screen width to match cursor screen
gridScreen.scaleX = 0.55; // Scale up to make grid bigger but not too big
gridScreen.scaleY = 0.55;
// Position cursor screen on the right half
cursorScreen.x = 512; // Shift right by quarter screen width
cursorScreen.y = 1366; // Move down to touch bottom of screen
cursorScreen.scaleX = 0.5; // Scale down to fit in half screen
cursorScreen.scaleY = 0.5;
game.addChild(gridScreen);
game.addChild(cursorScreen);
// Create the grid
var gridStartX = 2048 / 2 - gridSize * cellSize / 2 + cellSize / 2;
var gridStartY = 2732 / 2 - gridSize * cellSize / 2 + cellSize / 2;
for (var row = 0; row < gridSize; row++) {
gridCells[row] = [];
for (var col = 0; col < gridSize; col++) {
var cell = new GridCell();
cell.x = gridStartX + col * cellSize;
cell.y = gridStartY + row * cellSize;
gridCells[row][col] = cell;
gridScreen.addChild(cell);
}
}
// Add text labels to specific grid cells
var voidLabel = new Text2('Void', {
size: 40,
fill: 0xFFFFFF
});
voidLabel.anchor.set(0.5, 0.5);
voidLabel.x = gridStartX + 5 * cellSize; // Center column (5)
voidLabel.y = gridStartY + 5 * cellSize; // Center row (5)
gridScreen.addChild(voidLabel);
var earthLabel = new Text2('Earth', {
size: 40,
fill: 0xFFFFFF
});
earthLabel.anchor.set(0.5, 0.5);
earthLabel.x = gridStartX + 3 * cellSize; // 2 squares left from center (5-2=3)
earthLabel.y = gridStartY + 7 * cellSize; // 2 squares down from center (5+2=7)
gridScreen.addChild(earthLabel);
// Create player
player = gridScreen.addChild(new Player());
player.moveToGridPosition();
// Input handling for WASD keys
var keyStates = {
w: false,
a: false,
s: false,
d: false
};
var lastKeyStates = {
w: false,
a: false,
s: false,
d: false
};
// Simulate keyboard input through touch controls
var controlButtons = [];
var buttonSize = 120;
var buttonSpacing = 140;
// Create virtual WASD buttons
var wButton = gridScreen.addChild(LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
width: buttonSize,
height: buttonSize
}));
wButton.x = 2048 + 200;
wButton.y = 1366 - 400;
var aButton = gridScreen.addChild(LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
width: buttonSize,
height: buttonSize
}));
aButton.x = 2048 + 60;
aButton.y = 1366 - 260;
var sButton = gridScreen.addChild(LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
width: buttonSize,
height: buttonSize
}));
sButton.x = 2048 + 200;
sButton.y = 1366 - 260;
var dButton = gridScreen.addChild(LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
width: buttonSize,
height: buttonSize
}));
dButton.x = 2048 + 340;
dButton.y = 1366 - 260;
// Add labels to buttons
var wLabel = new Text2('W', {
size: 60,
fill: 0xFFFFFF
});
wLabel.anchor.set(0.5, 0.5);
wLabel.x = wButton.x;
wLabel.y = wButton.y;
gridScreen.addChild(wLabel);
// Add movement status display
var movementStatusText = new Text2('No movements available\nPlace crystals in pot!', {
size: 40,
fill: 0xFFFFFF
});
movementStatusText.anchor.set(0.5, 0.5);
movementStatusText.x = 300;
movementStatusText.y = 300;
gridScreen.addChild(movementStatusText);
var aLabel = new Text2('A', {
size: 60,
fill: 0xFFFFFF
});
aLabel.anchor.set(0.5, 0.5);
aLabel.x = aButton.x;
aLabel.y = aButton.y;
gridScreen.addChild(aLabel);
var sLabel = new Text2('S', {
size: 60,
fill: 0xFFFFFF
});
sLabel.anchor.set(0.5, 0.5);
sLabel.x = sButton.x;
sLabel.y = sButton.y;
gridScreen.addChild(sLabel);
var dLabel = new Text2('D', {
size: 60,
fill: 0xFFFFFF
});
dLabel.anchor.set(0.5, 0.5);
dLabel.x = dButton.x;
dLabel.y = dButton.y;
gridScreen.addChild(dLabel);
// Button event handlers
wButton.down = function () {
keyStates.w = true;
};
wButton.up = function () {
keyStates.w = false;
};
aButton.down = function () {
keyStates.a = true;
};
aButton.up = function () {
keyStates.a = false;
};
sButton.down = function () {
keyStates.s = true;
};
sButton.up = function () {
keyStates.s = false;
};
dButton.down = function () {
keyStates.d = true;
};
dButton.up = function () {
keyStates.d = false;
};
// Create white background for cursor screen
var whiteBackground = cursorScreen.addChild(LK.getAsset('gridCell', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: 2732
}));
whiteBackground.tint = 0xFFFFFF;
whiteBackground.x = 0;
whiteBackground.y = 0;
// Create cursor screen content
var cursorTitle = new Text2('Alchemy Lab', {
size: 80,
fill: 0x000000
});
cursorTitle.anchor.set(0.5, 0.5);
cursorTitle.x = 2048 / 2;
cursorTitle.y = 400;
cursorScreen.addChild(cursorTitle);
// Create table
var table = cursorScreen.addChild(new Table());
table.x = 2048 / 2;
table.y = 2400;
// Create pot
var pot = cursorScreen.addChild(new Pot());
pot.x = 2048 / 2;
pot.y = 2150;
// Create spoon
var spoon = cursorScreen.addChild(new Spoon());
spoon.x = 2048 / 2 + 300;
spoon.y = 2360; // Position on table surface (table.y - table.height/2 = 2400 - 40)
var draggedSpoon = null;
var draggedCrystal = null;
// Create shelves above the table
var topShelf = cursorScreen.addChild(new Shelf());
topShelf.x = 2048 / 2;
topShelf.y = 1200; // Much higher up, away from pot
var bottomShelf = cursorScreen.addChild(new Shelf());
bottomShelf.x = 2048 / 2;
bottomShelf.y = 1400; // Higher up, maintaining spacing from top shelf
// Create crystals for top shelf
var crystalColors = [0x8B4513, 0xff0000, 0x008080, 0x800080, 0x4B0082, 0x654321]; // brown, red, teal, purple, indigo, dark brown
var crystals = [];
// Top shelf crystals
for (var i = 0; i < 6; i++) {
var crystal = cursorScreen.addChild(new Crystal());
crystal.x = topShelf.x - 500 + i * 200;
crystal.y = topShelf.y - 60;
var crystalColor = crystalColors[i % crystalColors.length];
crystal.children[0].tint = crystalColor;
crystal.children[0].originalTint = crystalColor;
crystal.originalX = crystal.x;
crystal.originalY = crystal.y;
crystals.push(crystal);
}
// Bottom shelf crystals
for (var i = 0; i < 6; i++) {
var crystal = cursorScreen.addChild(new Crystal());
crystal.x = bottomShelf.x - 500 + i * 200;
crystal.y = bottomShelf.y - 60;
var crystalColor = crystalColors[(i + 3) % crystalColors.length];
crystal.children[0].tint = crystalColor;
crystal.children[0].originalTint = crystalColor;
crystal.originalX = crystal.x;
crystal.originalY = crystal.y;
crystals.push(crystal);
}
// Create title for dual screen view
var dualScreenTitle = new Text2('Grid & Kitchen View', {
size: 60,
fill: 0xFFFFFF
});
dualScreenTitle.anchor.set(0.5, 0.5);
dualScreenTitle.x = 2048 / 2;
dualScreenTitle.y = 150;
game.addChild(dualScreenTitle);
// Cursor screen interaction
cursorScreen.move = function (x, y, obj) {
// Handle crystal dragging - convert coordinates for scaled/positioned screen
var localPos = cursorScreen.toLocal({
x: x,
y: y
});
if (draggedCrystal) {
draggedCrystal.x = localPos.x;
draggedCrystal.y = localPos.y;
}
if (draggedSpoon) {
var potCenterX = 2048 / 2;
var potCenterY = 2150;
var potOuterRadius = 300; // Pot ellipse outer boundary (width/2 = 600/2)
var potHeightRadius = 250; // Pot ellipse height boundary (height/2 = 500/2)
var potWallThickness = 20; // Thickness of pot wall for collision
// Position spoon hanging from cursor when dragging (bowl at cursor position)
var proposedX = x;
var proposedY = y + 360; // Offset so bowl hangs at cursor position
// Function to check if spoon outline intersects with pot wall
var checkSpoonWallCollision = function checkSpoonWallCollision(spoonX, spoonY) {
// Check bowl collision with pot wall
var bowlX = spoonX;
var bowlY = spoonY - 360;
var bowlNormalizedX = (bowlX - potCenterX) / potOuterRadius;
var bowlNormalizedY = (bowlY - potCenterY) / potHeightRadius;
var bowlEllipticalDistance = Math.sqrt(bowlNormalizedX * bowlNormalizedX + bowlNormalizedY * bowlNormalizedY);
// Bowl radius factor for collision detection
var bowlRadius = 60; // Half of bowl width (120/2)
var bowlRadiusFactor = bowlRadius / Math.min(potOuterRadius, potHeightRadius);
// Define pot wall boundaries - spoon can't pass through this zone
var wallInnerBoundary = 1.0 - potWallThickness / Math.min(potOuterRadius, potHeightRadius);
var wallOuterBoundary = 1.0 + potWallThickness / Math.min(potOuterRadius, potHeightRadius);
// Check if bowl would intersect with pot wall
var bowlCollision = bowlEllipticalDistance + bowlRadiusFactor >= wallInnerBoundary && bowlEllipticalDistance - bowlRadiusFactor <= wallOuterBoundary;
if (bowlCollision) {
return true;
}
// Check handle collision with pot wall (sample multiple points along handle)
for (var i = 0; i <= 10; i++) {
var handleSampleY = spoonY - i * 36; // Sample along 360 pixel handle length
var handleNormalizedX = (spoonX - potCenterX) / potOuterRadius;
var handleNormalizedY = (handleSampleY - potCenterY) / potHeightRadius;
var handleEllipticalDistance = Math.sqrt(handleNormalizedX * handleNormalizedX + handleNormalizedY * handleNormalizedY);
// Handle radius factor for collision detection
var handleRadius = 20; // Half of handle width (40/2)
var handleRadiusFactor = handleRadius / Math.min(potOuterRadius, potHeightRadius);
// Check if this handle segment would intersect with pot wall
var handleCollision = handleEllipticalDistance + handleRadiusFactor >= wallInnerBoundary && handleEllipticalDistance - handleRadiusFactor <= wallOuterBoundary;
if (handleCollision) {
return true;
}
}
return false;
};
// Check current position and proposed new position
var currentlyCollidingWithWall = checkSpoonWallCollision(draggedSpoon.x, draggedSpoon.y);
var wouldCollideWithWall = checkSpoonWallCollision(proposedX, proposedY);
// Collision resolution logic
if (!currentlyCollidingWithWall && wouldCollideWithWall) {
// Moving from free space into pot wall - block the movement
return; // Don't update spoon position
} else if (currentlyCollidingWithWall && wouldCollideWithWall) {
// Currently stuck in wall and trying to move to another wall position
// Try to push spoon away from pot center to resolve collision
var dirX = proposedX - potCenterX;
var dirY = proposedY - 360 - potCenterY; // Use bowl position for direction calculation
var dirLength = Math.sqrt(dirX * dirX + dirY * dirY);
if (dirLength > 0) {
// Normalize direction vector
dirX /= dirLength;
dirY /= dirLength;
// Push spoon to safe distance outside pot wall
var safeDistance = potOuterRadius + 80; // Add safety margin
proposedX = potCenterX + dirX * safeDistance;
proposedY = potCenterY + dirY * safeDistance + 360; // Add back bowl offset
}
}
// Update spoon position with validated coordinates
draggedSpoon.x = proposedX;
draggedSpoon.y = proposedY;
// Enhanced stirring mechanics - only when spoon is in pot
// Manual stirring when in pot and moving
if (draggedSpoon.isInPot && draggedSpoon.isDragging) {
// Calculate angle for stirring effect but keep the flipped orientation
var angle = Math.atan2(y - potCenterY, x - potCenterX);
draggedSpoon.rotation = Math.PI + angle + Math.PI / 4;
// Create stirring particles effect using tween
if (LK.ticks % 10 === 0) {
// Every 10 frames
// Animate pot slightly to show stirring effect
tween(pot, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(pot, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeIn
});
}
});
// Create water ripple effect during stirring
if (LK.ticks % 15 === 0) {
// Get water element from pot (second child after potBase)
var potWater = pot.children[1];
if (potWater) {
tween(potWater, {
scaleX: 1.1,
scaleY: 0.95
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(potWater, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.easeIn
});
}
});
}
}
}
}
}
};
// Initialize screen visibility - both screens always visible
gridScreen.visible = true;
cursorScreen.visible = true;
function movePlayer(direction) {
// Check if we have any available movements
if (availableMovements.length === 0) {
return; // No movement allowed without crystals in pot
}
var newX = player.gridX;
var newY = player.gridY;
var movementUsed = null;
// Try to find a movement that matches the direction
for (var i = 0; i < availableMovements.length; i++) {
var movement = availableMovements[i];
var tempX = player.gridX;
var tempY = player.gridY;
switch (direction) {
case 'w':
// Up movement
if (movement.upSteps) {
tempY = Math.max(0, player.gridY - movement.upSteps);
if (movement.leftSteps) {
tempX = Math.max(0, player.gridX - movement.leftSteps);
} else if (movement.rightSteps) {
tempX = Math.min(gridSize - 1, player.gridX + movement.rightSteps);
}
movementUsed = i;
}
break;
case 's':
// Down movement
if (movement.downSteps) {
tempY = Math.min(gridSize - 1, player.gridY + movement.downSteps);
if (movement.leftSteps) {
tempX = Math.max(0, player.gridX - movement.leftSteps);
} else if (movement.rightSteps) {
tempX = Math.min(gridSize - 1, player.gridX + movement.rightSteps);
}
movementUsed = i;
}
break;
case 'a':
// Left movement
if (movement.leftSteps && !movement.upSteps && !movement.downSteps) {
tempX = Math.max(0, player.gridX - movement.leftSteps);
movementUsed = i;
}
break;
case 'd':
// Right movement
if (movement.rightSteps && !movement.upSteps && !movement.downSteps) {
tempX = Math.min(gridSize - 1, player.gridX + movement.rightSteps);
movementUsed = i;
}
break;
}
// If we found a valid movement, apply it
if (movementUsed !== null && (tempX !== player.gridX || tempY !== player.gridY)) {
newX = tempX;
newY = tempY;
// Remove the used movement
availableMovements.splice(movementUsed, 1);
break;
}
}
// Only move if position changed
if (newX !== player.gridX || newY !== player.gridY) {
player.gridX = newX;
player.gridY = newY;
player.moveToGridPosition();
}
}
game.update = function () {
// Update movement status display
var statusText = '';
if (availableMovements.length === 0) {
statusText = 'No movements available\nPlace crystals in pot!';
} else {
statusText = 'Available movements: ' + availableMovements.length + '\n';
for (var i = 0; i < availableMovements.length; i++) {
var mov = availableMovements[i];
statusText += mov.type + ' crystal: ';
if (mov.upSteps) statusText += mov.upSteps + ' up ';
if (mov.downSteps) statusText += mov.downSteps + ' down ';
if (mov.leftSteps) statusText += mov.leftSteps + ' left ';
if (mov.rightSteps) statusText += mov.rightSteps + ' right ';
statusText += '\n';
}
}
movementStatusText.setText(statusText);
// Handle grid controls
// Check for key press transitions (just pressed)
if (!lastKeyStates.w && keyStates.w) {
movePlayer('w');
}
if (!lastKeyStates.a && keyStates.a) {
movePlayer('a');
}
if (!lastKeyStates.s && keyStates.s) {
movePlayer('s');
}
if (!lastKeyStates.d && keyStates.d) {
movePlayer('d');
}
// Update last key states
lastKeyStates.w = keyStates.w;
lastKeyStates.a = keyStates.a;
lastKeyStates.s = keyStates.s;
lastKeyStates.d = keyStates.d;
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Crystal = Container.expand(function () {
var self = Container.call(this);
var crystal = self.attachAsset('crystal', {
anchorX: 0.5,
anchorY: 0.5
});
crystal.rotation = Math.PI / 4; // 45 degrees to make diamond shape
// Create larger invisible hitbox for better touch interaction
var hitbox = self.attachAsset('crystal', {
anchorX: 0.5,
anchorY: 0.5,
width: 180,
height: 180
});
hitbox.alpha = 0; // Make invisible
hitbox.interactive = true; // Ensure it can receive touch events
self.sparkleTimer = 0;
self.isDragging = false;
self.originalX = 0;
self.originalY = 0;
self.isInPot = false;
self.lastIsInPot = false;
self.down = function (x, y, obj) {
self.isDragging = true;
draggedCrystal = self;
self.originalX = self.x;
self.originalY = self.y;
// Bring crystal to front and scale it up slightly
tween(self, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.9
}, {
duration: 200,
easing: tween.easeOut
});
// Add visual feedback on touch
tween(crystal, {
tint: 0xFFFFFF
}, {
duration: 100,
easing: tween.easeOut
});
};
self.up = function (x, y, obj) {
self.isDragging = false;
draggedCrystal = null;
// Restore original tint when releasing
tween(crystal, {
tint: crystal.originalTint || 0xFFFFFF
}, {
duration: 100,
easing: tween.easeOut
});
// Check if crystal is dropped in pot
if (self.isInPot) {
// Get crystal color from tint
var crystalColor = self.children[0].tint;
// Add to crystals in pot tracking
crystalsInPot.push(crystalColor);
// Update available movements based on crystal color
if (crystalColor === 0x8B4513) {
// Brown crystal
availableMovements.push({
type: 'brown',
downSteps: 2,
leftSteps: 1
});
} else if (crystalColor === 0xff0000) {
// Red crystal
availableMovements.push({
type: 'red',
upSteps: 1,
rightSteps: 3
});
} else if (crystalColor === 0x008080) {
// Teal crystal
availableMovements.push({
type: 'teal',
leftSteps: 2,
upSteps: 2
});
} else if (crystalColor === 0x800080) {
// Purple crystal
availableMovements.push({
type: 'purple',
rightSteps: 1,
downSteps: 1
});
}
// Crystal successfully added to pot - destroy it with effect
tween(self, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
// Remove from crystals array
for (var i = crystals.length - 1; i >= 0; i--) {
if (crystals[i] === self) {
crystals.splice(i, 1);
break;
}
}
}
});
} else {
// Return to original position if not dropped in pot
tween(self, {
x: self.originalX,
y: self.originalY,
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 400,
easing: tween.easeOut
});
}
};
self.update = function () {
// Check if crystal is in pot
var potCenterX = 2048 / 2;
var potCenterY = 2150;
var potOuterRadius = 300;
var potHeightRadius = 250;
self.lastIsInPot = self.isInPot;
// Check if crystal is inside pot area
var normalizedX = (self.x - potCenterX) / (potOuterRadius - 60);
var normalizedY = (self.y - potCenterY) / (potHeightRadius - 60);
var ellipticalDistance = Math.sqrt(normalizedX * normalizedX + normalizedY * normalizedY);
self.isInPot = ellipticalDistance <= 1.0;
// Visual feedback when entering pot
if (!self.lastIsInPot && self.isInPot && self.isDragging) {
// Just entered pot - add glow effect
tween(crystal, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
} else if (self.lastIsInPot && !self.isInPot && self.isDragging) {
// Just left pot - restore original color
tween(crystal, {
tint: crystal.originalTint || 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
}
self.sparkleTimer++;
if (self.sparkleTimer >= 120) {
// Sparkle every 2 seconds
self.sparkleTimer = 0;
tween(crystal, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.7
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(crystal, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
});
}
};
return self;
});
var GridCell = Container.expand(function () {
var self = Container.call(this);
var border = self.attachAsset('gridBorder', {
anchorX: 0.5,
anchorY: 0.5
});
var cell = self.attachAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
var Player = Container.expand(function () {
var self = Container.call(this);
var playerBorder = self.attachAsset('playerBorder', {
anchorX: 0.5,
anchorY: 0.5
});
var playerInner = self.attachAsset('playerInner', {
anchorX: 0.5,
anchorY: 0.5
});
self.gridX = 5; // Center position (0-indexed)
self.gridY = 5; // Center position (0-indexed)
self.isFlashing = false;
self.flashTimer = 0;
self.update = function () {
// Handle flashing animation
self.flashTimer++;
if (self.flashTimer >= 30) {
// Flash every 30 frames (0.5 seconds at 60fps)
self.flashTimer = 0;
self.isFlashing = !self.isFlashing;
playerBorder.alpha = self.isFlashing ? 0.3 : 1.0;
playerInner.alpha = self.isFlashing ? 0.3 : 1.0;
}
};
self.moveToGridPosition = function () {
var cellSize = 164;
var gridStartX = 2048 / 2 - 11 * cellSize / 2 + cellSize / 2;
var gridStartY = 2732 / 2 - 11 * cellSize / 2 + cellSize / 2;
self.x = gridStartX + self.gridX * cellSize;
self.y = gridStartY + self.gridY * cellSize;
};
return self;
});
var Pot = Container.expand(function () {
var self = Container.call(this);
var potBase = self.attachAsset('pot', {
anchorX: 0.5,
anchorY: 0.5
});
var water = self.attachAsset('water', {
anchorX: 0.5,
anchorY: 0.5,
width: 680,
// Match rim width (700 - 20 for border effect)
height: 120 // Much thinner to fit only in rim area
});
water.y = -200; // Position at same level as rim
water.alpha = 0.8; // Make water semi-transparent
var rim = self.attachAsset('potRim', {
anchorX: 0.5,
anchorY: 0.5
});
rim.y = -200;
return self;
});
var Shelf = Container.expand(function () {
var self = Container.call(this);
var shelfBoard = self.attachAsset('shelf', {
anchorX: 0.5,
anchorY: 0.5
});
var leftSupport = self.attachAsset('shelfSupport', {
anchorX: 0.5,
anchorY: 0
});
leftSupport.x = -700;
leftSupport.y = 40;
var rightSupport = self.attachAsset('shelfSupport', {
anchorX: 0.5,
anchorY: 0
});
rightSupport.x = 700;
rightSupport.y = 40;
return self;
});
var Spoon = Container.expand(function () {
var self = Container.call(this);
var handle = self.attachAsset('spoonHandle', {
anchorX: 0.5,
anchorY: 1.0
});
var bowl = self.attachAsset('spoonBowl', {
anchorX: 0.5,
anchorY: 0.5
});
bowl.y = -360;
self.isDragging = false;
self.isInPot = false;
self.lastIsInPot = false;
self.isCollidingWithPot = false;
self.lastIsCollidingWithPot = false;
self.isStirring = false;
self.stirAngle = 0;
self.originalBowlTint = bowl.tint;
self.down = function (x, y, obj) {
self.isDragging = true;
draggedSpoon = self;
// Flip spoon upside down when dragging starts
tween(self, {
rotation: Math.PI
}, {
duration: 200,
easing: tween.easeOut
});
};
self.up = function (x, y, obj) {
self.isDragging = false;
self.isStirring = false;
draggedSpoon = null;
// Flip spoon back to normal when drag ends
tween(self, {
rotation: 0
}, {
duration: 200,
easing: tween.easeOut
});
};
self.update = function () {
// Check if spoon bowl is in pot
var potCenterX = 2048 / 2;
var potCenterY = 2150;
var bowlWorldX = self.x;
var bowlWorldY = self.y - 360; // Bowl offset
var handleWorldX = self.x;
var handleWorldY = self.y;
// Update collision detection state
self.lastIsCollidingWithPot = self.isCollidingWithPot;
// Collision detection - check both spoon bowl and handle against pot outline
var potOuterRadius = 300; // Pot ellipse outer boundary (width/2 = 600/2)
var potHeightRadius = 250; // Pot ellipse height boundary (height/2 = 500/2)
var potLineThickness = 10; // Thickness of the pot outline
// Check bowl collision with pot outline
var bowlNormalizedX = (bowlWorldX - potCenterX) / potOuterRadius;
var bowlNormalizedY = (bowlWorldY - potCenterY) / potHeightRadius;
var bowlEllipticalDistance = Math.sqrt(bowlNormalizedX * bowlNormalizedX + bowlNormalizedY * bowlNormalizedY);
// Account for bowl size (60 pixel radius = 120/2)
var bowlRadiusFactor = 60 / Math.min(potOuterRadius, potHeightRadius);
var bowlInnerBoundary = Math.max(0, 1.0 - potLineThickness / Math.min(potOuterRadius, potHeightRadius) - bowlRadiusFactor);
var bowlOuterBoundary = 1.0 + bowlRadiusFactor;
var bowlCollidingWithPot = bowlEllipticalDistance >= bowlInnerBoundary && bowlEllipticalDistance <= bowlOuterBoundary;
// Check handle collision with pot outline (sample points along handle)
var handleCollidingWithPot = false;
for (var i = 0; i <= 10; i++) {
var handleSampleY = handleWorldY - i * 36; // Sample along 360 pixel handle length
var handleNormalizedX = (handleWorldX - potCenterX) / potOuterRadius;
var handleNormalizedY = (handleSampleY - potCenterY) / potHeightRadius;
var handleEllipticalDistance = Math.sqrt(handleNormalizedX * handleNormalizedX + handleNormalizedY * handleNormalizedY);
// Account for handle width (20 pixel radius = 40/2)
var handleRadiusFactor = 20 / Math.min(potOuterRadius, potHeightRadius);
var handleInnerBoundary = Math.max(0, 1.0 - potLineThickness / Math.min(potOuterRadius, potHeightRadius) - handleRadiusFactor);
var handleOuterBoundary = 1.0 + handleRadiusFactor;
if (handleEllipticalDistance >= handleInnerBoundary && handleEllipticalDistance <= handleOuterBoundary) {
handleCollidingWithPot = true;
break;
}
}
self.isCollidingWithPot = bowlCollidingWithPot || handleCollidingWithPot;
// Update pot state - check if bowl is inside the pot ellipse
self.lastIsInPot = self.isInPot;
var inPotNormalizedX = (bowlWorldX - potCenterX) / (potOuterRadius - 60); // Account for bowl size
var inPotNormalizedY = (bowlWorldY - potCenterY) / (potHeightRadius - 60); // Account for bowl size
var inPotEllipticalDistance = Math.sqrt(inPotNormalizedX * inPotNormalizedX + inPotNormalizedY * inPotNormalizedY);
self.isInPot = inPotEllipticalDistance <= 1.0; // Inside pot for stirring
// Handle collision feedback
if (!self.lastIsCollidingWithPot && self.isCollidingWithPot) {
// Just started colliding with pot edge
tween(self, {
scaleX: 0.95,
scaleY: 0.95
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 150,
easing: tween.easeIn
});
}
});
}
// Visual feedback when entering/leaving pot
if (!self.lastIsInPot && self.isInPot) {
// Just entered pot - tint spoon darker
tween(bowl, {
tint: 0x4A2C17
}, {
duration: 300
});
tween(handle, {
tint: 0x4A2C17
}, {
duration: 300
});
} else if (self.lastIsInPot && !self.isInPot) {
// Just left pot - restore original color
tween(bowl, {
tint: 0x654321
}, {
duration: 300
});
tween(handle, {
tint: 0x654321
}, {
duration: 300
});
}
// Handle depth when in pot
if (self.isInPot) {
// Make bowl appear to dip into pot using elliptical distance
var depthFactor = Math.max(0, 1 - inPotEllipticalDistance);
bowl.y = -360 + depthFactor * 60; // Bowl sinks up to 60 pixels
// Adjust handle visibility based on depth
handle.alpha = Math.max(0.3, 1 - depthFactor * 0.7);
} else {
// Restore normal position when outside pot
bowl.y = -360;
handle.alpha = 1.0;
}
};
return self;
});
var Table = Container.expand(function () {
var self = Container.call(this);
var tabletop = self.attachAsset('table', {
anchorX: 0.5,
anchorY: 0.5
});
var leg1 = self.attachAsset('tableLeg1', {
anchorX: 0.5,
anchorY: 0
});
leg1.x = -500;
leg1.y = 40;
var leg2 = self.attachAsset('tableLeg2', {
anchorX: 0.5,
anchorY: 0
});
leg2.x = 500;
leg2.y = 40;
var leg3 = self.attachAsset('tableLeg3', {
anchorX: 0.5,
anchorY: 0
});
leg3.x = -500;
leg3.y = 40;
var leg4 = self.attachAsset('tableLeg4', {
anchorX: 0.5,
anchorY: 0
});
leg4.x = 500;
leg4.y = 40;
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x111111
});
/****
* Game Code
****/
var gridSize = 11;
var cellSize = 164;
var gridCells = [];
var player;
// Game state management
var gridScreen;
var cursorScreen;
// Crystal movement tracking
var crystalsInPot = [];
var availableMovements = [];
// Create screen containers positioned side by side
gridScreen = new Container();
cursorScreen = new Container();
// Position grid screen on the right half aligned with alchemy lab screen
gridScreen.x = 512; // Shift right by quarter screen width to match cursor screen
gridScreen.scaleX = 0.55; // Scale up to make grid bigger but not too big
gridScreen.scaleY = 0.55;
// Position cursor screen on the right half
cursorScreen.x = 512; // Shift right by quarter screen width
cursorScreen.y = 1366; // Move down to touch bottom of screen
cursorScreen.scaleX = 0.5; // Scale down to fit in half screen
cursorScreen.scaleY = 0.5;
game.addChild(gridScreen);
game.addChild(cursorScreen);
// Create the grid
var gridStartX = 2048 / 2 - gridSize * cellSize / 2 + cellSize / 2;
var gridStartY = 2732 / 2 - gridSize * cellSize / 2 + cellSize / 2;
for (var row = 0; row < gridSize; row++) {
gridCells[row] = [];
for (var col = 0; col < gridSize; col++) {
var cell = new GridCell();
cell.x = gridStartX + col * cellSize;
cell.y = gridStartY + row * cellSize;
gridCells[row][col] = cell;
gridScreen.addChild(cell);
}
}
// Add text labels to specific grid cells
var voidLabel = new Text2('Void', {
size: 40,
fill: 0xFFFFFF
});
voidLabel.anchor.set(0.5, 0.5);
voidLabel.x = gridStartX + 5 * cellSize; // Center column (5)
voidLabel.y = gridStartY + 5 * cellSize; // Center row (5)
gridScreen.addChild(voidLabel);
var earthLabel = new Text2('Earth', {
size: 40,
fill: 0xFFFFFF
});
earthLabel.anchor.set(0.5, 0.5);
earthLabel.x = gridStartX + 3 * cellSize; // 2 squares left from center (5-2=3)
earthLabel.y = gridStartY + 7 * cellSize; // 2 squares down from center (5+2=7)
gridScreen.addChild(earthLabel);
// Create player
player = gridScreen.addChild(new Player());
player.moveToGridPosition();
// Input handling for WASD keys
var keyStates = {
w: false,
a: false,
s: false,
d: false
};
var lastKeyStates = {
w: false,
a: false,
s: false,
d: false
};
// Simulate keyboard input through touch controls
var controlButtons = [];
var buttonSize = 120;
var buttonSpacing = 140;
// Create virtual WASD buttons
var wButton = gridScreen.addChild(LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
width: buttonSize,
height: buttonSize
}));
wButton.x = 2048 + 200;
wButton.y = 1366 - 400;
var aButton = gridScreen.addChild(LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
width: buttonSize,
height: buttonSize
}));
aButton.x = 2048 + 60;
aButton.y = 1366 - 260;
var sButton = gridScreen.addChild(LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
width: buttonSize,
height: buttonSize
}));
sButton.x = 2048 + 200;
sButton.y = 1366 - 260;
var dButton = gridScreen.addChild(LK.getAsset('gridCell', {
anchorX: 0.5,
anchorY: 0.5,
width: buttonSize,
height: buttonSize
}));
dButton.x = 2048 + 340;
dButton.y = 1366 - 260;
// Add labels to buttons
var wLabel = new Text2('W', {
size: 60,
fill: 0xFFFFFF
});
wLabel.anchor.set(0.5, 0.5);
wLabel.x = wButton.x;
wLabel.y = wButton.y;
gridScreen.addChild(wLabel);
// Add movement status display
var movementStatusText = new Text2('No movements available\nPlace crystals in pot!', {
size: 40,
fill: 0xFFFFFF
});
movementStatusText.anchor.set(0.5, 0.5);
movementStatusText.x = 300;
movementStatusText.y = 300;
gridScreen.addChild(movementStatusText);
var aLabel = new Text2('A', {
size: 60,
fill: 0xFFFFFF
});
aLabel.anchor.set(0.5, 0.5);
aLabel.x = aButton.x;
aLabel.y = aButton.y;
gridScreen.addChild(aLabel);
var sLabel = new Text2('S', {
size: 60,
fill: 0xFFFFFF
});
sLabel.anchor.set(0.5, 0.5);
sLabel.x = sButton.x;
sLabel.y = sButton.y;
gridScreen.addChild(sLabel);
var dLabel = new Text2('D', {
size: 60,
fill: 0xFFFFFF
});
dLabel.anchor.set(0.5, 0.5);
dLabel.x = dButton.x;
dLabel.y = dButton.y;
gridScreen.addChild(dLabel);
// Button event handlers
wButton.down = function () {
keyStates.w = true;
};
wButton.up = function () {
keyStates.w = false;
};
aButton.down = function () {
keyStates.a = true;
};
aButton.up = function () {
keyStates.a = false;
};
sButton.down = function () {
keyStates.s = true;
};
sButton.up = function () {
keyStates.s = false;
};
dButton.down = function () {
keyStates.d = true;
};
dButton.up = function () {
keyStates.d = false;
};
// Create white background for cursor screen
var whiteBackground = cursorScreen.addChild(LK.getAsset('gridCell', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: 2732
}));
whiteBackground.tint = 0xFFFFFF;
whiteBackground.x = 0;
whiteBackground.y = 0;
// Create cursor screen content
var cursorTitle = new Text2('Alchemy Lab', {
size: 80,
fill: 0x000000
});
cursorTitle.anchor.set(0.5, 0.5);
cursorTitle.x = 2048 / 2;
cursorTitle.y = 400;
cursorScreen.addChild(cursorTitle);
// Create table
var table = cursorScreen.addChild(new Table());
table.x = 2048 / 2;
table.y = 2400;
// Create pot
var pot = cursorScreen.addChild(new Pot());
pot.x = 2048 / 2;
pot.y = 2150;
// Create spoon
var spoon = cursorScreen.addChild(new Spoon());
spoon.x = 2048 / 2 + 300;
spoon.y = 2360; // Position on table surface (table.y - table.height/2 = 2400 - 40)
var draggedSpoon = null;
var draggedCrystal = null;
// Create shelves above the table
var topShelf = cursorScreen.addChild(new Shelf());
topShelf.x = 2048 / 2;
topShelf.y = 1200; // Much higher up, away from pot
var bottomShelf = cursorScreen.addChild(new Shelf());
bottomShelf.x = 2048 / 2;
bottomShelf.y = 1400; // Higher up, maintaining spacing from top shelf
// Create crystals for top shelf
var crystalColors = [0x8B4513, 0xff0000, 0x008080, 0x800080, 0x4B0082, 0x654321]; // brown, red, teal, purple, indigo, dark brown
var crystals = [];
// Top shelf crystals
for (var i = 0; i < 6; i++) {
var crystal = cursorScreen.addChild(new Crystal());
crystal.x = topShelf.x - 500 + i * 200;
crystal.y = topShelf.y - 60;
var crystalColor = crystalColors[i % crystalColors.length];
crystal.children[0].tint = crystalColor;
crystal.children[0].originalTint = crystalColor;
crystal.originalX = crystal.x;
crystal.originalY = crystal.y;
crystals.push(crystal);
}
// Bottom shelf crystals
for (var i = 0; i < 6; i++) {
var crystal = cursorScreen.addChild(new Crystal());
crystal.x = bottomShelf.x - 500 + i * 200;
crystal.y = bottomShelf.y - 60;
var crystalColor = crystalColors[(i + 3) % crystalColors.length];
crystal.children[0].tint = crystalColor;
crystal.children[0].originalTint = crystalColor;
crystal.originalX = crystal.x;
crystal.originalY = crystal.y;
crystals.push(crystal);
}
// Create title for dual screen view
var dualScreenTitle = new Text2('Grid & Kitchen View', {
size: 60,
fill: 0xFFFFFF
});
dualScreenTitle.anchor.set(0.5, 0.5);
dualScreenTitle.x = 2048 / 2;
dualScreenTitle.y = 150;
game.addChild(dualScreenTitle);
// Cursor screen interaction
cursorScreen.move = function (x, y, obj) {
// Handle crystal dragging - convert coordinates for scaled/positioned screen
var localPos = cursorScreen.toLocal({
x: x,
y: y
});
if (draggedCrystal) {
draggedCrystal.x = localPos.x;
draggedCrystal.y = localPos.y;
}
if (draggedSpoon) {
var potCenterX = 2048 / 2;
var potCenterY = 2150;
var potOuterRadius = 300; // Pot ellipse outer boundary (width/2 = 600/2)
var potHeightRadius = 250; // Pot ellipse height boundary (height/2 = 500/2)
var potWallThickness = 20; // Thickness of pot wall for collision
// Position spoon hanging from cursor when dragging (bowl at cursor position)
var proposedX = x;
var proposedY = y + 360; // Offset so bowl hangs at cursor position
// Function to check if spoon outline intersects with pot wall
var checkSpoonWallCollision = function checkSpoonWallCollision(spoonX, spoonY) {
// Check bowl collision with pot wall
var bowlX = spoonX;
var bowlY = spoonY - 360;
var bowlNormalizedX = (bowlX - potCenterX) / potOuterRadius;
var bowlNormalizedY = (bowlY - potCenterY) / potHeightRadius;
var bowlEllipticalDistance = Math.sqrt(bowlNormalizedX * bowlNormalizedX + bowlNormalizedY * bowlNormalizedY);
// Bowl radius factor for collision detection
var bowlRadius = 60; // Half of bowl width (120/2)
var bowlRadiusFactor = bowlRadius / Math.min(potOuterRadius, potHeightRadius);
// Define pot wall boundaries - spoon can't pass through this zone
var wallInnerBoundary = 1.0 - potWallThickness / Math.min(potOuterRadius, potHeightRadius);
var wallOuterBoundary = 1.0 + potWallThickness / Math.min(potOuterRadius, potHeightRadius);
// Check if bowl would intersect with pot wall
var bowlCollision = bowlEllipticalDistance + bowlRadiusFactor >= wallInnerBoundary && bowlEllipticalDistance - bowlRadiusFactor <= wallOuterBoundary;
if (bowlCollision) {
return true;
}
// Check handle collision with pot wall (sample multiple points along handle)
for (var i = 0; i <= 10; i++) {
var handleSampleY = spoonY - i * 36; // Sample along 360 pixel handle length
var handleNormalizedX = (spoonX - potCenterX) / potOuterRadius;
var handleNormalizedY = (handleSampleY - potCenterY) / potHeightRadius;
var handleEllipticalDistance = Math.sqrt(handleNormalizedX * handleNormalizedX + handleNormalizedY * handleNormalizedY);
// Handle radius factor for collision detection
var handleRadius = 20; // Half of handle width (40/2)
var handleRadiusFactor = handleRadius / Math.min(potOuterRadius, potHeightRadius);
// Check if this handle segment would intersect with pot wall
var handleCollision = handleEllipticalDistance + handleRadiusFactor >= wallInnerBoundary && handleEllipticalDistance - handleRadiusFactor <= wallOuterBoundary;
if (handleCollision) {
return true;
}
}
return false;
};
// Check current position and proposed new position
var currentlyCollidingWithWall = checkSpoonWallCollision(draggedSpoon.x, draggedSpoon.y);
var wouldCollideWithWall = checkSpoonWallCollision(proposedX, proposedY);
// Collision resolution logic
if (!currentlyCollidingWithWall && wouldCollideWithWall) {
// Moving from free space into pot wall - block the movement
return; // Don't update spoon position
} else if (currentlyCollidingWithWall && wouldCollideWithWall) {
// Currently stuck in wall and trying to move to another wall position
// Try to push spoon away from pot center to resolve collision
var dirX = proposedX - potCenterX;
var dirY = proposedY - 360 - potCenterY; // Use bowl position for direction calculation
var dirLength = Math.sqrt(dirX * dirX + dirY * dirY);
if (dirLength > 0) {
// Normalize direction vector
dirX /= dirLength;
dirY /= dirLength;
// Push spoon to safe distance outside pot wall
var safeDistance = potOuterRadius + 80; // Add safety margin
proposedX = potCenterX + dirX * safeDistance;
proposedY = potCenterY + dirY * safeDistance + 360; // Add back bowl offset
}
}
// Update spoon position with validated coordinates
draggedSpoon.x = proposedX;
draggedSpoon.y = proposedY;
// Enhanced stirring mechanics - only when spoon is in pot
// Manual stirring when in pot and moving
if (draggedSpoon.isInPot && draggedSpoon.isDragging) {
// Calculate angle for stirring effect but keep the flipped orientation
var angle = Math.atan2(y - potCenterY, x - potCenterX);
draggedSpoon.rotation = Math.PI + angle + Math.PI / 4;
// Create stirring particles effect using tween
if (LK.ticks % 10 === 0) {
// Every 10 frames
// Animate pot slightly to show stirring effect
tween(pot, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(pot, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeIn
});
}
});
// Create water ripple effect during stirring
if (LK.ticks % 15 === 0) {
// Get water element from pot (second child after potBase)
var potWater = pot.children[1];
if (potWater) {
tween(potWater, {
scaleX: 1.1,
scaleY: 0.95
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(potWater, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.easeIn
});
}
});
}
}
}
}
}
};
// Initialize screen visibility - both screens always visible
gridScreen.visible = true;
cursorScreen.visible = true;
function movePlayer(direction) {
// Check if we have any available movements
if (availableMovements.length === 0) {
return; // No movement allowed without crystals in pot
}
var newX = player.gridX;
var newY = player.gridY;
var movementUsed = null;
// Try to find a movement that matches the direction
for (var i = 0; i < availableMovements.length; i++) {
var movement = availableMovements[i];
var tempX = player.gridX;
var tempY = player.gridY;
switch (direction) {
case 'w':
// Up movement
if (movement.upSteps) {
tempY = Math.max(0, player.gridY - movement.upSteps);
if (movement.leftSteps) {
tempX = Math.max(0, player.gridX - movement.leftSteps);
} else if (movement.rightSteps) {
tempX = Math.min(gridSize - 1, player.gridX + movement.rightSteps);
}
movementUsed = i;
}
break;
case 's':
// Down movement
if (movement.downSteps) {
tempY = Math.min(gridSize - 1, player.gridY + movement.downSteps);
if (movement.leftSteps) {
tempX = Math.max(0, player.gridX - movement.leftSteps);
} else if (movement.rightSteps) {
tempX = Math.min(gridSize - 1, player.gridX + movement.rightSteps);
}
movementUsed = i;
}
break;
case 'a':
// Left movement
if (movement.leftSteps && !movement.upSteps && !movement.downSteps) {
tempX = Math.max(0, player.gridX - movement.leftSteps);
movementUsed = i;
}
break;
case 'd':
// Right movement
if (movement.rightSteps && !movement.upSteps && !movement.downSteps) {
tempX = Math.min(gridSize - 1, player.gridX + movement.rightSteps);
movementUsed = i;
}
break;
}
// If we found a valid movement, apply it
if (movementUsed !== null && (tempX !== player.gridX || tempY !== player.gridY)) {
newX = tempX;
newY = tempY;
// Remove the used movement
availableMovements.splice(movementUsed, 1);
break;
}
}
// Only move if position changed
if (newX !== player.gridX || newY !== player.gridY) {
player.gridX = newX;
player.gridY = newY;
player.moveToGridPosition();
}
}
game.update = function () {
// Update movement status display
var statusText = '';
if (availableMovements.length === 0) {
statusText = 'No movements available\nPlace crystals in pot!';
} else {
statusText = 'Available movements: ' + availableMovements.length + '\n';
for (var i = 0; i < availableMovements.length; i++) {
var mov = availableMovements[i];
statusText += mov.type + ' crystal: ';
if (mov.upSteps) statusText += mov.upSteps + ' up ';
if (mov.downSteps) statusText += mov.downSteps + ' down ';
if (mov.leftSteps) statusText += mov.leftSteps + ' left ';
if (mov.rightSteps) statusText += mov.rightSteps + ' right ';
statusText += '\n';
}
}
movementStatusText.setText(statusText);
// Handle grid controls
// Check for key press transitions (just pressed)
if (!lastKeyStates.w && keyStates.w) {
movePlayer('w');
}
if (!lastKeyStates.a && keyStates.a) {
movePlayer('a');
}
if (!lastKeyStates.s && keyStates.s) {
movePlayer('s');
}
if (!lastKeyStates.d && keyStates.d) {
movePlayer('d');
}
// Update last key states
lastKeyStates.w = keyStates.w;
lastKeyStates.a = keyStates.a;
lastKeyStates.s = keyStates.s;
lastKeyStates.d = keyStates.d;
};