Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
4e49ab077c Autosave: 20260303-165104 2026-03-03 16:51:04 +00:00
8 changed files with 656 additions and 434 deletions

15
api/progress.php Normal file
View File

@ -0,0 +1,15 @@
<?php
require_once __DIR__ . '/../db/config.php';
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
if (isset($data['level_id'], $data['stars'])) {
try {
$stmt = db()->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']);
}

View File

@ -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 { body {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); margin: 0;
background-size: 400% 400%; padding: 0;
animation: gradient 15s ease infinite; background-color: #d7ccc8;
color: #212529; font-family: var(--font-main);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; color: #3e2723;
font-size: 14px; overflow: hidden;
margin: 0; height: 100vh;
min-height: 100vh; display: flex;
justify-content: center;
align-items: center;
user-select: none;
-webkit-user-select: none;
touch-action: none;
} }
.main-wrapper { .game-container {
display: flex; width: 100vw;
align-items: center; height: 100vh;
justify-content: center; max-width: 900px;
min-height: 100vh; background: var(--wood-bg);
width: 100%; position: relative;
padding: 20px; border: 12px solid #8d6e63;
box-sizing: border-box; box-shadow: inset 0 0 100px rgba(0,0,0,0.15);
position: relative; /* Wood Board Texture */
z-index: 1; 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 { .game-header {
0% { padding: 15px 20px;
background-position: 0% 50%; display: flex;
} justify-content: space-between;
50% { align-items: center;
background-position: 100% 50%; z-index: 20;
}
100% {
background-position: 0% 50%;
}
} }
.chat-container { .icon-btn {
width: 100%; width: 48px;
max-width: 600px; height: 48px;
background: rgba(255, 255, 255, 0.85); border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.3); border: 2px solid var(--btn-border);
border-radius: 20px; background: rgba(255,255,255,0.4);
display: flex; color: var(--btn-border);
flex-direction: column; cursor: pointer;
height: 85vh; display: flex;
box-shadow: 0 20px 40px rgba(0,0,0,0.2); align-items: center;
backdrop-filter: blur(15px); justify-content: center;
-webkit-backdrop-filter: blur(15px); box-shadow: 0 3px 6px rgba(0,0,0,0.1);
overflow: hidden; transition: all 0.1s;
} }
.chat-header { .icon-btn:active {
padding: 1.5rem; transform: scale(0.92);
border-bottom: 1px solid rgba(0, 0, 0, 0.05); background: rgba(255,255,255,0.6);
background: rgba(255, 255, 255, 0.5);
font-weight: 700;
font-size: 1.1rem;
display: flex;
justify-content: space-between;
align-items: center;
} }
.chat-messages { .icon-btn svg {
flex: 1; width: 26px;
overflow-y: auto; height: 26px;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
} }
/* Custom Scrollbar */ .header-center {
::-webkit-scrollbar { flex: 1;
width: 6px; display: flex;
justify-content: center;
} }
::-webkit-scrollbar-track { .stars-display {
background: transparent; font-size: 2rem;
color: #ffb300;
text-shadow: 2px 2px 0 #3e2723;
letter-spacing: 4px;
} }
::-webkit-scrollbar-thumb { .star-gold { color: #ffb300; }
background: rgba(255, 255, 255, 0.3); .star-outline { color: #d7ccc8; opacity: 0.6; }
border-radius: 10px;
.main-stage {
flex: 1;
position: relative;
overflow: hidden;
} }
::-webkit-scrollbar-thumb:hover { #game-canvas {
background: rgba(255, 255, 255, 0.5); width: 100%;
height: 100%;
cursor: crosshair;
touch-action: none;
} }
.message { .game-footer {
max-width: 85%; padding: 20px;
padding: 0.85rem 1.1rem; display: flex;
border-radius: 16px; flex-direction: column;
line-height: 1.5; align-items: center;
font-size: 0.95rem; gap: 15px;
box-shadow: 0 4px 15px rgba(0,0,0,0.05); z-index: 20;
animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
} }
@keyframes fadeIn { .drop-trigger {
from { opacity: 0; transform: translateY(20px) scale(0.95); } background: var(--btn-red);
to { opacity: 1; transform: translateY(0) scale(1); } 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 { .drop-trigger:active {
align-self: flex-end; transform: translateY(3px);
background: linear-gradient(135deg, #212529 0%, #343a40 100%); box-shadow: 0 2px 0 var(--btn-border);
color: #fff;
border-bottom-right-radius: 4px;
} }
.message.bot { .drop-trigger:disabled {
align-self: flex-start; opacity: 0.6;
background: #ffffff; cursor: not-allowed;
color: #212529; transform: none;
border-bottom-left-radius: 4px; box-shadow: 0 5px 0 var(--btn-border);
} }
.chat-input-area { #accuracy-display {
padding: 1.25rem; font-weight: 700;
background: rgba(255, 255, 255, 0.5); font-size: 1rem;
border-top: 1px solid rgba(0, 0, 0, 0.05); 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 { #toast-container {
display: flex; position: fixed;
gap: 0.75rem; top: 40%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 1000;
} }
.chat-input-area input { .toast {
flex: 1; background: white;
border: 1px solid rgba(0, 0, 0, 0.1); padding: 20px 40px;
border-radius: 12px; border-radius: 40px;
padding: 0.75rem 1rem; border: 4px solid var(--btn-border);
outline: none; box-shadow: 8px 8px 0 var(--btn-border);
background: rgba(255, 255, 255, 0.9); font-size: 1.5rem;
transition: all 0.3s ease; font-weight: 700;
animation: pop-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
} }
.chat-input-area input:focus { @keyframes pop-in {
border-color: #23a6d5; 0% { transform: scale(0.4) rotate(-15deg); opacity: 0; }
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); 100% { transform: scale(1) rotate(0deg); opacity: 1; }
} }
.chat-input-area button { @media (max-width: 480px) {
background: #212529; .game-container { border-width: 6px; }
color: #fff; .drop-trigger { padding: 12px 35px; font-size: 1.1rem; }
border: none; .stars-display { font-size: 1.6rem; }
padding: 0.75rem 1.5rem; .icon-btn { width: 40px; height: 40px; }
border-radius: 12px; .icon-btn svg { width: 22px; height: 22px; }
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);
} }

View File

@ -1,39 +1,448 @@
document.addEventListener('DOMContentLoaded', () => { const { Engine, Render, Runner, World, Bodies, Body, Composite, Vertices, Vector, Events, Common, Mouse, MouseConstraint, Query } = Matter;
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages');
const appendMessage = (text, sender) => { // Ensure poly-decomp is set up for complex polygons
const msgDiv = document.createElement('div'); if (typeof decomp !== 'undefined') {
msgDiv.classList.add('message', sender); Common.setDecomp(window.decomp);
msgDiv.textContent = text; } else {
chatMessages.appendChild(msgDiv); console.error('poly-decomp.js is required for complex polygon splitting.');
chatMessages.scrollTop = chatMessages.scrollHeight; }
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) => { function resize() {
e.preventDefault(); const container = canvas.parentElement;
const message = chatInput.value.trim(); const cw = container.clientWidth;
if (!message) return; const ch = container.clientHeight;
canvas.width = cw;
appendMessage(message, 'visitor'); canvas.height = ch;
chatInput.value = ''; // Adjust config based on actual size but keep internal logic consistent
config.width = cw;
try { config.height = ch;
const response = await fetch('api/chat.php', { }
method: 'POST', window.addEventListener('resize', () => {
headers: { 'Content-Type': 'application/json' }, resize();
body: JSON.stringify({ message }) init();
});
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');
}
}); });
}); 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 ? '<span class="star-gold">⭐</span>' : '<span class="star-outline">☆</span>';
}
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();
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -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));

