Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
@ -1,15 +0,0 @@
|
||||
<?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']);
|
||||
}
|
||||
@ -1,184 +1,302 @@
|
||||
: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 {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
.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-header {
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 20;
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.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-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:active {
|
||||
transform: scale(0.92);
|
||||
background: rgba(255,255,255,0.6);
|
||||
.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 svg {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.header-center {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.stars-display {
|
||||
font-size: 2rem;
|
||||
color: #ffb300;
|
||||
text-shadow: 2px 2px 0 #3e2723;
|
||||
letter-spacing: 4px;
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.star-gold { color: #ffb300; }
|
||||
.star-outline { color: #d7ccc8; opacity: 0.6; }
|
||||
|
||||
.main-stage {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#game-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: crosshair;
|
||||
touch-action: none;
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.game-footer {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
z-index: 20;
|
||||
.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);
|
||||
}
|
||||
|
||||
.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;
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px) scale(0.95); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
.drop-trigger:active {
|
||||
transform: translateY(3px);
|
||||
box-shadow: 0 2px 0 var(--btn-border);
|
||||
.message.visitor {
|
||||
align-self: flex-end;
|
||||
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.drop-trigger:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: 0 5px 0 var(--btn-border);
|
||||
.message.bot {
|
||||
align-self: flex-start;
|
||||
background: #ffffff;
|
||||
color: #212529;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
#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 {
|
||||
padding: 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
#toast-container {
|
||||
position: fixed;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
.chat-input-area form {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.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 {
|
||||
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;
|
||||
}
|
||||
|
||||
@keyframes pop-in {
|
||||
0% { transform: scale(0.4) rotate(-15deg); opacity: 0; }
|
||||
100% { transform: scale(1) rotate(0deg); opacity: 1; }
|
||||
.chat-input-area input:focus {
|
||||
border-color: #23a6d5;
|
||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
|
||||
}
|
||||
|
||||
@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; }
|
||||
.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);
|
||||
}
|
||||
@ -1,448 +1,39 @@
|
||||
const { Engine, Render, Runner, World, Bodies, Body, Composite, Vertices, Vector, Events, Common, Mouse, MouseConstraint, Query } = Matter;
|
||||
|
||||
// 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');
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
|
||||
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'
|
||||
const appendMessage = (text, sender) => {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.classList.add('message', sender);
|
||||
msgDiv.textContent = text;
|
||||
chatMessages.appendChild(msgDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
};
|
||||
|
||||
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();
|
||||
chatForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
// --- 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];
|
||||
};
|
||||
appendMessage(message, 'visitor');
|
||||
chatInput.value = '';
|
||||
|
||||
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
|
||||
try {
|
||||
const response = await fetch('api/chat.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message })
|
||||
});
|
||||
}).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) };
|
||||
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');
|
||||
}
|
||||
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.
|
Before Width: | Height: | Size: 426 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 299 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB |
@ -1 +0,0 @@
|
||||
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
185
index.php
@ -1,65 +1,150 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
$projectName = $_SERVER['PROJECT_NAME'] ?? 'Bento Block: Rice Slicer';
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Slice the rice manually and drop it into the mold!';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
|
||||
<title><?= htmlspecialchars($projectName) ?></title>
|
||||
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?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.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
|
||||
|
||||
<!-- Matter.js Physics Engine & Decomposition -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/poly-decomp.js/0.3.0/poly-decomp.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--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>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<div class="game-header">
|
||||
<div class="header-left">
|
||||
<button id="btn-reset" class="icon-btn" title="Reset">
|
||||
<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>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</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 class="main-stage">
|
||||
<canvas id="game-canvas"></canvas>
|
||||
</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>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user