Commit 43bfe37d authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: modal stacking, scene unmount, chat scroll, shop gems, ludo/backgammon bugs

- modal.js: dismiss existing modal before showing new one (prevents stacking)
- scene.js: call unmount on current scene during replace() and switchWorld()
- chat.js: remove || true from scroll condition (only auto-scroll on new msgs)
- shop/browse.js: check gems affordability alongside coins
- ludo/game.js: fix undefined isLoser — use isLastPlace
- backgammon/rules.js: fix VARIANTS.standard crash — use VARIANTS.sheshbesh
- backgammon/game.js: sanitize opponentName and avatar URL (XSS prevention)

Fixes WTF #22, #25, #26, #59, #68, #71, #113
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 7f51492e
...@@ -7,6 +7,7 @@ import * as juice from './juice.js'; ...@@ -7,6 +7,7 @@ import * as juice from './juice.js';
let modalEl = null; let modalEl = null;
let backdropEl = null; let backdropEl = null;
let resolvePromise = null; let resolvePromise = null;
let modalQueue = [];
function ensureContainer() { function ensureContainer() {
if (backdropEl) return; if (backdropEl) return;
...@@ -77,6 +78,12 @@ function animateOut() { ...@@ -77,6 +78,12 @@ function animateOut() {
* Returns a Promise<boolean> * Returns a Promise<boolean>
*/ */
export function confirm(message, options = {}) { export function confirm(message, options = {}) {
// If a modal is already open, dismiss it (resolve false) before showing new one
if (isOpen() && resolvePromise) {
resolvePromise(false);
resolvePromise = null;
}
const { const {
title = '', title = '',
confirmText = 'تأكيد', confirmText = 'تأكيد',
......
...@@ -29,6 +29,14 @@ export function register(id, mountFn, unmountFn) { ...@@ -29,6 +29,14 @@ export function register(id, mountFn, unmountFn) {
export function switchWorld(world, resetToRoot = false) { export function switchWorld(world, resetToRoot = false) {
if (!worlds.includes(world)) return; if (!worlds.includes(world)) return;
if (world === currentWorld && !resetToRoot) return; if (world === currentWorld && !resetToRoot) return;
// Unmount current scene before switching
const prevStack = sceneStacks[currentWorld];
const prevScene = prevStack[prevStack.length - 1];
if (prevScene && sceneRegistry[prevScene.id]?.unmount) {
sceneRegistry[prevScene.id].unmount();
}
const prev = currentWorld; const prev = currentWorld;
currentWorld = world; currentWorld = world;
store.set('activeWorld', world); store.set('activeWorld', world);
...@@ -66,6 +74,10 @@ export function pop() { ...@@ -66,6 +74,10 @@ export function pop() {
export function replace(sceneId, params = {}) { export function replace(sceneId, params = {}) {
const stack = sceneStacks[currentWorld]; const stack = sceneStacks[currentWorld];
const prev = stack[stack.length - 1];
if (prev && sceneRegistry[prev.id]?.unmount) {
sceneRegistry[prev.id].unmount();
}
stack[stack.length - 1] = { id: sceneId, params }; stack[stack.length - 1] = { id: sceneId, params };
renderScene({ id: sceneId, params }); renderScene({ id: sceneId, params });
} }
......
...@@ -254,7 +254,7 @@ export function hasWon(state, type) { ...@@ -254,7 +254,7 @@ export function hasWon(state, type) {
export function getGameScore(state, winner, rule) { export function getGameScore(state, winner, rule) {
const opp = winner === WHITE ? BLACK : WHITE; const opp = winner === WHITE ? BLACK : WHITE;
if (state.outside[opp].length === 0) { if (state.outside[opp].length === 0) {
const r = rule || VARIANTS.standard; const r = rule || VARIANTS.sheshbesh;
if (countAtHigherPos(state, 18, opp, r) > 0 || havePiecesOnBar(state, opp)) { if (countAtHigherPos(state, 18, opp, r) > 0 || havePiecesOnBar(state, opp)) {
return 3; // backgammon return 3; // backgammon
} }
......
...@@ -45,9 +45,11 @@ export function mountGame(el, p) { ...@@ -45,9 +45,11 @@ export function mountGame(el, p) {
: emoji('person', '👤', 18); : emoji('person', '👤', 18);
const myLevel = player.level || 1; const myLevel = player.level || 1;
const oppName = mode === 'bot' ? 'بوت' : (params.opponentName || 'خصم'); const rawOppName = mode === 'bot' ? 'بوت' : (params.opponentName || 'خصم');
const oppAvatar = params.opponentAvatar const oppName = rawOppName.replace(/[<>"&]/g, c => ({'<':'&lt;','>':'&gt;','"':'&quot;','&':'&amp;'}[c]));
? `<img src="${params.opponentAvatar}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">` const oppAvatarUrl = params.opponentAvatar ? String(params.opponentAvatar).replace(/[<>"]/g, '') : '';
const oppAvatar = oppAvatarUrl
? `<img src="${oppAvatarUrl}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">`
: (mode === 'bot' ? '🤖' : emoji('person', '👤', 18)); : (mode === 'bot' ? '🤖' : emoji('person', '👤', 18));
const oppLevel = params.opponentLevel || (mode === 'bot' ? '—' : 1); const oppLevel = params.opponentLevel || (mode === 'bot' ? '—' : 1);
......
...@@ -1439,7 +1439,7 @@ function endGame(el) { ...@@ -1439,7 +1439,7 @@ function endGame(el) {
}).catch(() => {}); }).catch(() => {});
} }
if (!isLoser) { if (!isLastPlace) {
juice.confetti(window.innerWidth / 2, window.innerHeight / 3, 50); juice.confetti(window.innerWidth / 2, window.innerHeight / 3, 50);
juice.starBurst(window.innerWidth / 2, window.innerHeight / 3, 15); juice.starBurst(window.innerWidth / 2, window.innerHeight / 3, 15);
juice.hapticSuccess(); juice.hapticSuccess();
......
...@@ -61,7 +61,9 @@ function renderItems(el, items) { ...@@ -61,7 +61,9 @@ function renderItems(el, items) {
async function purchasePrompt(el, item) { async function purchasePrompt(el, item) {
const player = store.get('player'); const player = store.get('player');
const canAfford = (player?.coins || 0) >= (item.price_coins || 0); const canAffordCoins = (player?.coins || 0) >= (item.price_coins || 0);
const canAffordGems = (item.price_gems || 0) === 0 || (player?.gems || 0) >= (item.price_gems || 0);
const canAfford = canAffordCoins && canAffordGems;
const overlay = document.getElementById('overlay'); const overlay = document.getElementById('overlay');
overlay.classList.add('active'); overlay.classList.add('active');
......
...@@ -243,7 +243,7 @@ function renderMessages(el, scrollToBottom = false) { ...@@ -243,7 +243,7 @@ function renderMessages(el, scrollToBottom = false) {
container.innerHTML = html; container.innerHTML = html;
if (scrollToBottom || true) { if (scrollToBottom) {
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
} }
} }
......
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