185
index.php
View File

@ -1,150 +1,65 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
@ini_set('display_errors', '1'); require_once __DIR__ . '/db/config.php';
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION; $projectName = $_SERVER['PROJECT_NAME'] ?? 'Bento Block: Rice Slicer';
$now = date('Y-m-d H:i:s'); $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Slice the rice manually and drop it into the mold!';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?> ?>
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
<title>New Style</title> <title><?= htmlspecialchars($projectName) ?></title>
<?php <meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
// Read project preview data from environment <?php if ($projectImageUrl): ?>
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? ''; <meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; <?php endif; ?>
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600;700&display=swap" rel="stylesheet">
<style> <link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
:root {
--bg-color-start: #6a11cb; <!-- Matter.js Physics Engine & Decomposition -->
--bg-color-end: #2575fc; <script src="https://cdnjs.cloudflare.com/ajax/libs/poly-decomp.js/0.3.0/poly-decomp.min.js"></script>
--text-color: #ffffff; <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head> </head>
<body> <body>
<main> <div class="game-container">
<div class="card"> <div class="game-header">
<h1>Analyzing your requirements and generating your website…</h1> <div class="header-left">
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> <button id="btn-reset" class="icon-btn" title="Reset">
<span class="sr-only">Loading…</span> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6"></path><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>
</button>
</div>
<div class="header-center">
<div id="stars-preview" class="stars-display">
<span></span><span></span><span></span>
</div>
</div>
<div class="header-right">
<button id="btn-hint" class="icon-btn" title="Hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
</button>
</div> </div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div> </div>
</main>
<footer> <div class="main-stage">
Page updated: <?= htmlspecialchars($now) ?> (UTC) <canvas id="game-canvas"></canvas>
</footer> </div>
<div class="game-footer">
<div id="accuracy-display">Accuracy: <span id="accuracy-value">0</span>%</div>
<button id="btn-drop" class="drop-trigger">DROP RICE</button>
</div>
</div>
<div id="toast-container"></div>
<!-- Game logic -->
<script src="assets/js/main.js?v=<?= time() ?>"></script>
</body> </body>
</html> </html>