Commit 9964877a authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: add Ludo pawn sprites, sound effects & 25 emoji slots to branding

- Add 4 Ludo pawn sprite upload slots (red/green/yellow/blue) to admin branding
- Add 15 sound effect upload slots (mp3/ogg/wav) with audio preview
- Add 25 missing emoji slots (crown, mute, block, hourglass, etc.)
- Ludo game.js: draw custom pawn sprites with same bounce/highlight animation
- Ludo game.js: pawn colors now respect branding ludo_red/green/yellow/blue
- audio.js: plays custom audio files if uploaded, falls back to synthesized sounds
- branding.php: support audio MIME types for Supabase storage upload
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent d84fed77
This diff is collapsed.
import * as store from './store.js'; import * as store from './store.js';
import { getAsset } from './theme.js';
let ctx = null; let ctx = null;
const buffers = {}; const buffers = {};
const audioElements = {};
const volumes = { ui: 0.5, game: 0.7, reward: 0.8 }; const volumes = { ui: 0.5, game: 0.7, reward: 0.8 };
const sounds = { const sounds = {
...@@ -17,7 +19,9 @@ const sounds = { ...@@ -17,7 +19,9 @@ const sounds = {
levelUp: { src: null, freq: 880, dur: 0.6 }, levelUp: { src: null, freq: 880, dur: 0.6 },
notification: { src: null, freq: 700, dur: 0.15 }, notification: { src: null, freq: 700, dur: 0.15 },
dice: { src: null, freq: 250, dur: 0.3 }, dice: { src: null, freq: 250, dur: 0.3 },
place: { src: null, freq: 350, dur: 0.1 } place: { src: null, freq: 350, dur: 0.1 },
reward: { src: null, freq: 660, dur: 0.4 },
match_found: { src: null, freq: 740, dur: 0.3 }
}; };
function getCtx() { function getCtx() {
...@@ -30,6 +34,7 @@ function getCtx() { ...@@ -30,6 +34,7 @@ function getCtx() {
export function init() { export function init() {
document.addEventListener('touchstart', resume, { once: true }); document.addEventListener('touchstart', resume, { once: true });
document.addEventListener('click', resume, { once: true }); document.addEventListener('click', resume, { once: true });
preloadCustomAudio();
} }
function resume() { function resume() {
...@@ -37,8 +42,31 @@ function resume() { ...@@ -37,8 +42,31 @@ function resume() {
if (c.state === 'suspended') c.resume(); if (c.state === 'suspended') c.resume();
} }
function preloadCustomAudio() {
for (const name of Object.keys(sounds)) {
const url = getAsset('sfx_' + name);
if (url) {
const audio = new Audio();
audio.preload = 'auto';
audio.src = url;
audioElements[name] = audio;
}
}
}
export function play(name, category = 'ui') { export function play(name, category = 'ui') {
if (!store.get('audioEnabled')) return; if (!store.get('audioEnabled')) return;
// Use custom audio file if uploaded via branding
if (audioElements[name]) {
const el = audioElements[name];
el.volume = volumes[category] || 0.5;
el.currentTime = 0;
el.play().catch(() => {});
return;
}
// Fallback: synthesized sound
const sound = sounds[name]; const sound = sounds[name];
if (!sound) return; if (!sound) return;
......
...@@ -9,7 +9,7 @@ import * as juice from '../../../core/juice.js'; ...@@ -9,7 +9,7 @@ import * as juice from '../../../core/juice.js';
import { getPiecePosition, getHomeBasePosition, SAFE_SQUARES, HOME_COLUMNS, SHARED_PATH } from '../logic/board-map.js'; import { getPiecePosition, getHomeBasePosition, SAFE_SQUARES, HOME_COLUMNS, SHARED_PATH } from '../logic/board-map.js';
import * as emoteSystem from '../../chess/components/emotes.js'; import * as emoteSystem from '../../chess/components/emotes.js';
import * as mp from '../../../core/multiplayer.js'; import * as mp from '../../../core/multiplayer.js';
import { emoji, getAsset } from '../../../core/theme.js'; import { emoji, getAsset, getColor } from '../../../core/theme.js';
import * as matchLive from '../../../core/match-live.js'; import * as matchLive from '../../../core/match-live.js';
import * as net from '../../../core/net.js'; import * as net from '../../../core/net.js';
import * as modal from '../../../core/modal.js'; import * as modal from '../../../core/modal.js';
...@@ -24,9 +24,21 @@ let animFrame = null; // rAF handle ...@@ -24,9 +24,21 @@ let animFrame = null; // rAF handle
let pieceAnims = new Map(); // pieceId -> {fromX,fromY,toX,toY,t,duration} let pieceAnims = new Map(); // pieceId -> {fromX,fromY,toX,toY,t,duration}
// Order: Red(BL), Green(TL), Yellow(TR), Blue(BR) — standard Ludo layout // Order: Red(BL), Green(TL), Yellow(TR), Blue(BR) — standard Ludo layout
const COLORS = ['#E53935', '#43A047', '#FDD835', '#1E88E5']; const DEFAULT_COLORS = ['#E53935', '#43A047', '#FDD835', '#1E88E5'];
const COLORS_LIGHT = ['#EF9A9A', '#A5D6A7', '#FFF59D', '#90CAF9']; const DEFAULT_COLORS_LIGHT = ['#EF9A9A', '#A5D6A7', '#FFF59D', '#90CAF9'];
let COLORS = DEFAULT_COLORS;
let COLORS_LIGHT = DEFAULT_COLORS_LIGHT;
const PAWN_SLOTS = ['ludo_pawn_red', 'ludo_pawn_green', 'ludo_pawn_yellow', 'ludo_pawn_blue'];
const pawnImages = [null, null, null, null];
let PLAYER_NAMES = ['أنت', 'Bot 1', 'Bot 2', 'Bot 3']; let PLAYER_NAMES = ['أنت', 'Bot 1', 'Bot 2', 'Bot 3'];
function lightenColor(hex, amount) {
const num = parseInt(hex.replace('#', ''), 16);
const r = Math.min(255, ((num >> 16) & 0xFF) + Math.round(255 * amount));
const g = Math.min(255, ((num >> 8) & 0xFF) + Math.round(255 * amount));
const b = Math.min(255, (num & 0xFF) + Math.round(255 * amount));
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
}
let livePoller = null; let livePoller = null;
let myPlayerIndex = 0; let myPlayerIndex = 0;
let matchId = null; let matchId = null;
...@@ -149,6 +161,27 @@ export function mountGame(el, params) { ...@@ -149,6 +161,27 @@ export function mountGame(el, params) {
boardDirty = true; boardDirty = true;
pieceAnims = new Map(); pieceAnims = new Map();
// Load themed pawn colors
COLORS = [
getColor('ludo_red', DEFAULT_COLORS[0]),
getColor('ludo_green', DEFAULT_COLORS[1]),
getColor('ludo_yellow', DEFAULT_COLORS[2]),
getColor('ludo_blue', DEFAULT_COLORS[3]),
];
COLORS_LIGHT = COLORS.map(c => lightenColor(c, 0.4));
// Preload custom pawn sprites (non-blocking)
PAWN_SLOTS.forEach((slot, i) => {
const url = getAsset(slot);
if (url) {
const img = new Image();
img.src = url;
img.onload = () => { pawnImages[i] = img; };
} else {
pawnImages[i] = null;
}
});
renderDiceFace(el.querySelector('#dice-box'), 1); renderDiceFace(el.querySelector('#dice-box'), 1);
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
const md = el.querySelector(`#dice-${i}`); const md = el.querySelector(`#dice-${i}`);
...@@ -966,7 +999,7 @@ function drawPieces(cs) { ...@@ -966,7 +999,7 @@ function drawPieces(cs) {
const bounceY = piece._bounceOffset || 0; const bounceY = piece._bounceOffset || 0;
const drawY = oy + bounceY; const drawY = oy + bounceY;
// Highlight ring // Highlight ring (same for sprite or canvas pawn)
if (isHighlighted) { if (isHighlighted) {
ctx.beginPath(); ctx.arc(ox, drawY, r + 3, 0, Math.PI*2); ctx.beginPath(); ctx.arc(ox, drawY, r + 3, 0, Math.PI*2);
ctx.strokeStyle = '#E4AC38'; ctx.lineWidth = 2.5; ctx.stroke(); ctx.strokeStyle = '#E4AC38'; ctx.lineWidth = 2.5; ctx.stroke();
...@@ -975,15 +1008,19 @@ function drawPieces(cs) { ...@@ -975,15 +1008,19 @@ function drawPieces(cs) {
} }
// Shadow // Shadow
ctx.beginPath(); ctx.arc(ox, oy + 2, r, 0, Math.PI*2); ctx.fillStyle = 'rgba(0,0,0,0.2)'; ctx.fill(); ctx.beginPath(); ctx.arc(ox, oy + 2, r, 0, Math.PI*2); ctx.fillStyle = 'rgba(0,0,0,0.2)'; ctx.fill();
// Body
ctx.beginPath(); ctx.arc(ox, drawY - r*0.15, r*0.65, 0, Math.PI*2); ctx.fillStyle = COLORS[pIdx]; ctx.fill(); const spriteImg = pawnImages[pIdx];
// Base if (spriteImg) {
ctx.beginPath(); ctx.ellipse(ox, drawY + r*0.4, r*0.8, r*0.4, 0, 0, Math.PI*2); ctx.fillStyle = COLORS[pIdx]; ctx.fill(); const size = r * 2;
// Border ctx.drawImage(spriteImg, ox - size/2, drawY - size/2, size, size);
ctx.beginPath(); ctx.arc(ox, drawY - r*0.15, r*0.65, 0, Math.PI*2); ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke(); } else {
ctx.beginPath(); ctx.ellipse(ox, drawY + r*0.4, r*0.8, r*0.4, 0, 0, Math.PI*2); ctx.strokeStyle = '#fff'; ctx.lineWidth = 1; ctx.stroke(); // Fallback: canvas-drawn pawn
// Highlight dot ctx.beginPath(); ctx.arc(ox, drawY - r*0.15, r*0.65, 0, Math.PI*2); ctx.fillStyle = COLORS[pIdx]; ctx.fill();
ctx.beginPath(); ctx.arc(ox - r*0.2, drawY - r*0.35, r*0.18, 0, Math.PI*2); ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.fill(); ctx.beginPath(); ctx.ellipse(ox, drawY + r*0.4, r*0.8, r*0.4, 0, 0, Math.PI*2); ctx.fillStyle = COLORS[pIdx]; ctx.fill();
ctx.beginPath(); ctx.arc(ox, drawY - r*0.15, r*0.65, 0, Math.PI*2); ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke();
ctx.beginPath(); ctx.ellipse(ox, drawY + r*0.4, r*0.8, r*0.4, 0, 0, Math.PI*2); ctx.strokeStyle = '#fff'; ctx.lineWidth = 1; ctx.stroke();
ctx.beginPath(); ctx.arc(ox - r*0.2, drawY - r*0.35, r*0.18, 0, Math.PI*2); ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.fill();
}
}); });
}); });
} }
......
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