diff --git a/api/progress.php b/api/progress.php
new file mode 100644
index 0000000..cfdbc8b
--- /dev/null
+++ b/api/progress.php
@@ -0,0 +1,15 @@
+prepare('INSERT INTO user_progress (level_id, stars) VALUES (?, ?) ON DUPLICATE KEY UPDATE stars = GREATEST(stars, VALUES(stars))');
+ $stmt->execute([$data['level_id'], $data['stars']]);
+ echo json_encode(['success' => true]);
+ } catch (Exception $e) {
+ echo json_encode(['error' => $e->getMessage()]);
+ }
+} else {
+ echo json_encode(['error' => 'Missing data']);
+}
diff --git a/assets/css/custom.css b/assets/css/custom.css
index 50e0502..c6b0c62 100644
--- a/assets/css/custom.css
+++ b/assets/css/custom.css
@@ -1,302 +1,184 @@
+:root {
+ --wood-bg: #f9d8b8;
+ --wood-line: rgba(139, 69, 19, 0.08);
+ --btn-red: #d35d5d;
+ --btn-border: #4e342e;
+ --font-main: 'Fredoka', sans-serif;
+ --mold-fill: rgba(239, 154, 154, 0.4);
+ --mold-border: #5d4037;
+}
+
body {
- background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
- background-size: 400% 400%;
- animation: gradient 15s ease infinite;
- color: #212529;
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- font-size: 14px;
- margin: 0;
- min-height: 100vh;
+ margin: 0;
+ padding: 0;
+ background-color: #d7ccc8;
+ font-family: var(--font-main);
+ color: #3e2723;
+ overflow: hidden;
+ height: 100vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ user-select: none;
+ -webkit-user-select: none;
+ touch-action: none;
}
-.main-wrapper {
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: 100vh;
- width: 100%;
- padding: 20px;
- box-sizing: border-box;
- position: relative;
- z-index: 1;
+.game-container {
+ width: 100vw;
+ height: 100vh;
+ max-width: 900px;
+ background: var(--wood-bg);
+ position: relative;
+ border: 12px solid #8d6e63;
+ box-shadow: inset 0 0 100px rgba(0,0,0,0.15);
+ /* Wood Board Texture */
+ background-image:
+ linear-gradient(var(--wood-line) 1px, transparent 1px),
+ linear-gradient(90deg, var(--wood-line) 1px, transparent 1px);
+ background-size: 40px 40px;
+ display: flex;
+ flex-direction: column;
}
-@keyframes gradient {
- 0% {
- background-position: 0% 50%;
- }
- 50% {
- background-position: 100% 50%;
- }
- 100% {
- background-position: 0% 50%;
- }
+.game-header {
+ padding: 15px 20px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ z-index: 20;
}
-.chat-container {
- width: 100%;
- max-width: 600px;
- background: rgba(255, 255, 255, 0.85);
- border: 1px solid rgba(255, 255, 255, 0.3);
- border-radius: 20px;
- display: flex;
- flex-direction: column;
- height: 85vh;
- box-shadow: 0 20px 40px rgba(0,0,0,0.2);
- backdrop-filter: blur(15px);
- -webkit-backdrop-filter: blur(15px);
- overflow: hidden;
+.icon-btn {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ border: 2px solid var(--btn-border);
+ background: rgba(255,255,255,0.4);
+ color: var(--btn-border);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 3px 6px rgba(0,0,0,0.1);
+ transition: all 0.1s;
}
-.chat-header {
- padding: 1.5rem;
- border-bottom: 1px solid rgba(0, 0, 0, 0.05);
- background: rgba(255, 255, 255, 0.5);
- font-weight: 700;
- font-size: 1.1rem;
- display: flex;
- justify-content: space-between;
- align-items: center;
+.icon-btn:active {
+ transform: scale(0.92);
+ background: rgba(255,255,255,0.6);
}
-.chat-messages {
- flex: 1;
- overflow-y: auto;
- padding: 1.5rem;
- display: flex;
- flex-direction: column;
- gap: 1.25rem;
+.icon-btn svg {
+ width: 26px;
+ height: 26px;
}
-/* Custom Scrollbar */
-::-webkit-scrollbar {
- width: 6px;
+.header-center {
+ flex: 1;
+ display: flex;
+ justify-content: center;
}
-::-webkit-scrollbar-track {
- background: transparent;
+.stars-display {
+ font-size: 2rem;
+ color: #ffb300;
+ text-shadow: 2px 2px 0 #3e2723;
+ letter-spacing: 4px;
}
-::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.3);
- border-radius: 10px;
+.star-gold { color: #ffb300; }
+.star-outline { color: #d7ccc8; opacity: 0.6; }
+
+.main-stage {
+ flex: 1;
+ position: relative;
+ overflow: hidden;
}
-::-webkit-scrollbar-thumb:hover {
- background: rgba(255, 255, 255, 0.5);
+#game-canvas {
+ width: 100%;
+ height: 100%;
+ cursor: crosshair;
+ touch-action: none;
}
-.message {
- max-width: 85%;
- padding: 0.85rem 1.1rem;
- border-radius: 16px;
- line-height: 1.5;
- font-size: 0.95rem;
- box-shadow: 0 4px 15px rgba(0,0,0,0.05);
- animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+.game-footer {
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 15px;
+ z-index: 20;
}
-@keyframes fadeIn {
- from { opacity: 0; transform: translateY(20px) scale(0.95); }
- to { opacity: 1; transform: translateY(0) scale(1); }
+.drop-trigger {
+ background: var(--btn-red);
+ color: white;
+ border: 3px solid var(--btn-border);
+ padding: 14px 45px;
+ border-radius: 35px;
+ font-family: inherit;
+ font-weight: 700;
+ font-size: 1.3rem;
+ cursor: pointer;
+ box-shadow: 0 5px 0 var(--btn-border);
+ transition: all 0.1s;
+ text-transform: uppercase;
+ letter-spacing: 2px;
}
-.message.visitor {
- align-self: flex-end;
- background: linear-gradient(135deg, #212529 0%, #343a40 100%);
- color: #fff;
- border-bottom-right-radius: 4px;
+.drop-trigger:active {
+ transform: translateY(3px);
+ box-shadow: 0 2px 0 var(--btn-border);
}
-.message.bot {
- align-self: flex-start;
- background: #ffffff;
- color: #212529;
- border-bottom-left-radius: 4px;
+.drop-trigger:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none;
+ box-shadow: 0 5px 0 var(--btn-border);
}
-.chat-input-area {
- padding: 1.25rem;
- background: rgba(255, 255, 255, 0.5);
- border-top: 1px solid rgba(0, 0, 0, 0.05);
+#accuracy-display {
+ font-weight: 700;
+ font-size: 1rem;
+ color: #3e2723;
+ background: rgba(255,255,255,0.5);
+ padding: 5px 15px;
+ border-radius: 20px;
+ border: 1px solid rgba(0,0,0,0.1);
}
-.chat-input-area form {
- display: flex;
- gap: 0.75rem;
+#toast-container {
+ position: fixed;
+ top: 40%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ pointer-events: none;
+ z-index: 1000;
}
-.chat-input-area input {
- flex: 1;
- border: 1px solid rgba(0, 0, 0, 0.1);
- border-radius: 12px;
- padding: 0.75rem 1rem;
- outline: none;
- background: rgba(255, 255, 255, 0.9);
- transition: all 0.3s ease;
+.toast {
+ background: white;
+ padding: 20px 40px;
+ border-radius: 40px;
+ border: 4px solid var(--btn-border);
+ box-shadow: 8px 8px 0 var(--btn-border);
+ font-size: 1.5rem;
+ font-weight: 700;
+ animation: pop-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
-.chat-input-area input:focus {
- border-color: #23a6d5;
- box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
+@keyframes pop-in {
+ 0% { transform: scale(0.4) rotate(-15deg); opacity: 0; }
+ 100% { transform: scale(1) rotate(0deg); opacity: 1; }
}
-.chat-input-area button {
- background: #212529;
- color: #fff;
- border: none;
- padding: 0.75rem 1.5rem;
- border-radius: 12px;
- cursor: pointer;
- font-weight: 600;
- transition: all 0.3s ease;
-}
-
-.chat-input-area button:hover {
- background: #000;
- transform: translateY(-2px);
- box-shadow: 0 5px 15px rgba(0,0,0,0.2);
-}
-
-/* Background Animations */
-.bg-animations {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 0;
- overflow: hidden;
- pointer-events: none;
-}
-
-.blob {
- position: absolute;
- width: 500px;
- height: 500px;
- background: rgba(255, 255, 255, 0.2);
- border-radius: 50%;
- filter: blur(80px);
- animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
-}
-
-.blob-1 {
- top: -10%;
- left: -10%;
- background: rgba(238, 119, 82, 0.4);
-}
-
-.blob-2 {
- bottom: -10%;
- right: -10%;
- background: rgba(35, 166, 213, 0.4);
- animation-delay: -7s;
- width: 600px;
- height: 600px;
-}
-
-.blob-3 {
- top: 40%;
- left: 30%;
- background: rgba(231, 60, 126, 0.3);
- animation-delay: -14s;
- width: 450px;
- height: 450px;
-}
-
-@keyframes move {
- 0% { transform: translate(0, 0) rotate(0deg) scale(1); }
- 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
- 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
- 100% { transform: translate(0, 0) rotate(360deg) scale(1); }
-}
-
-.admin-link {
- font-size: 14px;
- color: #fff;
- text-decoration: none;
- background: rgba(0, 0, 0, 0.2);
- padding: 0.5rem 1rem;
- border-radius: 8px;
- transition: all 0.3s ease;
-}
-
-.admin-link:hover {
- background: rgba(0, 0, 0, 0.4);
- text-decoration: none;
-}
-
-/* Admin Styles */
-.admin-container {
- max-width: 900px;
- margin: 3rem auto;
- padding: 2.5rem;
- background: rgba(255, 255, 255, 0.85);
- backdrop-filter: blur(20px);
- -webkit-backdrop-filter: blur(20px);
- border-radius: 24px;
- box-shadow: 0 20px 50px rgba(0,0,0,0.15);
- border: 1px solid rgba(255, 255, 255, 0.4);
- position: relative;
- z-index: 1;
-}
-
-.admin-container h1 {
- margin-top: 0;
- color: #212529;
- font-weight: 800;
-}
-
-.table {
- width: 100%;
- border-collapse: separate;
- border-spacing: 0 8px;
- margin-top: 1.5rem;
-}
-
-.table th {
- background: transparent;
- border: none;
- padding: 1rem;
- color: #6c757d;
- font-weight: 600;
- text-transform: uppercase;
- font-size: 0.75rem;
- letter-spacing: 1px;
-}
-
-.table td {
- background: #fff;
- padding: 1rem;
- border: none;
-}
-
-.table tr td:first-child { border-radius: 12px 0 0 12px; }
-.table tr td:last-child { border-radius: 0 12px 12px 0; }
-
-.form-group {
- margin-bottom: 1.25rem;
-}
-
-.form-group label {
- display: block;
- margin-bottom: 0.5rem;
- font-weight: 600;
- font-size: 0.9rem;
-}
-
-.form-control {
- width: 100%;
- padding: 0.75rem 1rem;
- border: 1px solid rgba(0, 0, 0, 0.1);
- border-radius: 12px;
- background: #fff;
- transition: all 0.3s ease;
- box-sizing: border-box;
-}
-
-.form-control:focus {
- outline: none;
- border-color: #23a6d5;
- box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
+@media (max-width: 480px) {
+ .game-container { border-width: 6px; }
+ .drop-trigger { padding: 12px 35px; font-size: 1.1rem; }
+ .stars-display { font-size: 1.6rem; }
+ .icon-btn { width: 40px; height: 40px; }
+ .icon-btn svg { width: 22px; height: 22px; }
}
\ No newline at end of file
diff --git a/assets/js/main.js b/assets/js/main.js
index d349598..03fa178 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -1,39 +1,448 @@
-document.addEventListener('DOMContentLoaded', () => {
- const chatForm = document.getElementById('chat-form');
- const chatInput = document.getElementById('chat-input');
- const chatMessages = document.getElementById('chat-messages');
+const { Engine, Render, Runner, World, Bodies, Body, Composite, Vertices, Vector, Events, Common, Mouse, MouseConstraint, Query } = Matter;
- const appendMessage = (text, sender) => {
- const msgDiv = document.createElement('div');
- msgDiv.classList.add('message', sender);
- msgDiv.textContent = text;
- chatMessages.appendChild(msgDiv);
- chatMessages.scrollTop = chatMessages.scrollHeight;
+// Ensure poly-decomp is set up for complex polygons
+if (typeof decomp !== 'undefined') {
+ Common.setDecomp(window.decomp);
+} else {
+ console.error('poly-decomp.js is required for complex polygon splitting.');
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ const canvas = document.getElementById('game-canvas');
+ const btnDrop = document.getElementById('btn-drop');
+ const btnReset = document.getElementById('btn-reset');
+ const btnHint = document.getElementById('btn-hint');
+ const starsPreviewEl = document.getElementById('stars-preview');
+ const accuracyValueEl = document.getElementById('accuracy-value');
+
+ let engine, render, runner, mouseConstraint;
+ let riceBodies = [];
+ let currentCuts = 0;
+ let isDropped = false;
+ let isDragging = false;
+ let lastMousePos = null;
+ let currentLevel = 1;
+ let ricePattern;
+
+ const config = {
+ width: 800,
+ height: 500,
+ winThreshold: 0.95,
+ gravity: 1.5,
+ moldFill: 'rgba(239, 154, 154, 0.4)',
+ moldBorder: '#5d4037',
+ riceBorder: '#ffffff',
+ cutColor: '#ffffff'
};
- chatForm.addEventListener('submit', async (e) => {
- e.preventDefault();
- const message = chatInput.value.trim();
- if (!message) return;
-
- appendMessage(message, 'visitor');
- chatInput.value = '';
-
- try {
- const response = await fetch('api/chat.php', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
- const data = await response.json();
-
- // Artificial delay for realism
- setTimeout(() => {
- appendMessage(data.reply, 'bot');
- }, 500);
- } catch (error) {
- console.error('Error:', error);
- appendMessage("Sorry, something went wrong. Please try again.", 'bot');
- }
+ function resize() {
+ const container = canvas.parentElement;
+ const cw = container.clientWidth;
+ const ch = container.clientHeight;
+ canvas.width = cw;
+ canvas.height = ch;
+ // Adjust config based on actual size but keep internal logic consistent
+ config.width = cw;
+ config.height = ch;
+ }
+ window.addEventListener('resize', () => {
+ resize();
+ init();
});
-});
+ resize();
+
+ // --- Levels ---
+ const getLevelData = (lv) => {
+ const w = config.width;
+ const h = config.height;
+ const levels = {
+ 1: { // Square
+ rice: { x: w * 0.25, y: h * 0.5, w: 140, h: 140 },
+ mold: [
+ { x: w * 0.7 - 70, y: h * 0.6 - 70 },
+ { x: w * 0.7 + 70, y: h * 0.6 - 70 },
+ { x: w * 0.7 + 70, y: h * 0.6 + 70 },
+ { x: w * 0.7 - 70, y: h * 0.6 + 70 }
+ ]
+ },
+ 2: { // Triangle
+ rice: { x: w * 0.25, y: h * 0.5, w: 160, h: 160 },
+ mold: [
+ { x: w * 0.7, y: h * 0.6 - 80 },
+ { x: w * 0.7 + 90, y: h * 0.6 + 80 },
+ { x: w * 0.7 - 90, y: h * 0.6 + 80 }
+ ]
+ },
+ 3: { // House shape
+ rice: { x: w * 0.25, y: h * 0.5, w: 150, h: 150 },
+ mold: [
+ { x: w * 0.7 - 70, y: h * 0.6 + 70 },
+ { x: w * 0.7 + 70, y: h * 0.6 + 70 },
+ { x: w * 0.7 + 70, y: h * 0.6 - 20 },
+ { x: w * 0.7, y: h * 0.6 - 80 },
+ { x: w * 0.7 - 70, y: h * 0.6 - 20 }
+ ]
+ }
+ };
+ return levels[lv] || levels[1];
+ };
+
+ function createRiceTexture() {
+ const tCanvas = document.createElement('canvas');
+ tCanvas.width = 64; tCanvas.height = 64;
+ const tCtx = tCanvas.getContext('2d');
+ tCtx.fillStyle = '#ffffff';
+ tCtx.fillRect(0, 0, 64, 64);
+ for (let i = 0; i < 150; i++) {
+ const x = Math.random() * 64;
+ const y = Math.random() * 64;
+ const size = 1 + Math.random() * 2;
+ tCtx.beginPath();
+ tCtx.ellipse(x, y, size, size * 1.8, Math.random() * Math.PI, 0, Math.PI * 2);
+ tCtx.fillStyle = i % 15 === 0 ? '#f5f5f5' : '#ffffff';
+ tCtx.fill();
+ tCtx.strokeStyle = 'rgba(0,0,0,0.02)';
+ tCtx.lineWidth = 0.5;
+ tCtx.stroke();
+ }
+ ricePattern = tCtx.createPattern(tCanvas, 'repeat');
+ }
+ createRiceTexture();
+
+ function init() {
+ if (engine) {
+ World.clear(engine.world);
+ Engine.clear(engine);
+ Render.stop(render);
+ Runner.stop(runner);
+ }
+
+ engine = Engine.create();
+ engine.world.gravity.y = 0;
+
+ render = Render.create({
+ canvas: canvas,
+ engine: engine,
+ options: {
+ width: config.width,
+ height: config.height,
+ wireframes: false,
+ background: 'transparent'
+ }
+ });
+
+ Render.run(render);
+ runner = Runner.create();
+ Runner.run(runner, engine);
+
+ const mouse = Mouse.create(render.canvas);
+ mouseConstraint = MouseConstraint.create(engine, {
+ mouse: mouse,
+ constraint: { stiffness: 0.2, render: { visible: false } }
+ });
+ World.add(engine.world, mouseConstraint);
+ render.mouse = mouse;
+
+ loadLevel(currentLevel);
+ setupSlicing(mouse);
+
+ Events.on(render, 'afterRender', () => {
+ const ctx = render.context;
+ drawMold(ctx);
+ if (isDragging && lastMousePos) {
+ const current = mouse.position;
+ ctx.beginPath();
+ ctx.moveTo(lastMousePos.x, lastMousePos.y);
+ ctx.lineTo(current.x, current.y);
+ ctx.strokeStyle = config.cutColor;
+ ctx.lineWidth = 3;
+ ctx.setLineDash([5, 5]);
+ ctx.stroke();
+ ctx.setLineDash([]);
+ }
+ });
+
+ Events.on(engine, 'afterUpdate', () => {
+ if (isDropped) updateAccuracy();
+ });
+ }
+
+ function loadLevel(lv) {
+ const data = getLevelData(lv);
+ currentCuts = 0;
+ isDropped = false;
+ btnDrop.disabled = false;
+ engine.world.gravity.y = 0;
+ updateAccuracyUI(0);
+ updateStars(3);
+
+ const rice = Bodies.rectangle(data.rice.x, data.rice.y, data.rice.w, data.rice.h, {
+ render: { fillStyle: ricePattern, strokeStyle: config.riceBorder, lineWidth: 1 },
+ friction: 0.5,
+ restitution: 0.1,
+ slop: 0.01
+ });
+ riceBodies = [rice];
+ World.add(engine.world, rice);
+
+ // Ground for dropping
+ const ground = Bodies.rectangle(config.width * 0.7, config.height + 20, config.width, 40, { isStatic: true });
+ World.add(engine.world, ground);
+ }
+
+ function drawMold(ctx) {
+ const data = getLevelData(currentLevel);
+ ctx.beginPath();
+ ctx.moveTo(data.mold[0].x, data.mold[0].y);
+ for (let i = 1; i < data.mold.length; i++) ctx.lineTo(data.mold[i].x, data.mold[i].y);
+ ctx.closePath();
+ ctx.fillStyle = config.moldFill;
+ ctx.fill();
+ ctx.strokeStyle = config.moldBorder;
+ ctx.lineWidth = 8;
+ ctx.lineJoin = 'round';
+ ctx.stroke();
+ }
+
+ function setupSlicing(mouse) {
+ Events.on(mouseConstraint, 'mousedown', () => {
+ if (isDropped) return;
+ // Check if we are clicking a body to drag it
+ const clicked = Query.point(riceBodies, mouse.position)[0];
+ if (!clicked) {
+ isDragging = true;
+ lastMousePos = { x: mouse.position.x, y: mouse.position.y };
+ }
+ });
+
+ Events.on(mouseConstraint, 'mousemove', () => {
+ if (isDragging && !isDropped) {
+ const current = { x: mouse.position.x, y: mouse.position.y };
+ if (lastMousePos) {
+ performSlicing(lastMousePos, current);
+ }
+ lastMousePos = current;
+ }
+ });
+
+ Events.on(mouseConstraint, 'mouseup', () => {
+ isDragging = false;
+ lastMousePos = null;
+ });
+ }
+
+ function performSlicing(p1, p2) {
+ if (Vector.magnitude(Vector.sub(p1, p2)) < 5) return;
+
+ let slicedAny = false;
+ const toAdd = [];
+ const toRemove = [];
+
+ riceBodies.forEach(body => {
+ const result = sliceBody(body, p1, p2);
+ if (result && result.length > 1) {
+ toRemove.push(body);
+ toAdd.push(...result);
+ slicedAny = true;
+ }
+ });
+
+ if (slicedAny) {
+ World.remove(engine.world, toRemove);
+ World.add(engine.world, toAdd);
+ riceBodies = riceBodies.filter(b => !toRemove.includes(b)).concat(toAdd);
+ currentCuts++;
+ updateStars(calculateStars());
+ }
+ }
+
+ function sliceBody(body, p1, p2) {
+ const vertices = body.vertices.map(v => ({ x: v.x, y: v.y }));
+ const intersections = [];
+
+ for (let i = 0; i < vertices.length; i++) {
+ const a = vertices[i];
+ const b = vertices[(i + 1) % vertices.length];
+ const intersect = lineIntersect(p1, p2, a, b);
+ if (intersect) {
+ intersections.push({ point: intersect, edgeIndex: i });
+ }
+ }
+
+ // Only split if we have exactly 2 intersections with this segment
+ if (intersections.length !== 2) return null;
+
+ // Robust splitting
+ intersections.sort((a, b) => a.edgeIndex - b.edgeIndex);
+ const [i1, i2] = intersections;
+
+ const poly1 = [i1.point, i2.point];
+ for (let i = i2.edgeIndex + 1; i <= i1.edgeIndex + vertices.length; i++) {
+ poly1.push(vertices[i % vertices.length]);
+ }
+
+ const poly2 = [i2.point, i1.point];
+ for (let i = i1.edgeIndex + 1; i <= i2.edgeIndex; i++) {
+ poly2.push(vertices[i % vertices.length]);
+ }
+
+ return [poly1, poly2].map(poly => {
+ const center = Vertices.centre(poly);
+ return Bodies.fromVertices(center.x, center.y, [poly], {
+ render: { fillStyle: ricePattern, strokeStyle: config.riceBorder, lineWidth: 1 },
+ friction: 0.5,
+ restitution: 0.1
+ });
+ }).filter(b => b && b.area > 50);
+ }
+
+ function lineIntersect(p1, p2, p3, p4) {
+ const x1 = p1.x, y1 = p1.y, x2 = p2.x, y2 = p2.y;
+ const x3 = p3.x, y3 = p3.y, x4 = p4.x, y4 = p4.y;
+ const den = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
+ if (den === 0) return null;
+ const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / den;
+ const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / den;
+ if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
+ return { x: x1 + ua * (x2 - x1), y: y1 + ua * (y2 - y1) };
+ }
+ return null;
+ }
+
+ const offCanvas = document.createElement('canvas');
+ const offCtx = offCanvas.getContext('2d', { willReadFrequently: true });
+
+ function updateAccuracy() {
+ const data = getLevelData(currentLevel);
+ const mold = data.mold;
+
+ // Find bounds of mold to optimize
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
+ mold.forEach(v => {
+ minX = Math.min(minX, v.x); maxX = Math.max(maxX, v.x);
+ minY = Math.min(minY, v.y); maxY = Math.max(maxY, v.y);
+ });
+
+ const pad = 50;
+ offCanvas.width = maxX - minX + pad * 2;
+ offCanvas.height = maxY - minY + pad * 2;
+ const ox = minX - pad;
+ const oy = minY - pad;
+
+ offCtx.clearRect(0, 0, offCanvas.width, offCanvas.height);
+
+ // Draw mold (Target) in Red
+ offCtx.fillStyle = '#ff0000';
+ offCtx.beginPath();
+ offCtx.moveTo(mold[0].x - ox, mold[0].y - oy);
+ for (let i = 1; i < mold.length; i++) offCtx.lineTo(mold[i].x - ox, mold[i].y - oy);
+ offCtx.closePath();
+ offCtx.fill();
+
+ const moldImgData = offCtx.getImageData(0, 0, offCanvas.width, offCanvas.height).data;
+ let moldPixels = 0;
+ for (let i = 0; i < moldImgData.length; i += 4) {
+ if (moldImgData[i] > 128) moldPixels++;
+ }
+
+ // Draw overlapping rice in Blue using source-atop
+ offCtx.globalCompositeOperation = 'source-atop';
+ offCtx.fillStyle = '#0000ff';
+ riceBodies.forEach(b => {
+ offCtx.beginPath();
+ offCtx.moveTo(b.vertices[0].x - ox, b.vertices[0].y - oy);
+ for (let i = 1; i < b.vertices.length; i++) offCtx.lineTo(b.vertices[i].x - ox, b.vertices[i].y - oy);
+ offCtx.closePath();
+ offCtx.fill();
+ });
+
+ const overlapImgData = offCtx.getImageData(0, 0, offCanvas.width, offCanvas.height).data;
+ let overlapPixels = 0;
+ for (let i = 0; i < overlapImgData.length; i += 4) {
+ if (overlapImgData[i + 2] > 128) overlapPixels++;
+ }
+
+ const accuracy = moldPixels > 0 ? (overlapPixels / moldPixels) : 0;
+ updateAccuracyUI(accuracy);
+
+ // Auto win check
+ const allStopped = riceBodies.every(b => b.speed < 0.2);
+ if (allStopped && accuracy >= config.winThreshold) {
+ handleWin(accuracy);
+ }
+ }
+
+ function updateAccuracyUI(acc) {
+ const percent = Math.floor(acc * 100);
+ accuracyValueEl.textContent = percent;
+ }
+
+ function calculateStars() {
+ if (currentCuts <= 3) return 3;
+ if (currentCuts <= 6) return 2;
+ return 1;
+ }
+
+ function updateStars(count) {
+ let html = '';
+ for (let i = 0; i < 3; i++) {
+ html += i < count ? '⭐' : '☆';
+ }
+ starsPreviewEl.innerHTML = html;
+ }
+
+ function handleWin(acc) {
+ if (btnDrop.disabled && accuracyValueEl.dataset.win === "true") return;
+ accuracyValueEl.dataset.win = "true";
+ showToast(`EXCELLENT! ${Math.floor(acc * 100)}% Accuracy`);
+ setTimeout(() => {
+ currentLevel++;
+ if (currentLevel > 3) currentLevel = 1;
+ init();
+ }, 2500);
+ }
+
+ function showToast(msg) {
+ const container = document.getElementById('toast-container');
+ const toast = document.createElement('div');
+ toast.className = 'toast';
+ toast.textContent = msg;
+ container.appendChild(toast);
+ setTimeout(() => {
+ toast.style.opacity = '0';
+ setTimeout(() => toast.remove(), 500);
+ }, 2000);
+ }
+
+ btnDrop.onclick = () => {
+ isDropped = true;
+ btnDrop.disabled = true;
+
+ // Move pieces to above the mold
+ const data = getLevelData(currentLevel);
+ const moldCenter = { x: 0, y: 0 };
+ data.mold.forEach(v => { moldCenter.x += v.x; moldCenter.y += v.y; });
+ moldCenter.x /= data.mold.length;
+ moldCenter.y /= data.mold.length;
+
+ const riceCenter = data.rice;
+
+ riceBodies.forEach(b => {
+ const dx = b.position.x - riceCenter.x;
+ const dy = b.position.y - riceCenter.y;
+ Body.setPosition(b, { x: moldCenter.x + dx, y: moldCenter.y - 200 + dy });
+ });
+
+ engine.world.gravity.y = config.gravity;
+ };
+
+ btnReset.onclick = () => init();
+ btnHint.onclick = () => showToast("Slice manually then DROP!");
+
+ // Prevent scrolling on touch
+ document.body.addEventListener("touchstart", (e) => { if (e.target == canvas) e.preventDefault(); }, { passive: false });
+ document.body.addEventListener("touchend", (e) => { if (e.target == canvas) e.preventDefault(); }, { passive: false });
+ document.body.addEventListener("touchmove", (e) => { if (e.target == canvas) e.preventDefault(); }, { passive: false });
+
+ init();
+});
\ No newline at end of file
diff --git a/assets/pasted-20260303-164043-377c7089.jpg b/assets/pasted-20260303-164043-377c7089.jpg
new file mode 100644
index 0000000..4e4e8a0
Binary files /dev/null and b/assets/pasted-20260303-164043-377c7089.jpg differ
diff --git a/assets/pasted-20260303-164623-9997dfbb.jpg b/assets/pasted-20260303-164623-9997dfbb.jpg
new file mode 100644
index 0000000..d2b12fa
Binary files /dev/null and b/assets/pasted-20260303-164623-9997dfbb.jpg differ
diff --git a/assets/vm-shot-2026-03-03T16-40-40-984Z.jpg b/assets/vm-shot-2026-03-03T16-40-40-984Z.jpg
new file mode 100644
index 0000000..4bf41b5
Binary files /dev/null and b/assets/vm-shot-2026-03-03T16-40-40-984Z.jpg differ
diff --git a/db/migrations/01_init.sql b/db/migrations/01_init.sql
new file mode 100644
index 0000000..4829adf
--- /dev/null
+++ b/db/migrations/01_init.sql
@@ -0,0 +1 @@
+CREATE TABLE IF NOT EXISTS user_progress (id INT AUTO_INCREMENT PRIMARY KEY, level_id INT NOT NULL, stars INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY level_user (level_id));
diff --git a/index.php b/index.php
index 7205f3d..3519731 100644
--- a/index.php
+++ b/index.php
@@ -1,150 +1,65 @@
-
- New Style
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ = htmlspecialchars($projectName) ?>
+
+
+
+
-
-
+
+
+
+
+
+
-
-
-
Analyzing your requirements and generating your website…
-
-
Loading…
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+