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 { getAsset } from './theme.js';
let ctx = null;
const buffers = {};
const audioElements = {};
const volumes = { ui: 0.5, game: 0.7, reward: 0.8 };
const sounds = {
......@@ -17,7 +19,9 @@ const sounds = {
levelUp: { src: null, freq: 880, dur: 0.6 },
notification: { src: null, freq: 700, dur: 0.15 },
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() {
......@@ -30,6 +34,7 @@ function getCtx() {
export function init() {
document.addEventListener('touchstart', resume, { once: true });
document.addEventListener('click', resume, { once: true });
preloadCustomAudio();
}
function resume() {
......@@ -37,8 +42,31 @@ function 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') {
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];
if (!sound) return;
......
......@@ -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 * as emoteSystem from '../../chess/components/emotes.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 net from '../../../core/net.js';
import * as modal from '../../../core/modal.js';
......@@ -24,9 +24,21 @@ let animFrame = null; // rAF handle
let pieceAnims = new Map(); // pieceId -> {fromX,fromY,toX,toY,t,duration}
// Order: Red(BL), Green(TL), Yellow(TR), Blue(BR) — standard Ludo layout
const COLORS = ['#E53935', '#43A047', '#FDD835', '#1E88E5'];
const COLORS_LIGHT = ['#EF9A9A', '#A5D6A7', '#FFF59D', '#90CAF9'];
const DEFAULT_COLORS = ['#E53935', '#43A047', '#FDD835', '#1E88E5'];
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'];
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 myPlayerIndex = 0;
let matchId = null;
......@@ -149,6 +161,27 @@ export function mountGame(el, params) {
boardDirty = true;
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);
for (let i = 0; i < 4; i++) {
const md = el.querySelector(`#dice-${i}`);
......@@ -966,7 +999,7 @@ function drawPieces(cs) {
const bounceY = piece._bounceOffset || 0;
const drawY = oy + bounceY;
// Highlight ring
// Highlight ring (same for sprite or canvas pawn)
if (isHighlighted) {
ctx.beginPath(); ctx.arc(ox, drawY, r + 3, 0, Math.PI*2);
ctx.strokeStyle = '#E4AC38'; ctx.lineWidth = 2.5; ctx.stroke();
......@@ -975,15 +1008,19 @@ function drawPieces(cs) {
}
// Shadow
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();
// Base
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();
// Border
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();
// Highlight dot
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();
const spriteImg = pawnImages[pIdx];
if (spriteImg) {
const size = r * 2;
ctx.drawImage(spriteImg, ox - size/2, drawY - size/2, size, size);
} else {
// Fallback: canvas-drawn pawn
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.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