/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { particles: [], settings: { gravity: 0.5, friction: 0.98, elasticity: 0.8 } }); /**** * Classes ****/ var MaterialButton = Container.expand(function (materialType, color) { var self = Container.call(this); self.materialType = materialType; self.isSelected = false; // Create button background self.background = self.attachAsset('particle', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.5, scaleY: 2.5 }); self.background.tint = 0x333333; // Create material preview self.preview = self.attachAsset('particle', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.8, scaleY: 1.8 }); self.preview.tint = color; // Apply special effects for different materials switch (materialType) { case 'water': self.preview.alpha = 0.8; break; case 'gas': self.preview.alpha = 0.6; break; case 'fire': // Create flickering effect LK.setInterval(function () { if (self.preview) { var flicker = 0.9 + Math.random() * 0.2; self.preview.alpha = flicker; } }, 100); break; } // Event handlers self.down = function (x, y, obj) { selectMaterial(self.materialType); }; self.setSelected = function (selected) { self.isSelected = selected; if (selected) { tween(self.background, { tint: 0x3498db }, { duration: 200 }); tween(self, { scaleX: 1.1, scaleY: 1.1 }, { duration: 200 }); } else { tween(self.background, { tint: 0x333333 }, { duration: 200 }); tween(self, { scaleX: 1.0, scaleY: 1.0 }, { duration: 200 }); } }; return self; }); var Particle = Container.expand(function (type, size, color) { var self = Container.call(this); // Default properties if not specified self.type = type || 'solid'; self.size = size || 20; self.color = color || 0xFFFFFF; // Physical properties self.vx = 0; self.vy = 0; self.mass = self.size / 10; self.elasticity = 0.8; self.friction = 0.98; self.isFixed = false; self.lifespan = -1; // -1 means infinite self.age = 0; // Visual representation self.visual = self.attachAsset('particle', { anchorX: 0.5, anchorY: 0.5, scaleX: self.size / 20, scaleY: self.size / 20 }); self.visual.tint = self.color; // Type-specific properties switch (self.type) { case 'water': self.elasticity = 0.3; self.visual.tint = 0x3498db; self.visual.alpha = 0.8; break; case 'fire': self.lifespan = 120; self.elasticity = 0.1; self.visual.tint = 0xe74c3c; break; case 'gas': self.mass = self.size / 20; self.elasticity = 0.1; self.visual.tint = 0xecf0f1; self.visual.alpha = 0.6; break; case 'bouncy': self.elasticity = 1.2; self.visual.tint = 0x2ecc71; break; case 'heavy': self.mass = self.size / 5; self.visual.tint = 0x7f8c8d; break; } // Event handlers self.down = function (x, y, obj) { // Start dragging this particle self.isDragging = true; self.dragOffsetX = x; self.dragOffsetY = y; // If we're in fixed mode, toggle fixed state if (currentTool === 'fix') { self.isFixed = !self.isFixed; self.visual.alpha = self.isFixed ? 0.5 : 1.0; LK.getSound('pop').play(); } // If we're in erase mode, mark for deletion if (currentTool === 'erase') { self.toDelete = true; LK.getSound('pop').play(); } }; self.up = function (x, y, obj) { self.isDragging = false; // Apply velocity from drag if we just released if (currentTool === 'move' && !self.isFixed) { self.vx = (x - self.dragOffsetX) / 10; self.vy = (y - self.dragOffsetY) / 10; } }; self.update = function () { // Skip physics for fixed particles if (self.isFixed) { return; } // Age particles with lifespans if (self.lifespan > 0) { self.age++; if (self.age >= self.lifespan) { self.toDelete = true; return; } // Fade out as we age self.visual.alpha = 1 - self.age / self.lifespan; } // Skip physics for particles being dragged if (self.isDragging && currentTool === 'move') { return; } // Apply gravity based on mass and global gravity setting if (currentTool !== 'antigravity' && !isAntigravityActive) { self.vy += settings.gravity * self.mass; } else { self.vy -= settings.gravity * self.mass / 2; } // Apply type-specific behavior if (self.type === 'gas') { self.vy -= settings.gravity * 1.5; // Gases rise self.vx += (Math.random() - 0.5) * 0.3; // Random horizontal drift } else if (self.type === 'fire') { self.vy -= settings.gravity * 1.2; // Fire rises self.vx += (Math.random() - 0.5) * 0.5; // More horizontal drift } // Apply velocity self.x += self.vx; self.y += self.vy; // Apply friction self.vx *= settings.friction; self.vy *= settings.friction; // Boundary collisions handleBoundaryCollisions(self); }; return self; }); var ToolButton = Container.expand(function (toolName, iconColor, iconShape) { var self = Container.call(this); self.toolName = toolName; self.isSelected = false; // Create button background self.background = self.attachAsset('particle', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.5, scaleY: 2.5 }); self.background.tint = 0x333333; // Create icon self.icon = self.attachAsset(iconShape || 'particle', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.8, scaleY: 1.8 }); self.icon.tint = iconColor || 0xFFFFFF; // Event handlers self.down = function (x, y, obj) { selectTool(self.toolName); }; self.setSelected = function (selected) { self.isSelected = selected; if (selected) { tween(self.background, { tint: 0x3498db }, { duration: 200 }); tween(self, { scaleX: 1.1, scaleY: 1.1 }, { duration: 200 }); } else { tween(self.background, { tint: 0x333333 }, { duration: 200 }); tween(self, { scaleX: 1.0, scaleY: 1.0 }, { duration: 200 }); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // Game settings var settings = storage.settings || { gravity: 0.5, friction: 0.98, elasticity: 0.8 }; // Game state var particles = []; var currentTool = 'create'; var currentMaterial = 'solid'; var toolButtons = []; var materialButtons = []; var isMouseDown = false; var lastCreationTime = 0; var creationDelay = 5; // Ticks between particle creation var isAntigravityActive = false; var dragTarget = null; var particleSize = 20; // Create UI elements function createUI() { // Create toolbar var toolbar = new Container(); toolbar.x = 2048 / 2; toolbar.y = 2732 - 100; game.addChild(toolbar); // Create tool buttons var toolDefs = [{ name: 'create', color: 0xFFFFFF, shape: 'particle' }, { name: 'move', color: 0x3498db, shape: 'particle' }, { name: 'erase', color: 0xe74c3c, shape: 'eraser' }, { name: 'fix', color: 0xf1c40f, shape: 'particle' }, { name: 'antigravity', color: 0x00FFFF, shape: 'gravityIndicator' }]; var buttonSpacing = 100; for (var i = 0; i < toolDefs.length; i++) { var def = toolDefs[i]; var button = new ToolButton(def.name, def.color, def.shape); button.x = (i - Math.floor(toolDefs.length / 2)) * buttonSpacing; toolbar.addChild(button); toolButtons.push(button); } // Create material selector var materials = new Container(); materials.x = 100; materials.y = 2732 / 2; game.addChild(materials); // Create material buttons var materialDefs = [{ type: 'solid', color: 0xFFFFFF }, { type: 'water', color: 0x3498db }, { type: 'bouncy', color: 0x2ecc71 }, { type: 'heavy', color: 0x7f8c8d }, { type: 'fire', color: 0xe74c3c }, { type: 'gas', color: 0xecf0f1 }]; for (var j = 0; j < materialDefs.length; j++) { var matDef = materialDefs[j]; var matButton = new MaterialButton(matDef.type, matDef.color); matButton.y = (j - Math.floor(materialDefs.length / 2)) * buttonSpacing; materials.addChild(matButton); materialButtons.push(matButton); } // Create size controls var sizeControls = new Container(); sizeControls.x = 2048 - 100; sizeControls.y = 2732 / 2; game.addChild(sizeControls); var sizeUpButton = new ToolButton('sizeUp', 0xFFFFFF, 'particle'); sizeUpButton.y = -50; sizeUpButton.down = function () { particleSize = Math.min(particleSize + 5, 50); }; sizeControls.addChild(sizeUpButton); var sizeDownButton = new ToolButton('sizeDown', 0xFFFFFF, 'particle'); sizeDownButton.y = 50; sizeDownButton.down = function () { particleSize = Math.max(particleSize - 5, 10); }; sizeControls.addChild(sizeDownButton); // Set initially selected tool and material selectTool('create'); selectMaterial('solid'); // Create info text var infoText = new Text2('Universal Sandbox', { size: 40, fill: 0xFFFFFF }); infoText.anchor.set(0.5, 0); LK.gui.top.addChild(infoText); } // Tool selection logic function selectTool(toolName) { currentTool = toolName; // Update button appearances for (var i = 0; i < toolButtons.length; i++) { var button = toolButtons[i]; button.setSelected(button.toolName === currentTool); } // Special case for antigravity tool isAntigravityActive = currentTool === 'antigravity'; } // Material selection logic function selectMaterial(materialType) { currentMaterial = materialType; // Update button appearances for (var i = 0; i < materialButtons.length; i++) { var button = materialButtons[i]; button.setSelected(button.materialType === currentMaterial); } } // Particle creation logic function createParticle(x, y) { // Get appropriate color based on material var color; switch (currentMaterial) { case 'water': color = 0x3498db; break; case 'fire': color = 0xe74c3c; break; case 'gas': color = 0xecf0f1; break; case 'bouncy': color = 0x2ecc71; break; case 'heavy': color = 0x7f8c8d; break; default: color = 0xFFFFFF; // solid } var particle = new Particle(currentMaterial, particleSize, color); particle.x = x; particle.y = y; // Add some randomness to initial state particle.vx = (Math.random() - 0.5) * 2; particle.vy = (Math.random() - 0.5) * 2; // Add to game game.addChild(particle); particles.push(particle); // Play creation sound if (particles.length % 5 === 0) { LK.getSound('pop').play(); } return particle; } // Boundary collision handling function handleBoundaryCollisions(particle) { var padding = particle.size / 2; // Left and right edges if (particle.x < padding) { particle.x = padding; particle.vx = -particle.vx * particle.elasticity; if (Math.abs(particle.vx) > 1) { LK.getSound('bounce').play(); } } else if (particle.x > 2048 - padding) { particle.x = 2048 - padding; particle.vx = -particle.vx * particle.elasticity; if (Math.abs(particle.vx) > 1) { LK.getSound('bounce').play(); } } // Top and bottom edges if (particle.y < padding) { particle.y = padding; particle.vy = -particle.vy * particle.elasticity; if (Math.abs(particle.vy) > 1) { LK.getSound('bounce').play(); } } else if (particle.y > 2732 - 200 - padding) { // Bottom minus toolbar height particle.y = 2732 - 200 - padding; particle.vy = -particle.vy * particle.elasticity; if (Math.abs(particle.vy) > 1) { LK.getSound('bounce').play(); } } } // Particle collision detection and response function handleParticleCollisions() { for (var i = 0; i < particles.length; i++) { var p1 = particles[i]; if (p1.toDelete || p1.isFixed) { continue; } for (var j = i + 1; j < particles.length; j++) { var p2 = particles[j]; if (p2.toDelete || p2.isFixed) { continue; } // Calculate distance var dx = p2.x - p1.x; var dy = p2.y - p1.y; var distance = Math.sqrt(dx * dx + dy * dy); var minDistance = (p1.size + p2.size) / 2; // Collision detected if (distance < minDistance) { // Normalize collision vector var nx = dx / distance; var ny = dy / distance; // Calculate relative velocity var vx = p1.vx - p2.vx; var vy = p1.vy - p2.vy; // Calculate relative velocity in terms of the normal direction var velocityAlongNormal = vx * nx + vy * ny; // Do not resolve if velocities are separating if (velocityAlongNormal > 0) { continue; } // Calculate combined elasticity var combinedElasticity = (p1.elasticity + p2.elasticity) / 2; // Calculate impulse scalar var impulseScalar = -(1 + combinedElasticity) * velocityAlongNormal; impulseScalar /= 1 / p1.mass + 1 / p2.mass; // Apply impulse var impulseX = impulseScalar * nx; var impulseY = impulseScalar * ny; p1.vx -= impulseX / p1.mass; p1.vy -= impulseY / p1.mass; p2.vx += impulseX / p2.mass; p2.vy += impulseY / p2.mass; // Move particles apart to prevent sticking var overlap = minDistance - distance; var correctionX = overlap * nx * 0.5; var correctionY = overlap * ny * 0.5; p1.x -= correctionX; p1.y -= correctionY; p2.x += correctionX; p2.y += correctionY; // Play sound for significant collisions if (Math.abs(velocityAlongNormal) > 5) { if (p1.type === 'water' || p2.type === 'water') { LK.getSound('splash').play(); } else { LK.getSound('bounce').play(); } } } } } } // Event handlers game.down = function (x, y, obj) { isMouseDown = true; lastCreationTime = 0; // Reset to create immediately // Handle tool-specific behavior if (currentTool === 'create') { createParticle(x, y); } else if (currentTool === 'move') { // Find particle under cursor for (var i = particles.length - 1; i >= 0; i--) { var p = particles[i]; var dx = p.x - x; var dy = p.y - y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < p.size) { dragTarget = p; dragTarget.isDragging = true; dragTarget.dragOffsetX = x; dragTarget.dragOffsetY = y; break; } } } }; game.up = function (x, y, obj) { isMouseDown = false; // Release drag target if (dragTarget) { dragTarget.up(x, y, obj); dragTarget = null; } }; game.move = function (x, y, obj) { // Handle continuous creation if (isMouseDown && currentTool === 'create' && LK.ticks - lastCreationTime > creationDelay) { createParticle(x, y); lastCreationTime = LK.ticks; } // Handle eraser tool if (isMouseDown && currentTool === 'erase') { for (var i = particles.length - 1; i >= 0; i--) { var p = particles[i]; var dx = p.x - x; var dy = p.y - y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < p.size + 25) { p.toDelete = true; } } } // Update drag target position if (dragTarget) { dragTarget.x = x; dragTarget.y = y; } }; // Initialize game createUI(); // Start background music LK.playMusic('ambient', { loop: true, fade: { start: 0, end: 0.6, duration: 1000 } }); // Set background color game.setBackgroundColor(0x1a1a2e); // Main game loop game.update = function () { // Clean up particles marked for deletion for (var i = particles.length - 1; i >= 0; i--) { if (particles[i].toDelete) { particles[i].destroy(); particles.splice(i, 1); } } // Handle particle collisions handleParticleCollisions(); // Special effects for different materials for (var j = 0; j < particles.length; j++) { var p = particles[j]; // Fire effects if (p.type === 'fire' && Math.random() < 0.1) { // Create smoke particles occasionally if (Math.random() < 0.05) { var smoke = new Particle('gas', p.size * 0.8, 0x7f8c8d); smoke.x = p.x + (Math.random() - 0.5) * 10; smoke.y = p.y - 10; smoke.lifespan = 60; game.addChild(smoke); particles.push(smoke); } } // Water effects if (p.type === 'water' && Math.random() < 0.02) { // Make water particles occasionally merge or split if (Math.random() < 0.5 && p.size > 15) { // Split var droplet = new Particle('water', p.size * 0.6, 0x3498db); droplet.x = p.x + (Math.random() - 0.5) * 20; droplet.y = p.y + (Math.random() - 0.5) * 20; droplet.vx = p.vx + (Math.random() - 0.5) * 2; droplet.vy = p.vy + (Math.random() - 0.5) * 2; game.addChild(droplet); particles.push(droplet); p.size *= 0.9; p.visual.scaleX = p.size / 20; p.visual.scaleY = p.size / 20; } } } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
particles: [],
settings: {
gravity: 0.5,
friction: 0.98,
elasticity: 0.8
}
});
/****
* Classes
****/
var MaterialButton = Container.expand(function (materialType, color) {
var self = Container.call(this);
self.materialType = materialType;
self.isSelected = false;
// Create button background
self.background = self.attachAsset('particle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.5,
scaleY: 2.5
});
self.background.tint = 0x333333;
// Create material preview
self.preview = self.attachAsset('particle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.8,
scaleY: 1.8
});
self.preview.tint = color;
// Apply special effects for different materials
switch (materialType) {
case 'water':
self.preview.alpha = 0.8;
break;
case 'gas':
self.preview.alpha = 0.6;
break;
case 'fire':
// Create flickering effect
LK.setInterval(function () {
if (self.preview) {
var flicker = 0.9 + Math.random() * 0.2;
self.preview.alpha = flicker;
}
}, 100);
break;
}
// Event handlers
self.down = function (x, y, obj) {
selectMaterial(self.materialType);
};
self.setSelected = function (selected) {
self.isSelected = selected;
if (selected) {
tween(self.background, {
tint: 0x3498db
}, {
duration: 200
});
tween(self, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 200
});
} else {
tween(self.background, {
tint: 0x333333
}, {
duration: 200
});
tween(self, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200
});
}
};
return self;
});
var Particle = Container.expand(function (type, size, color) {
var self = Container.call(this);
// Default properties if not specified
self.type = type || 'solid';
self.size = size || 20;
self.color = color || 0xFFFFFF;
// Physical properties
self.vx = 0;
self.vy = 0;
self.mass = self.size / 10;
self.elasticity = 0.8;
self.friction = 0.98;
self.isFixed = false;
self.lifespan = -1; // -1 means infinite
self.age = 0;
// Visual representation
self.visual = self.attachAsset('particle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: self.size / 20,
scaleY: self.size / 20
});
self.visual.tint = self.color;
// Type-specific properties
switch (self.type) {
case 'water':
self.elasticity = 0.3;
self.visual.tint = 0x3498db;
self.visual.alpha = 0.8;
break;
case 'fire':
self.lifespan = 120;
self.elasticity = 0.1;
self.visual.tint = 0xe74c3c;
break;
case 'gas':
self.mass = self.size / 20;
self.elasticity = 0.1;
self.visual.tint = 0xecf0f1;
self.visual.alpha = 0.6;
break;
case 'bouncy':
self.elasticity = 1.2;
self.visual.tint = 0x2ecc71;
break;
case 'heavy':
self.mass = self.size / 5;
self.visual.tint = 0x7f8c8d;
break;
}
// Event handlers
self.down = function (x, y, obj) {
// Start dragging this particle
self.isDragging = true;
self.dragOffsetX = x;
self.dragOffsetY = y;
// If we're in fixed mode, toggle fixed state
if (currentTool === 'fix') {
self.isFixed = !self.isFixed;
self.visual.alpha = self.isFixed ? 0.5 : 1.0;
LK.getSound('pop').play();
}
// If we're in erase mode, mark for deletion
if (currentTool === 'erase') {
self.toDelete = true;
LK.getSound('pop').play();
}
};
self.up = function (x, y, obj) {
self.isDragging = false;
// Apply velocity from drag if we just released
if (currentTool === 'move' && !self.isFixed) {
self.vx = (x - self.dragOffsetX) / 10;
self.vy = (y - self.dragOffsetY) / 10;
}
};
self.update = function () {
// Skip physics for fixed particles
if (self.isFixed) {
return;
}
// Age particles with lifespans
if (self.lifespan > 0) {
self.age++;
if (self.age >= self.lifespan) {
self.toDelete = true;
return;
}
// Fade out as we age
self.visual.alpha = 1 - self.age / self.lifespan;
}
// Skip physics for particles being dragged
if (self.isDragging && currentTool === 'move') {
return;
}
// Apply gravity based on mass and global gravity setting
if (currentTool !== 'antigravity' && !isAntigravityActive) {
self.vy += settings.gravity * self.mass;
} else {
self.vy -= settings.gravity * self.mass / 2;
}
// Apply type-specific behavior
if (self.type === 'gas') {
self.vy -= settings.gravity * 1.5; // Gases rise
self.vx += (Math.random() - 0.5) * 0.3; // Random horizontal drift
} else if (self.type === 'fire') {
self.vy -= settings.gravity * 1.2; // Fire rises
self.vx += (Math.random() - 0.5) * 0.5; // More horizontal drift
}
// Apply velocity
self.x += self.vx;
self.y += self.vy;
// Apply friction
self.vx *= settings.friction;
self.vy *= settings.friction;
// Boundary collisions
handleBoundaryCollisions(self);
};
return self;
});
var ToolButton = Container.expand(function (toolName, iconColor, iconShape) {
var self = Container.call(this);
self.toolName = toolName;
self.isSelected = false;
// Create button background
self.background = self.attachAsset('particle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.5,
scaleY: 2.5
});
self.background.tint = 0x333333;
// Create icon
self.icon = self.attachAsset(iconShape || 'particle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.8,
scaleY: 1.8
});
self.icon.tint = iconColor || 0xFFFFFF;
// Event handlers
self.down = function (x, y, obj) {
selectTool(self.toolName);
};
self.setSelected = function (selected) {
self.isSelected = selected;
if (selected) {
tween(self.background, {
tint: 0x3498db
}, {
duration: 200
});
tween(self, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 200
});
} else {
tween(self.background, {
tint: 0x333333
}, {
duration: 200
});
tween(self, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200
});
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Game settings
var settings = storage.settings || {
gravity: 0.5,
friction: 0.98,
elasticity: 0.8
};
// Game state
var particles = [];
var currentTool = 'create';
var currentMaterial = 'solid';
var toolButtons = [];
var materialButtons = [];
var isMouseDown = false;
var lastCreationTime = 0;
var creationDelay = 5; // Ticks between particle creation
var isAntigravityActive = false;
var dragTarget = null;
var particleSize = 20;
// Create UI elements
function createUI() {
// Create toolbar
var toolbar = new Container();
toolbar.x = 2048 / 2;
toolbar.y = 2732 - 100;
game.addChild(toolbar);
// Create tool buttons
var toolDefs = [{
name: 'create',
color: 0xFFFFFF,
shape: 'particle'
}, {
name: 'move',
color: 0x3498db,
shape: 'particle'
}, {
name: 'erase',
color: 0xe74c3c,
shape: 'eraser'
}, {
name: 'fix',
color: 0xf1c40f,
shape: 'particle'
}, {
name: 'antigravity',
color: 0x00FFFF,
shape: 'gravityIndicator'
}];
var buttonSpacing = 100;
for (var i = 0; i < toolDefs.length; i++) {
var def = toolDefs[i];
var button = new ToolButton(def.name, def.color, def.shape);
button.x = (i - Math.floor(toolDefs.length / 2)) * buttonSpacing;
toolbar.addChild(button);
toolButtons.push(button);
}
// Create material selector
var materials = new Container();
materials.x = 100;
materials.y = 2732 / 2;
game.addChild(materials);
// Create material buttons
var materialDefs = [{
type: 'solid',
color: 0xFFFFFF
}, {
type: 'water',
color: 0x3498db
}, {
type: 'bouncy',
color: 0x2ecc71
}, {
type: 'heavy',
color: 0x7f8c8d
}, {
type: 'fire',
color: 0xe74c3c
}, {
type: 'gas',
color: 0xecf0f1
}];
for (var j = 0; j < materialDefs.length; j++) {
var matDef = materialDefs[j];
var matButton = new MaterialButton(matDef.type, matDef.color);
matButton.y = (j - Math.floor(materialDefs.length / 2)) * buttonSpacing;
materials.addChild(matButton);
materialButtons.push(matButton);
}
// Create size controls
var sizeControls = new Container();
sizeControls.x = 2048 - 100;
sizeControls.y = 2732 / 2;
game.addChild(sizeControls);
var sizeUpButton = new ToolButton('sizeUp', 0xFFFFFF, 'particle');
sizeUpButton.y = -50;
sizeUpButton.down = function () {
particleSize = Math.min(particleSize + 5, 50);
};
sizeControls.addChild(sizeUpButton);
var sizeDownButton = new ToolButton('sizeDown', 0xFFFFFF, 'particle');
sizeDownButton.y = 50;
sizeDownButton.down = function () {
particleSize = Math.max(particleSize - 5, 10);
};
sizeControls.addChild(sizeDownButton);
// Set initially selected tool and material
selectTool('create');
selectMaterial('solid');
// Create info text
var infoText = new Text2('Universal Sandbox', {
size: 40,
fill: 0xFFFFFF
});
infoText.anchor.set(0.5, 0);
LK.gui.top.addChild(infoText);
}
// Tool selection logic
function selectTool(toolName) {
currentTool = toolName;
// Update button appearances
for (var i = 0; i < toolButtons.length; i++) {
var button = toolButtons[i];
button.setSelected(button.toolName === currentTool);
}
// Special case for antigravity tool
isAntigravityActive = currentTool === 'antigravity';
}
// Material selection logic
function selectMaterial(materialType) {
currentMaterial = materialType;
// Update button appearances
for (var i = 0; i < materialButtons.length; i++) {
var button = materialButtons[i];
button.setSelected(button.materialType === currentMaterial);
}
}
// Particle creation logic
function createParticle(x, y) {
// Get appropriate color based on material
var color;
switch (currentMaterial) {
case 'water':
color = 0x3498db;
break;
case 'fire':
color = 0xe74c3c;
break;
case 'gas':
color = 0xecf0f1;
break;
case 'bouncy':
color = 0x2ecc71;
break;
case 'heavy':
color = 0x7f8c8d;
break;
default:
color = 0xFFFFFF;
// solid
}
var particle = new Particle(currentMaterial, particleSize, color);
particle.x = x;
particle.y = y;
// Add some randomness to initial state
particle.vx = (Math.random() - 0.5) * 2;
particle.vy = (Math.random() - 0.5) * 2;
// Add to game
game.addChild(particle);
particles.push(particle);
// Play creation sound
if (particles.length % 5 === 0) {
LK.getSound('pop').play();
}
return particle;
}
// Boundary collision handling
function handleBoundaryCollisions(particle) {
var padding = particle.size / 2;
// Left and right edges
if (particle.x < padding) {
particle.x = padding;
particle.vx = -particle.vx * particle.elasticity;
if (Math.abs(particle.vx) > 1) {
LK.getSound('bounce').play();
}
} else if (particle.x > 2048 - padding) {
particle.x = 2048 - padding;
particle.vx = -particle.vx * particle.elasticity;
if (Math.abs(particle.vx) > 1) {
LK.getSound('bounce').play();
}
}
// Top and bottom edges
if (particle.y < padding) {
particle.y = padding;
particle.vy = -particle.vy * particle.elasticity;
if (Math.abs(particle.vy) > 1) {
LK.getSound('bounce').play();
}
} else if (particle.y > 2732 - 200 - padding) {
// Bottom minus toolbar height
particle.y = 2732 - 200 - padding;
particle.vy = -particle.vy * particle.elasticity;
if (Math.abs(particle.vy) > 1) {
LK.getSound('bounce').play();
}
}
}
// Particle collision detection and response
function handleParticleCollisions() {
for (var i = 0; i < particles.length; i++) {
var p1 = particles[i];
if (p1.toDelete || p1.isFixed) {
continue;
}
for (var j = i + 1; j < particles.length; j++) {
var p2 = particles[j];
if (p2.toDelete || p2.isFixed) {
continue;
}
// Calculate distance
var dx = p2.x - p1.x;
var dy = p2.y - p1.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var minDistance = (p1.size + p2.size) / 2;
// Collision detected
if (distance < minDistance) {
// Normalize collision vector
var nx = dx / distance;
var ny = dy / distance;
// Calculate relative velocity
var vx = p1.vx - p2.vx;
var vy = p1.vy - p2.vy;
// Calculate relative velocity in terms of the normal direction
var velocityAlongNormal = vx * nx + vy * ny;
// Do not resolve if velocities are separating
if (velocityAlongNormal > 0) {
continue;
}
// Calculate combined elasticity
var combinedElasticity = (p1.elasticity + p2.elasticity) / 2;
// Calculate impulse scalar
var impulseScalar = -(1 + combinedElasticity) * velocityAlongNormal;
impulseScalar /= 1 / p1.mass + 1 / p2.mass;
// Apply impulse
var impulseX = impulseScalar * nx;
var impulseY = impulseScalar * ny;
p1.vx -= impulseX / p1.mass;
p1.vy -= impulseY / p1.mass;
p2.vx += impulseX / p2.mass;
p2.vy += impulseY / p2.mass;
// Move particles apart to prevent sticking
var overlap = minDistance - distance;
var correctionX = overlap * nx * 0.5;
var correctionY = overlap * ny * 0.5;
p1.x -= correctionX;
p1.y -= correctionY;
p2.x += correctionX;
p2.y += correctionY;
// Play sound for significant collisions
if (Math.abs(velocityAlongNormal) > 5) {
if (p1.type === 'water' || p2.type === 'water') {
LK.getSound('splash').play();
} else {
LK.getSound('bounce').play();
}
}
}
}
}
}
// Event handlers
game.down = function (x, y, obj) {
isMouseDown = true;
lastCreationTime = 0; // Reset to create immediately
// Handle tool-specific behavior
if (currentTool === 'create') {
createParticle(x, y);
} else if (currentTool === 'move') {
// Find particle under cursor
for (var i = particles.length - 1; i >= 0; i--) {
var p = particles[i];
var dx = p.x - x;
var dy = p.y - y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < p.size) {
dragTarget = p;
dragTarget.isDragging = true;
dragTarget.dragOffsetX = x;
dragTarget.dragOffsetY = y;
break;
}
}
}
};
game.up = function (x, y, obj) {
isMouseDown = false;
// Release drag target
if (dragTarget) {
dragTarget.up(x, y, obj);
dragTarget = null;
}
};
game.move = function (x, y, obj) {
// Handle continuous creation
if (isMouseDown && currentTool === 'create' && LK.ticks - lastCreationTime > creationDelay) {
createParticle(x, y);
lastCreationTime = LK.ticks;
}
// Handle eraser tool
if (isMouseDown && currentTool === 'erase') {
for (var i = particles.length - 1; i >= 0; i--) {
var p = particles[i];
var dx = p.x - x;
var dy = p.y - y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < p.size + 25) {
p.toDelete = true;
}
}
}
// Update drag target position
if (dragTarget) {
dragTarget.x = x;
dragTarget.y = y;
}
};
// Initialize game
createUI();
// Start background music
LK.playMusic('ambient', {
loop: true,
fade: {
start: 0,
end: 0.6,
duration: 1000
}
});
// Set background color
game.setBackgroundColor(0x1a1a2e);
// Main game loop
game.update = function () {
// Clean up particles marked for deletion
for (var i = particles.length - 1; i >= 0; i--) {
if (particles[i].toDelete) {
particles[i].destroy();
particles.splice(i, 1);
}
}
// Handle particle collisions
handleParticleCollisions();
// Special effects for different materials
for (var j = 0; j < particles.length; j++) {
var p = particles[j];
// Fire effects
if (p.type === 'fire' && Math.random() < 0.1) {
// Create smoke particles occasionally
if (Math.random() < 0.05) {
var smoke = new Particle('gas', p.size * 0.8, 0x7f8c8d);
smoke.x = p.x + (Math.random() - 0.5) * 10;
smoke.y = p.y - 10;
smoke.lifespan = 60;
game.addChild(smoke);
particles.push(smoke);
}
}
// Water effects
if (p.type === 'water' && Math.random() < 0.02) {
// Make water particles occasionally merge or split
if (Math.random() < 0.5 && p.size > 15) {
// Split
var droplet = new Particle('water', p.size * 0.6, 0x3498db);
droplet.x = p.x + (Math.random() - 0.5) * 20;
droplet.y = p.y + (Math.random() - 0.5) * 20;
droplet.vx = p.vx + (Math.random() - 0.5) * 2;
droplet.vy = p.vy + (Math.random() - 0.5) * 2;
game.addChild(droplet);
particles.push(droplet);
p.size *= 0.9;
p.visual.scaleX = p.size / 20;
p.visual.scaleY = p.size / 20;
}
}
}
};