Commit 3fc863bf authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: chess piece theming + tournament module + promo assets

- Add chess piece upload UI to admin/branding.php (12 slots: white + black)
- Fix loadPieceImages race condition in board renderer
- Add tournament system module (Swiss, knockout, arena)
- Add promo banner (1m×3m display) and video assets
- Add 2-min display video script
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 40356211
...@@ -259,6 +259,81 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['asset'])) { ...@@ -259,6 +259,81 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['asset'])) {
</div> </div>
</div> </div>
<!-- CHESS PIECES -->
<h2>♟ قطع الشطرنج</h2>
<p style="color:#64748b;font-size:12px;margin-bottom:16px;">ارفع صور PNG أو SVG لكل قطعة — ستظهر على الرقعة مباشرة بدل الرسم الافتراضي</p>
<div class="section">
<h3 style="color:#f8fafc;margin-bottom:12px;">⬜ القطع البيضاء</h3>
<div class="grid">
<?php
$whitePieces = [
['slot' => 'chess_piece_wK', 'label' => 'ملك أبيض ♔', 'w' => 128, 'h' => 128, 'hint' => 'White King'],
['slot' => 'chess_piece_wQ', 'label' => 'وزير أبيض ♕', 'w' => 128, 'h' => 128, 'hint' => 'White Queen'],
['slot' => 'chess_piece_wR', 'label' => 'قلعة بيضاء ♖', 'w' => 128, 'h' => 128, 'hint' => 'White Rook'],
['slot' => 'chess_piece_wB', 'label' => 'فيل أبيض ♗', 'w' => 128, 'h' => 128, 'hint' => 'White Bishop'],
['slot' => 'chess_piece_wN', 'label' => 'حصان أبيض ♘', 'w' => 128, 'h' => 128, 'hint' => 'White Knight'],
['slot' => 'chess_piece_wP', 'label' => 'بيدق أبيض ♙', 'w' => 128, 'h' => 128, 'hint' => 'White Pawn'],
];
foreach ($whitePieces as $a):
$current = $theme['assets'][$a['slot']] ?? null;
?>
<div class="field">
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="slot" value="<?= $a['slot'] ?>">
<input type="hidden" name="expected_w" value="<?= $a['w'] ?>">
<input type="hidden" name="expected_h" value="<?= $a['h'] ?>">
<label><?= $a['label'] ?></label>
<div class="upload-box" onclick="this.querySelector('input[type=file]').click()">
<input type="file" name="asset" accept=".svg,.png,.webp" style="display:none" onchange="this.form.submit()">
<?php if ($current): ?>
<div class="current"><img src="<?= $current ?>" style="width:64px;height:64px;object-fit:contain;background:#F0D9B5;border-radius:8px;padding:4px;"></div>
<div style="font-size:10px;color:#34D399;margin-top:4px;">✓ مرفوع</div>
<?php else: ?>
📤 اضغط للرفع
<?php endif; ?>
<div class="size-hint"><?= $a['w'] ?>×<?= $a['h'] ?>px — <?= $a['hint'] ?></div>
</div>
</form>
</div>
<?php endforeach; ?>
</div>
<h3 style="color:#f8fafc;margin:20px 0 12px;">⬛ القطع السوداء</h3>
<div class="grid">
<?php
$blackPieces = [
['slot' => 'chess_piece_bK', 'label' => 'ملك أسود ♚', 'w' => 128, 'h' => 128, 'hint' => 'Black King'],
['slot' => 'chess_piece_bQ', 'label' => 'وزير أسود ♛', 'w' => 128, 'h' => 128, 'hint' => 'Black Queen'],
['slot' => 'chess_piece_bR', 'label' => 'قلعة سوداء ♜', 'w' => 128, 'h' => 128, 'hint' => 'Black Rook'],
['slot' => 'chess_piece_bB', 'label' => 'فيل أسود ♝', 'w' => 128, 'h' => 128, 'hint' => 'Black Bishop'],
['slot' => 'chess_piece_bN', 'label' => 'حصان أسود ♞', 'w' => 128, 'h' => 128, 'hint' => 'Black Knight'],
['slot' => 'chess_piece_bP', 'label' => 'بيدق أسود ♟', 'w' => 128, 'h' => 128, 'hint' => 'Black Pawn'],
];
foreach ($blackPieces as $a):
$current = $theme['assets'][$a['slot']] ?? null;
?>
<div class="field">
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="slot" value="<?= $a['slot'] ?>">
<input type="hidden" name="expected_w" value="<?= $a['w'] ?>">
<input type="hidden" name="expected_h" value="<?= $a['h'] ?>">
<label><?= $a['label'] ?></label>
<div class="upload-box" onclick="this.querySelector('input[type=file]').click()">
<input type="file" name="asset" accept=".svg,.png,.webp" style="display:none" onchange="this.form.submit()">
<?php if ($current): ?>
<div class="current"><img src="<?= $current ?>" style="width:64px;height:64px;object-fit:contain;background:#B58863;border-radius:8px;padding:4px;"></div>
<div style="font-size:10px;color:#34D399;margin-top:4px;">✓ مرفوع</div>
<?php else: ?>
📤 اضغط للرفع
<?php endif; ?>
<div class="size-hint"><?= $a['w'] ?>×<?= $a['h'] ?>px — <?= $a['hint'] ?></div>
</div>
</form>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- LUDO BOARD --> <!-- LUDO BOARD -->
<h2>🎲 Ludo Board Colors</h2> <h2>🎲 Ludo Board Colors</h2>
<div class="section"> <div class="section">
......
# سكريبت عرض — العب | دقيقتين | بدون صوت | 6m x 4m (3:2 landscape)
> **Format:** Text + visuals only. No voiceover. Designed for large wall display.
> **Aspect Ratio:** 3:2 landscape (6 meters wide x 4 meters tall)
> **Duration:** 120 seconds
> **Style:** Game trailer — kinetic Arabic typography, hard cuts, bass thuds, glitch flashes
---
## BEAT 1 — BLACK (0:00 – 0:05)
**[VISUAL]:** أسود تام. فلاش أبيض سريع (0.1s). تظهر 3 خطوط رفيعة بالتتابع:
```
🎲 ━━━━━━━━━━━━━━━━━━
♟ ━━━━━━━━━━━━━━━━━━
🁣 ━━━━━━━━━━━━━━━━━━
```
الخطوط تخفت بعد ثانيتين. Bass thud خفيف.
---
## BEAT 2 — HOOK (0:05 – 0:15)
**[VISUAL]:** فلاش أبيض →
**TEXT SLAM (ملء الشاشة):**
> في ناس شايفة إن الألعاب الذهنية
> ~~حاجة قديمة.~~
*يفضل 3 ثواني. فلاش →*
**TEXT SLAM:**
> الجديد؟
*ثانية واحدة. فلاش →*
**TEXT SLAM:**
> إنك تحطها في إيد
> **كل طفل في مصر.** ← (ذهبي)
---
## BEAT 3 — FIRST LOOK (0:15 – 0:25)
**[VISUAL]:** فلاش → Screenshot لوحة الشطرنج (phone frame) يظهر من blur+scale كبير → يثبت في النص. يفضل 3 ثواني.
فلاش ذهبي →
**TEXT SLAM (ضخم جداً):**
> **ده العب.** ← (ذهبي، 14vh)
---
## BEAT 4 — THE THREE GAMES (0:25 – 0:40)
**[VISUAL]:** فلاش → 3 phones تطلع من تحت بالتتابع (0.1s delay بينهم):
| يسار | وسط | يمين |
|------|------|------|
| شطرنج (midgame) | لودو (board) | دومينو (menu) |
تحتهم بعد ثانية:
> شطرنج • لودو • دومينو
*تفضل 4 ثواني. فلاش →*
**TEXT SLAM:**
> من أول ما تفتح — لحد ما تلعب
> **ثواني.** ← (سيان)
*فلاش →*
**TEXT SLAM (3 أسطر):**
> مفيش تسجيل معقد.
> مفيش إعلان بيقطعك.
> **60 فريم. سلس.** ← (ذهبي)
---
## BEAT 5 — NOT JUST A GAME (0:40 – 0:55)
**[VISUAL]:** فلاش →
**TEXT SLAM (كبير):**
> مش لعبة ~~وخلاص.~~
*فلاش → Screenshot شطرنج midgame (phone frame) →*
**TEXT SLAM:**
> تعليم شطرنج مدمج.
> ألغاز يومية. تصنيف.
> **الطفل بيلعب وهو بيتعلم.** ← (ذهبي)
---
## BEAT 6 — TOURNAMENTS (0:55 – 1:20)
**[VISUAL]:** فلاش ذهبي →
**TEXT SLAM (ضخم):**
> **بطولات.** ← (ذهبي، 14vh)
*فلاش → Bracket animation: نقاط ذهبية تظهر واحدة واحدة، خطوط تتمد بينهم (tournament bracket visual). فوقه:*
**TEXT SLAM:**
> مدارس • جامعات • أندية
> شركات • مراكز شباب • أحياء
*5 ثواني. فلاش →*
**TEXT SLAM:**
> Swiss • Knockout • Arena
> **كل الأنظمة جاهزة.** ← (ذهبي)
*فلاش →*
**TEXT SLAM (4 أسطر):**
> القرعة أوتوماتيك.
> الجدول أوتوماتيك.
> النتايج أوتوماتيك.
> **البنية التحتية شغالة.** ← (سيان)
---
## BEAT 7 — SAFETY (1:20 – 1:30)
**[VISUAL]:** فلاش → 🛡️ (ضخم، يظهر بـspin+scale). تحته:
**TEXT SLAM:**
> **مفيش chat مفتوح.** ← (أخضر)
> عبارات جاهزة. نظام إبلاغ.
> **بيئة نضيفة. الأهل يطمنوا.** ← (أخضر)
---
## BEAT 8 — THE NUMBERS (1:30 – 1:45)
**[VISUAL]:** فلاش → 3 أرقام تظهر بالتتابع (stat counters):
| 4 | 38 | 100% |
|---|---|---|
| ألعاب ذهنية | module إدارة | مصري |
*3 ثواني. فلاش ذهبي →*
**NUMBER SLAM (ضخم جداً، 20vh):**
> **400M**
تحته:
> المنطقة العربية.
> **مفيهاش منافس محلي.** ← (ذهبي)
---
## BEAT 9 — THE PUNCH (1:45 – 1:52)
**[VISUAL]:** فلاش →
**TEXT SLAM (كبير):**
> السوق مفتوح.
> **والمنتج جاهز.** ← (سيان)
---
## BEAT 10 — CLOSER (1:52 – 2:00)
**[VISUAL]:** فلاش ذهبي → أسود. اللوجو (logof.png) يظهر من scale صغير → يكبر ببطء. Drop shadow ذهبي ضخم. بعد ثانيتين:
**TEXT (هادي، مش slam):**
> الأطفال بتوعنا يستاهلوا أحسن من كده.
**TEXT (ذهبي):**
> وده — أحسن من كده.
*الجزيئات الذهبية بتطلع ببطء. اللوجو يفضل 5 ثواني. Fade to black.*
---
## ملاحظات إنتاج
| | |
|---|---|
| **Aspect Ratio** | 3:2 landscape (6000 x 4000px render, scaled to display) |
| **Tempo** | Cut كل 3-6 ثواني. مفيش مشهد يفضل أكتر من 10 ثواني |
| **Typography** | IBM Plex Sans Arabic — weights 700-900 للـslams |
| **Colors** | Black bg, white text, gold (#E4AC38) accent, cyan (#00FFFF) secondary, green (#34D399) safety |
| **Transitions** | White flash (0.12s) on every cut. Gold flash for emphasis moments |
| **Audio** | Sub-bass drone (45Hz) + bass thud on every flash/cut. No VO. No music bed needed — the thuds ARE the rhythm |
| **Rhythm** | ~20 cuts total. كل cut عليه thud. ده بيخلي الفيديو عنده heartbeat |
| **Text Rules** | Max 3 lines per slam. One idea per screen. Gold = emphasis. Dim/strikethrough = dismissal |
| **Screenshots** | Phone frames (rounded corners, subtle glow, dark border) — never flat rectangles |
| **Particles** | Gold + cyan particles floating up slowly throughout. Subtle. Depth. |
This diff is collapsed.
{
"devDependencies": {
"puppeteer": "^24.43.1"
}
}
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EL3AB — Promo Display A</title>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;600;700;800;900&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100vw; height: 100vh;
overflow: hidden;
font-family: 'IBM Plex Sans Arabic', sans-serif;
background: #000; color: #F8FAFC;
display: flex; align-items: center; justify-content: center;
}
.stage {
width: min(100vw, calc(100vh / 3));
height: min(100vh, calc(100vw * 3));
position: relative; overflow: hidden;
background: #030508;
}
/* BG */
.bg {
position: absolute; inset: 0;
background:
radial-gradient(ellipse 120% 25% at 50% 0%, rgba(228,172,56,0.05) 0%, transparent 50%),
radial-gradient(ellipse 100% 20% at 50% 100%, rgba(32,130,240,0.03) 0%, transparent 50%);
}
.grid-bg {
position: absolute; inset: 0; opacity: 0.015;
background-image:
linear-gradient(rgba(255,255,255,0.4) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.4) 1px, transparent 1px);
background-size: 10% 3.33%;
animation: gridm 25s linear infinite;
}
@keyframes gridm { 0%{transform:translateY(0);} 100%{transform:translateY(3.33%);} }
/* Particles */
.ptcls { position: absolute; inset: 0; z-index: 1; pointer-events: none; }
.pt {
position: absolute; border-radius: 50%;
animation: ptup linear infinite;
}
@keyframes ptup {
0%{transform:translateY(0) scale(1);opacity:0;}
10%{opacity:0.7;} 90%{opacity:0.2;}
100%{transform:translateY(-100vh) scale(0.2);opacity:0;}
}
/* Content */
.content {
position: relative; z-index: 10;
width: 100%; height: 100%;
display: flex; flex-direction: column;
align-items: center; justify-content: space-between;
padding: 4% 7%;
}
/* TOP */
.top {
display: flex; flex-direction: column;
align-items: center; gap: 2cqi;
padding-top: 2%;
}
.logo {
width: 28cqi; height: 28cqi;
object-fit: contain;
filter: drop-shadow(0 0 25px rgba(228,172,56,0.4));
animation: lbr 3.5s ease-in-out infinite;
}
@keyframes lbr { 0%,100%{transform:scale(1);} 50%{transform:scale(1.03);} }
.brand {
font-size: 12cqi; font-weight: 900;
background: linear-gradient(180deg, #FFF 20%, #E4AC38 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.tagline {
font-size: 3.5cqi; color: #94A3B8;
font-weight: 300; letter-spacing: 0.15em;
}
/* MIDDLE */
.mid {
display: flex; flex-direction: column;
align-items: center; gap: 4%;
flex: 1; justify-content: center;
width: 100%;
}
.phone-hero {
position: relative;
width: 60%;
aspect-ratio: 9/19.5;
border-radius: 5%;
overflow: hidden;
border: 2px solid rgba(255,255,255,0.06);
box-shadow: 0 30px 80px rgba(0,0,0,0.7), 0 0 50px rgba(32,130,240,0.08);
animation: phf 5s ease-in-out infinite;
}
@keyframes phf { 0%,100%{transform:translateY(0);} 50%{transform:translateY(-1%);} }
.phone-hero img { width: 100%; height: 100%; object-fit: cover; }
/* Phone glow ring */
.phone-glow {
position: absolute; inset: -3px;
border-radius: 5%; z-index: -1;
background: conic-gradient(from 0deg, rgba(228,172,56,0.3), rgba(32,130,240,0.2), rgba(0,255,255,0.2), rgba(228,172,56,0.3));
filter: blur(8px);
animation: glow-spin 8s linear infinite;
}
@keyframes glow-spin { 0%{transform:rotate(0);} 100%{transform:rotate(360deg);} }
.features {
display: flex; flex-direction: column;
align-items: center; gap: 2cqi;
}
.feat {
display: flex; align-items: center; gap: 2cqi;
font-size: 3cqi; color: #94A3B8;
}
.feat-dot {
width: 6px; height: 6px; border-radius: 50%;
background: #E4AC38; flex-shrink: 0;
}
/* BOTTOM */
.bot {
display: flex; flex-direction: column;
align-items: center; gap: 3%;
padding-bottom: 3%;
width: 100%;
}
.cta-line {
font-size: 5cqi; font-weight: 800;
text-align: center;
}
.cta-line .gold { color: #E4AC38; }
.qr-box {
position: relative;
padding: 3%;
background: #fff;
border-radius: 6%;
box-shadow: 0 0 40px rgba(228,172,56,0.2), 0 15px 40px rgba(0,0,0,0.5);
animation: qrp 2.5s ease-in-out infinite;
}
@keyframes qrp {
0%,100%{box-shadow: 0 0 40px rgba(228,172,56,0.2), 0 15px 40px rgba(0,0,0,0.5);}
50%{box-shadow: 0 0 60px rgba(228,172,56,0.4), 0 15px 40px rgba(0,0,0,0.5);}
}
.qr-box img { width: 22cqi; height: 22cqi; display: block; }
.url {
font-size: 2.5cqi; color: #2082F0;
direction: ltr; font-family: monospace;
}
.egypt {
display: flex; align-items: center; gap: 1.5cqi;
font-size: 2.5cqi; color: #64748B;
padding: 1.5% 4%;
border-radius: 999px;
background: rgba(255,255,255,0.02);
border: 1px solid rgba(255,255,255,0.05);
}
.eg-flag {
width: 3cqi; height: 2cqi;
border-radius: 2px;
display: flex; flex-direction: column; overflow: hidden;
}
.eg-flag span { flex: 1; }
.eg-flag span:nth-child(1) { background: #CE1126; }
.eg-flag span:nth-child(2) { background: #FFF; }
.eg-flag span:nth-child(3) { background: #000; }
</style>
</head>
<body>
<div class="stage">
<div class="bg"></div>
<div class="grid-bg"></div>
<div class="ptcls" id="ptcls"></div>
<div class="content">
<div class="top">
<img class="logo" src="video/assets/logof.png" alt="EL3AB">
<div class="brand">العب</div>
<div class="tagline">منصة الألعاب الذهنية المصرية</div>
</div>
<div class="mid">
<div class="phone-hero">
<div class="phone-glow"></div>
<img src="screenshots/02-home.png" alt="EL3AB">
</div>
<div class="features">
<div class="feat"><span class="feat-dot"></span>بطولات مدارس وجامعات وأندية</div>
<div class="feat"><span class="feat-dot"></span>تعليم شطرنج تفاعلي</div>
<div class="feat"><span class="feat-dot"></span>بيئة آمنة 100% للأطفال</div>
<div class="feat"><span class="feat-dot"></span>مجاني بالكامل</div>
</div>
</div>
<div class="bot">
<div class="cta-line">امسح <span class="gold">وابدأ العب</span></div>
<div class="qr-box">
<img src="video/assets/qr-code.png" alt="QR">
</div>
<div class="url">el3ab-player.caprover.al-arcade.com</div>
<div class="egypt">
<div class="eg-flag"><span></span><span></span><span></span></div>
صنع في مصر
</div>
</div>
</div>
</div>
<script>
const c = document.getElementById('ptcls');
const cols = ['rgba(228,172,56,0.5)','rgba(32,130,240,0.4)','rgba(0,255,255,0.3)'];
for (let i = 0; i < 20; i++) {
const p = document.createElement('div');
p.className = 'pt';
const s = 2 + Math.random() * 4;
Object.assign(p.style, {
width:s+'px', height:s+'px',
left:Math.random()*100+'%', bottom:'-3%',
background:cols[i%3],
animationDuration:(10+Math.random()*14)+'s',
animationDelay:(Math.random()*12)+'s'
});
c.appendChild(p);
}
</script>
</body>
</html>
This diff is collapsed.
This diff is collapsed.
const puppeteer = require('puppeteer');
const path = require('path');
const BASE = 'https://el3ab-player.caprover.al-arcade.com';
const OUT = path.join(__dirname, 'screenshots');
const PHONE = { width: 390, height: 844, deviceScaleFactor: 2 };
const wait = ms => new Promise(r => setTimeout(r, ms));
const EMAIL = 'promo-test@el3ab.com';
const PASS = 'PromoTest123';
async function run() {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.setViewport(PHONE);
// ─── 1. LOGIN ───
console.log('1. Loading app...');
await page.goto(BASE, { waitUntil: 'networkidle2', timeout: 30000 });
await wait(3000);
await page.screenshot({ path: path.join(OUT, '01-login.png') });
console.log(' ✓ 01-login.png');
console.log('2. Logging in...');
const inputs = await page.$$('input');
if (inputs.length >= 2) {
await inputs[0].click({ clickCount: 3 });
await inputs[0].type(EMAIL);
await inputs[1].click({ clickCount: 3 });
await inputs[1].type(PASS);
await wait(300);
const btn = await page.$('button');
if (btn) await btn.click();
await wait(6000);
}
// ─── 2. HOME SCREEN ───
await page.screenshot({ path: path.join(OUT, '02-home.png') });
console.log(' ✓ 02-home.png');
// ─── 3. CHESS: Navigate to chess ───
console.log('3. Navigating to Chess...');
try {
const chessEl = await page.evaluateHandle(() => {
const all = document.querySelectorAll('*');
for (const el of all) {
const txt = el.textContent?.trim();
if (txt === 'شطرنج' || txt === 'Chess') return el;
}
for (const el of all) {
if (el.textContent?.includes('شطرنج') && el.tagName !== 'BODY' && el.tagName !== 'HTML' && el.children.length < 5) return el;
}
return null;
});
if (chessEl) {
await chessEl.click();
await wait(3000);
await page.screenshot({ path: path.join(OUT, '03-chess-menu.png') });
console.log(' ✓ 03-chess-menu.png');
}
} catch(e) { console.log(' ✗ Chess nav failed:', e.message); }
// ─── 4. CHESS: Click "لاعب واحد" (single player) ───
console.log('4. Selecting single player mode...');
try {
const singleEl = await page.evaluateHandle(() => {
const all = document.querySelectorAll('*');
for (const el of all) {
const txt = el.textContent?.trim();
if (txt === 'لاعب واحد' || txt === 'Single Player') return el;
}
return null;
});
if (singleEl) {
await singleEl.click();
await wait(3000);
}
} catch(e) { console.log(' ✗ Single player:', e.message); }
// ─── 5. CHESS: Bot selection screen ───
await page.screenshot({ path: path.join(OUT, '04-bot-select.png') });
console.log(' ✓ 04-bot-select.png');
// ─── 6. CHESS: Click first bot card ───
console.log('5. Selecting a bot...');
try {
const botCard = await page.evaluateHandle(() => {
const card = document.querySelector('.bot-card');
return card || null;
});
if (botCard) {
await botCard.click();
await wait(2000);
console.log(' → Bot selected, now on time select');
// ─── 7. CHESS: Click a time control (5+0 blitz) ───
const timeBtn = await page.evaluateHandle(() => {
const btns = document.querySelectorAll('.time-btn, button');
for (const b of btns) {
if (b.textContent?.includes('5+0') || b.textContent?.includes('3+0')) return b;
}
// Fallback: first time button
const firstTime = document.querySelector('.time-btn');
return firstTime || null;
});
if (timeBtn) {
await timeBtn.click();
console.log(' → Time control selected, game starting...');
await wait(6000);
await page.screenshot({ path: path.join(OUT, '05-chess-start.png') });
console.log(' ✓ 05-chess-start.png');
// ─── 8. CHESS: Play moves by clicking canvas ───
console.log('6. Playing chess moves...');
const canvas = await page.$('canvas');
if (canvas) {
const box = await canvas.boundingBox();
if (box) {
const sq = box.width / 8;
// Click e2 then e4 (column 4, rows from bottom: row 6 = e2, row 4 = e4)
await page.mouse.click(box.x + sq * 4.5, box.y + sq * 6.5);
await wait(500);
await page.mouse.click(box.x + sq * 4.5, box.y + sq * 4.5);
await wait(3000); // Wait for bot response
// Click d2 then d4
await page.mouse.click(box.x + sq * 3.5, box.y + sq * 6.5);
await wait(500);
await page.mouse.click(box.x + sq * 3.5, box.y + sq * 4.5);
await wait(3000); // Wait for bot response
// Click Nf3 (g1 to f3)
await page.mouse.click(box.x + sq * 6.5, box.y + sq * 7.5);
await wait(500);
await page.mouse.click(box.x + sq * 5.5, box.y + sq * 5.5);
await wait(3000);
}
}
await page.screenshot({ path: path.join(OUT, '06-chess-midgame.png') });
console.log(' ✓ 06-chess-midgame.png');
}
}
} catch(e) { console.log(' ✗ Bot/game flow:', e.message); }
// ─── 9. LUDO ───
console.log('7. Navigating to Ludo...');
await page.goto(BASE, { waitUntil: 'networkidle2', timeout: 30000 });
await wait(4000);
try {
const ludoEl = await page.evaluateHandle(() => {
const all = document.querySelectorAll('*');
for (const el of all) {
const txt = el.textContent?.trim();
if (txt === 'لودو' || txt === 'Ludo') return el;
}
for (const el of all) {
if (el.textContent?.includes('لودو') && el.tagName !== 'BODY' && el.tagName !== 'HTML' && el.children.length < 5) return el;
}
return null;
});
if (ludoEl) {
await ludoEl.click();
await wait(3000);
await page.screenshot({ path: path.join(OUT, '07-ludo-menu.png') });
console.log(' ✓ 07-ludo-menu.png');
// Start ludo game vs bot
const ludoPlayBtn = await page.evaluateHandle(() => {
const all = document.querySelectorAll('button, .btn, [class*="play"], [class*="start"]');
for (const el of all) {
const txt = el.textContent?.trim();
if (txt && (txt.includes('العب') || txt.includes('ابدأ') || txt.includes('بوت') || txt.includes('لاعب واحد'))) return el;
}
return null;
});
if (ludoPlayBtn) {
await ludoPlayBtn.click();
await wait(6000);
await page.screenshot({ path: path.join(OUT, '08-ludo-game.png') });
console.log(' ✓ 08-ludo-game.png');
}
}
} catch(e) { console.log(' ✗ Ludo:', e.message); }
// ─── 10. DOMINO ───
console.log('8. Navigating to Domino...');
await page.goto(BASE, { waitUntil: 'networkidle2', timeout: 30000 });
await wait(4000);
try {
const domEl = await page.evaluateHandle(() => {
const all = document.querySelectorAll('*');
for (const el of all) {
const txt = el.textContent?.trim();
if (txt === 'دومينو' || txt === 'Domino') return el;
}
return null;
});
if (domEl) {
await domEl.click();
await wait(3000);
await page.screenshot({ path: path.join(OUT, '09-domino-menu.png') });
console.log(' ✓ 09-domino-menu.png');
}
} catch(e) { console.log(' ✗ Domino:', e.message); }
// ─── 11. FINAL HOME ───
console.log('9. Final home capture...');
await page.goto(BASE, { waitUntil: 'networkidle2', timeout: 30000 });
await wait(5000);
await page.screenshot({ path: path.join(OUT, '10-home-final.png') });
console.log(' ✓ 10-home-final.png');
await browser.close();
console.log('\n✅ Done! Screenshots saved to:', OUT);
}
run().catch(e => { console.error(e); process.exit(1); });
This diff is collapsed.
...@@ -74,17 +74,17 @@ function renderTabBar() { ...@@ -74,17 +74,17 @@ function renderTabBar() {
rank: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3 7h7l-5.5 4.5 2 7L12 16l-6.5 4.5 2-7L2 9h7z"/></svg>', rank: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3 7h7l-5.5 4.5 2 7L12 16l-6.5 4.5 2-7L2 9h7z"/></svg>',
social: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M16 11c1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3 1.34 3 3 3zm-8 0c1.66 0 3-1.34 3-3S9.66 5 8 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>', social: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M16 11c1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3 1.34 3 3 3zm-8 0c1.66 0 3-1.34 3-3S9.66 5 8 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>',
play: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>', play: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>',
shop: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 18c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zM1 2v2h2l3.6 7.59-1.35 2.45c-.16.28-.25.61-.25.96 0 1.1.9 2 2 2h12v-2H7.42c-.14 0-.25-.11-.25-.25l.03-.12.9-1.63h7.45c.75 0 1.41-.41 1.75-1.03l3.58-6.49c.08-.14.12-.31.12-.48 0-.55-.45-1-1-1H5.21l-.94-2H1zm16 16c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>', tournaments: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 5h-2V3H7v2H5c-1.1 0-2 .9-2 2v1c0 2.55 1.92 4.63 4.39 4.94.63 1.5 1.98 2.63 3.61 2.96V19H7v2h10v-2h-4v-3.1c1.63-.33 2.98-1.46 3.61-2.96C19.08 12.63 21 10.55 21 8V7c0-1.1-.9-2-2-2zM5 8V7h2v3.82C5.84 10.4 5 9.3 5 8zm14 0c0 1.3-.84 2.4-2 2.82V7h2v1z"/></svg>',
profile: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>' profile: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>'
}; };
const icons = { const icons = {
rank: assetImg('tab_rank', svgFallback.rank, 22, 22), rank: assetImg('tab_rank', svgFallback.rank, 22, 22),
social: assetImg('tab_social', svgFallback.social, 22, 22), social: assetImg('tab_social', svgFallback.social, 22, 22),
play: assetImg('tab_play', svgFallback.play, 22, 22), play: assetImg('tab_play', svgFallback.play, 22, 22),
shop: assetImg('tab_shop', svgFallback.shop, 22, 22), tournaments: assetImg('tab_tournaments', svgFallback.tournaments, 22, 22),
profile: assetImg('tab_profile', svgFallback.profile, 22, 22), profile: assetImg('tab_profile', svgFallback.profile, 22, 22),
}; };
const labels = { rank: 'nav.rank', social: 'nav.social', play: 'nav.play', shop: 'nav.shop', profile: 'nav.profile' }; const labels = { rank: 'nav.rank', social: 'nav.social', play: 'nav.play', tournaments: 'nav.tournaments', profile: 'nav.profile' };
const current = scene.getCurrentWorld(); const current = scene.getCurrentWorld();
tabBar.innerHTML = worlds.map(w => ` tabBar.innerHTML = worlds.map(w => `
......
...@@ -6,7 +6,7 @@ const strings = { ...@@ -6,7 +6,7 @@ const strings = {
'nav.rank': 'الترتيب', 'nav.rank': 'الترتيب',
'nav.social': 'الأصدقاء', 'nav.social': 'الأصدقاء',
'nav.play': 'العب', 'nav.play': 'العب',
'nav.shop': 'المتجر', 'nav.tournaments': 'البطولات',
'nav.profile': 'حسابي', 'nav.profile': 'حسابي',
'auth.login': 'تسجيل الدخول', 'auth.login': 'تسجيل الدخول',
'auth.register': 'حساب جديد', 'auth.register': 'حساب جديد',
...@@ -75,7 +75,7 @@ const strings = { ...@@ -75,7 +75,7 @@ const strings = {
'nav.rank': 'Rank', 'nav.rank': 'Rank',
'nav.social': 'Social', 'nav.social': 'Social',
'nav.play': 'Play', 'nav.play': 'Play',
'nav.shop': 'Shop', 'nav.tournaments': 'Tournaments',
'nav.profile': 'Profile', 'nav.profile': 'Profile',
'auth.login': 'Login', 'auth.login': 'Login',
'auth.register': 'Register', 'auth.register': 'Register',
......
import * as bus from './bus.js'; import * as bus from './bus.js';
import * as store from './store.js'; import * as store from './store.js';
const worlds = ['rank', 'social', 'play', 'shop', 'profile']; const worlds = ['rank', 'social', 'play', 'tournaments', 'profile'];
const sceneStacks = { rank: [], social: [], play: [], shop: [], profile: [] }; const sceneStacks = { rank: [], social: [], play: [], tournaments: [], profile: [] };
const sceneRegistry = {}; const sceneRegistry = {};
let currentWorld = 'play'; let currentWorld = 'play';
let container = null; let container = null;
......
...@@ -24,7 +24,7 @@ async function boot() { ...@@ -24,7 +24,7 @@ async function boot() {
scene.setRoot('play', 'play-table'); scene.setRoot('play', 'play-table');
scene.setRoot('rank', 'leaderboard'); scene.setRoot('rank', 'leaderboard');
scene.setRoot('social', 'friends'); scene.setRoot('social', 'friends');
scene.setRoot('shop', 'shop-browse'); scene.setRoot('tournaments', 'tournaments-hub');
scene.setRoot('profile', 'profile-view'); scene.setRoot('profile', 'profile-view');
// Check for active match to resume (tab refresh / re-entry recovery) // Check for active match to resume (tab refresh / re-entry recovery)
...@@ -73,7 +73,7 @@ function onAuthSuccess() { ...@@ -73,7 +73,7 @@ function onAuthSuccess() {
scene.setRoot('play', 'play-table'); scene.setRoot('play', 'play-table');
scene.setRoot('rank', 'leaderboard'); scene.setRoot('rank', 'leaderboard');
scene.setRoot('social', 'friends'); scene.setRoot('social', 'friends');
scene.setRoot('shop', 'shop-browse'); scene.setRoot('tournaments', 'tournaments-hub');
scene.setRoot('profile', 'profile-view'); scene.setRoot('profile', 'profile-view');
scene.switchWorld('play'); scene.switchWorld('play');
tournamentSession.init(); tournamentSession.init();
...@@ -101,7 +101,7 @@ async function loadModules() { ...@@ -101,7 +101,7 @@ async function loadModules() {
await import('./modules/ludo/mod.js'); await import('./modules/ludo/mod.js');
await import('./modules/rank/mod.js'); await import('./modules/rank/mod.js');
await import('./modules/social/mod.js'); await import('./modules/social/mod.js');
await import('./modules/shop/mod.js'); await import('./modules/tournaments/mod.js');
await import('./modules/profile/mod.js'); await import('./modules/profile/mod.js');
await import('./modules/rewards/mod.js'); await import('./modules/rewards/mod.js');
await import('./modules/puzzles/mod.js'); await import('./modules/puzzles/mod.js');
......
...@@ -22,18 +22,22 @@ let boardInstance = null; ...@@ -22,18 +22,22 @@ let boardInstance = null;
function loadPieceImages() { function loadPieceImages() {
if (pieceImagesLoaded) return; if (pieceImagesLoaded) return;
pieceImagesLoaded = true; let anyFound = false;
let pending = 0; let pending = 0;
for (const [piece, slot] of Object.entries(PIECE_ASSET_MAP)) { for (const [piece, slot] of Object.entries(PIECE_ASSET_MAP)) {
const url = getAsset(slot); const url = getAsset(slot);
if (url) { if (url) {
anyFound = true;
if (pieceImages[piece]?.src === url) continue;
const img = new Image(); const img = new Image();
pending++; pending++;
img.onload = () => { if (--pending === 0 && boardInstance) boardInstance.render(); }; img.onload = () => { if (--pending === 0 && boardInstance) boardInstance.render(); };
img.onerror = () => { --pending; };
img.src = url; img.src = url;
pieceImages[piece] = img; pieceImages[piece] = img;
} }
} }
if (anyFound) pieceImagesLoaded = true;
} }
const PIECE_PATHS = { const PIECE_PATHS = {
......
import * as scene from '../../core/scene.js';
import { mountTournamentsHub } from './scenes/hub.js';
import { mountTournamentDetail } from './scenes/detail.js';
import { mountTournamentBracket } from './scenes/bracket.js';
import { mountTournamentArena } from './scenes/arena.js';
import { mountTournamentLobby } from './scenes/lobby.js';
import { mountTournamentLive } from './scenes/live.js';
scene.register('tournaments-hub', mountTournamentsHub);
scene.register('tournament-detail', mountTournamentDetail);
scene.register('tournament-bracket', mountTournamentBracket);
scene.register('tournament-arena', mountTournamentArena);
scene.register('tournament-lobby', mountTournamentLobby);
scene.register('tournament-live', mountTournamentLive);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment