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… +
+
+
+ +
+ +
+
+ +
+
+ +
+
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

-
- + +
+ +
+ + + + +
+ + + - + \ No newline at end of file