Commit 00e13552 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(game): vibration switch in settings

Haptics (feel.haptic) are off when the player turns vibration off; the
setting persists with the rest of the player's preferences.
Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
parent 8822f88d
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
// //
// One global layer, so every screen gets the same feel without wiring it. // One global layer, so every screen gets the same feel without wiring it.
import * as audio from './audio.js'; import * as audio from './audio.js';
import * as store from './store.js';
const PRESSABLE = 'button, .deck-card, .chip, [role="button"], [role="radio"], [role="switch"], [role="tab"], .tab-item, a.btn'; const PRESSABLE = 'button, .deck-card, .chip, [role="button"], [role="radio"], [role="switch"], [role="tab"], .tab-item, a.btn';
const MOVE_CANCEL = 10; // px of travel that turns a press into a scroll const MOVE_CANCEL = 10; // px of travel that turns a press into a scroll
...@@ -21,6 +22,7 @@ let sceneMountedAt = 0; ...@@ -21,6 +22,7 @@ let sceneMountedAt = 0;
/** Haptics: native shell when present, the Vibration API otherwise. */ /** Haptics: native shell when present, the Vibration API otherwise. */
export function haptic(kind = 'light') { export function haptic(kind = 'light') {
if (store.get('hapticsEnabled') === false) return;
const bridge = window.flutter_bridge; const bridge = window.flutter_bridge;
if (bridge?.postMessage) { if (bridge?.postMessage) {
try { bridge.postMessage(JSON.stringify({ cmd: 'haptic', type: kind })); } catch (e) {} try { bridge.postMessage(JSON.stringify({ cmd: 'haptic', type: kind })); } catch (e) {}
......
...@@ -1001,7 +1001,111 @@ const strings = { ...@@ -1001,7 +1001,111 @@ const strings = {
'hud.level_up': 'مستوى جديد!', 'hud.level_up': 'مستوى جديد!',
'hud.level_up_sub': 'وصلت إلى المستوى {n}', 'hud.level_up_sub': 'وصلت إلى المستوى {n}',
'hud.great': 'رائع!', 'hud.great': 'رائع!',
'hud.achievement_unlocked': 'إنجاز جديد!' 'hud.achievement_unlocked': 'إنجاز جديد!',
'chess.reason_checkmate': 'كش مات',
'chess.reason_resign_won': 'خصمك استسلم',
'chess.reason_resign_lost': 'استسلمت',
'chess.reason_timeout_won': 'انتهى وقت خصمك',
'chess.reason_timeout_lost': 'انتهى وقتك',
'chess.reason_abandon_won': 'غادر خصمك المباراة',
'chess.reason_abandon_lost': 'غادرت المباراة',
'chess.reason_stalemate': 'بات: لا توجد نقلة قانونية',
'chess.reason_agreement': 'تعادل بالاتفاق',
'chess.reason_threefold': 'تكرر الوضع ثلاث مرات',
'chess.reason_insufficient': 'القطع لا تكفي لكش مات',
'chess.reason_fifty': 'قاعدة الخمسين نقلة',
'chess.reason_aborted': 'أُلغيت المباراة',
'chess.reason_unknown': 'انتهت المباراة',
'chess.piece_q': 'وزير',
'chess.piece_r': 'طابية',
'chess.piece_b': 'فيل',
'chess.piece_n': 'حصان',
'chess.draw_accepted': 'قبل الخصم التعادل',
'chess.draw_wait': 'انتظر قليلًا قبل عرض تعادل جديد',
'chess.draw_expired': 'لم يعد عرض التعادل قائمًا',
'chess.checking_time': 'نتحقق من الساعة...',
'chess.resign_confirm': 'الاستسلام يعني خسارة المباراة. هل أنت متأكد؟',
'puzzle.find_best': 'جد أفضل نقلة',
'puzzle.streak': '{n} على التوالي',
'puzzle.next': 'الأحجية التالية',
'puzzle.try_hint': 'أُعيدت النقلة، حاول مرة أخرى',
'puzzle.retry': 'حاول مجددًا',
'puzzle.show_solution': 'اعرض الحل',
'analysis.prev': 'النقلة السابقة',
'analysis.next': 'النقلة التالية',
'puzzles.theme_short': 'قصيرة',
'puzzles.theme_long': 'طويلة',
'puzzles.theme_sacrifice': 'تضحية',
'puzzles.theme_fork': 'شوكة',
'puzzles.theme_pin': 'تثبيت',
'puzzles.theme_skewer': 'سيخ',
'puzzles.theme_back-rank': 'الصف الأخير',
'puzzles.theme_discovery': 'هجوم كاشف',
'puzzles.theme_double-check': 'كش مزدوج',
'puzzles.theme_attack': 'هجوم',
'puzzles.theme_defense': 'دفاع',
'puzzles.theme_endgame': 'نهايات',
'puzzles.theme_promotion': 'ترقية',
'puzzles.theme_trapped': 'قطعة محاصرة',
'puzzles.theme_zugzwang': 'زوغزوانغ',
'puzzles.theme_tactics': 'تكتيك',
'puzzles.theme_pawn': 'بيادق',
'puzzles.theme_en-passant': 'الأخذ بالمرور',
'puzzles.theme_rook': 'طابية',
'puzzles.theme_stalemate': 'بات',
'puzzles.theme_opening': 'افتتاحية',
'puzzles.theme_italian': 'الإيطالية',
'puzzles.theme_advance': 'تقدّم',
'puzzles.theme_check': 'كش',
'puzzles.theme_queen': 'الوزير',
'ludo.bonus_roll': 'رمية إضافية!',
'ludo.three_sixes': 'ثلاث ستّات! ضاع الدور',
'ludo.no_move': 'لا حركة متاحة',
'ludo.captured': 'ضربة!',
'ludo.token_home': 'وصلت!',
'ludo.player_done': '{name} في المركز {place}',
'ludo.player_left': '{name} غادر الطاولة',
'ludo.player_left_win': 'خصمك غادر الطاولة، الفوز لك!',
'ludo.player_away': '{name} لا يستجيب، والبوت يلعب عنه',
'ludo.left_tag': 'غادر',
'ludo.home_count': '{n} من 4 وصلت البيت',
'ludo.result_forfeit': 'خصمك غادر الطاولة',
'domino.table': 'طاولة الدومينو',
'domino.your_hand': 'قطعك',
'domino.you_lead': 'أنت تبدأ',
'domino.opp_leads': '{name} يبدأ',
'domino.no_move_pass': 'لا لعب ولا سحب: تمرير',
'domino.no_move_draw': 'لا قطعة تركب، اسحب من المخزن',
'domino.choose_end': 'اختر الطرف',
'domino.domino': 'دومينو!',
'domino.you_can_play': 'عندك قطعة تركب',
'domino.drew_playable': 'هذه تركب!',
'domino.you_passed': 'مرّرت',
'domino.opp_passed': '{name} مرّر',
'domino.opp_domino': '{name} أنهى قطعه!',
'domino.round_tied': 'تعادل في الجولة',
'domino.how_domino_you': 'أنهيت قطعك أولاً',
'domino.how_domino_opp': '{name} أنهى قطعه أولاً',
'domino.how_blocked_tie': 'انغلق اللعب وتساوت النقاط',
'domino.how_blocked': 'انغلق اللعب: الأقل نقاطاً يكسب',
'domino.your_pips': 'نقاط قطعك',
'domino.opp_pips': 'نقاط {name}',
'domino.no_points': 'لا نقاط لأحد',
'domino.points_you': 'لك',
'domino.points_opp': 'لـ{name}',
'domino.see_result': 'النتيجة',
'domino.next_round': 'الجولة التالية',
'domino.waiting_deal': 'في انتظار التوزيع',
'domino.counting': 'نعدّ النقاط',
'domino.time_up': 'انتهى وقتك، لعبنا عنك',
'domino.dealing': 'التوزيع',
'domino.reached_target': 'وصلت إلى {n} أولاً',
'domino.opp_reached_target': '{name} وصل إلى {n} أولاً',
'domino.result_win': 'فزت!',
'domino.result_loss': 'خسرت',
'domino.final_score': 'النتيجة',
'domino.rounds_played': 'الجولات',
'settings.vibration': 'الاهتزاز'
}, },
en: { en: {
'app.name': 'EL3AB', 'app.name': 'EL3AB',
...@@ -2003,7 +2107,111 @@ const strings = { ...@@ -2003,7 +2107,111 @@ const strings = {
'hud.level_up': 'Level up!', 'hud.level_up': 'Level up!',
'hud.level_up_sub': 'You reached level {n}', 'hud.level_up_sub': 'You reached level {n}',
'hud.great': 'Awesome!', 'hud.great': 'Awesome!',
'hud.achievement_unlocked': 'Achievement unlocked!' 'hud.achievement_unlocked': 'Achievement unlocked!',
'chess.reason_checkmate': 'Checkmate',
'chess.reason_resign_won': 'Your opponent resigned',
'chess.reason_resign_lost': 'You resigned',
'chess.reason_timeout_won': 'Your opponent ran out of time',
'chess.reason_timeout_lost': 'You ran out of time',
'chess.reason_abandon_won': 'Your opponent left the game',
'chess.reason_abandon_lost': 'You left the game',
'chess.reason_stalemate': 'Stalemate: no legal move',
'chess.reason_agreement': 'Draw by agreement',
'chess.reason_threefold': 'Threefold repetition',
'chess.reason_insufficient': 'Not enough material to mate',
'chess.reason_fifty': 'Fifty-move rule',
'chess.reason_aborted': 'Game aborted',
'chess.reason_unknown': 'Game over',
'chess.piece_q': 'Queen',
'chess.piece_r': 'Rook',
'chess.piece_b': 'Bishop',
'chess.piece_n': 'Knight',
'chess.draw_accepted': 'Draw accepted',
'chess.draw_wait': 'Wait a moment before offering again',
'chess.draw_expired': 'That draw offer is no longer open',
'chess.checking_time': 'Checking the clock...',
'chess.resign_confirm': 'Resigning loses the game. Are you sure?',
'puzzle.find_best': 'Find the best move',
'puzzle.streak': '{n} in a row',
'puzzle.next': 'Next puzzle',
'puzzle.try_hint': 'The move was taken back. Try again',
'puzzle.retry': 'Try again',
'puzzle.show_solution': 'Show the answer',
'analysis.prev': 'Previous move',
'analysis.next': 'Next move',
'puzzles.theme_short': 'Short',
'puzzles.theme_long': 'Long',
'puzzles.theme_sacrifice': 'Sacrifice',
'puzzles.theme_fork': 'Fork',
'puzzles.theme_pin': 'Pin',
'puzzles.theme_skewer': 'Skewer',
'puzzles.theme_back-rank': 'Back rank',
'puzzles.theme_discovery': 'Discovered attack',
'puzzles.theme_double-check': 'Double check',
'puzzles.theme_attack': 'Attack',
'puzzles.theme_defense': 'Defense',
'puzzles.theme_endgame': 'Endgame',
'puzzles.theme_promotion': 'Promotion',
'puzzles.theme_trapped': 'Trapped piece',
'puzzles.theme_zugzwang': 'Zugzwang',
'puzzles.theme_tactics': 'Tactics',
'puzzles.theme_pawn': 'Pawns',
'puzzles.theme_en-passant': 'En passant',
'puzzles.theme_rook': 'Rook',
'puzzles.theme_stalemate': 'Stalemate',
'puzzles.theme_opening': 'Opening',
'puzzles.theme_italian': 'Italian',
'puzzles.theme_advance': 'Advance',
'puzzles.theme_check': 'Check',
'puzzles.theme_queen': 'Queen',
'ludo.bonus_roll': 'Bonus roll!',
'ludo.three_sixes': 'Three sixes! Turn lost',
'ludo.no_move': 'No move',
'ludo.captured': 'Hit!',
'ludo.token_home': 'Home!',
'ludo.player_done': '{name} takes {place} place',
'ludo.player_left': '{name} left the table',
'ludo.player_left_win': 'Your opponent left. The win is yours!',
'ludo.player_away': '{name} isn\'t responding. A bot is playing for them.',
'ludo.left_tag': 'Left',
'ludo.home_count': '{n} of 4 home',
'ludo.result_forfeit': 'Your opponent left the table',
'domino.table': 'Domino table',
'domino.your_hand': 'Your tiles',
'domino.you_lead': 'You lead',
'domino.opp_leads': '{name} leads',
'domino.no_move_pass': 'No move and nothing to draw: pass',
'domino.no_move_draw': 'Nothing fits: draw from the boneyard',
'domino.choose_end': 'Choose an end',
'domino.domino': 'Domino!',
'domino.you_can_play': 'You have a tile that fits',
'domino.drew_playable': 'This one fits!',
'domino.you_passed': 'You passed',
'domino.opp_passed': '{name} passed',
'domino.opp_domino': '{name} is out!',
'domino.round_tied': 'Round tied',
'domino.how_domino_you': 'You went out first',
'domino.how_domino_opp': '{name} went out first',
'domino.how_blocked_tie': 'Blocked, and the hands are level',
'domino.how_blocked': 'Blocked: the lighter hand wins',
'domino.your_pips': 'Your tiles',
'domino.opp_pips': '{name}\'s tiles',
'domino.no_points': 'No points',
'domino.points_you': 'To you',
'domino.points_opp': 'To {name}',
'domino.see_result': 'See the result',
'domino.next_round': 'Next round',
'domino.waiting_deal': 'Waiting for the deal',
'domino.counting': 'Counting the tiles',
'domino.time_up': 'Time\'s up: played for you',
'domino.dealing': 'Dealing',
'domino.reached_target': 'You reached {n} first',
'domino.opp_reached_target': '{name} reached {n} first',
'domino.result_win': 'You won!',
'domino.result_loss': 'You lost',
'domino.final_score': 'Score',
'domino.rounds_played': 'Rounds',
'settings.vibration': 'Vibration'
} }
}; };
......
...@@ -11,6 +11,7 @@ const defaultState = { ...@@ -11,6 +11,7 @@ const defaultState = {
features: {}, features: {},
activeWorld: 'play', activeWorld: 'play',
audioEnabled: true, audioEnabled: true,
hapticsEnabled: true,
language: 'ar' language: 'ar'
}; };
...@@ -33,6 +34,7 @@ function persist() { ...@@ -33,6 +34,7 @@ function persist() {
player: state.player, player: state.player,
activeWorld: state.activeWorld, activeWorld: state.activeWorld,
audioEnabled: state.audioEnabled, audioEnabled: state.audioEnabled,
hapticsEnabled: state.hapticsEnabled,
language: state.language, language: state.language,
home: state.home home: state.home
}; };
......
...@@ -14,6 +14,7 @@ import { screenHead, wireBack } from '../../../core/ui.js'; ...@@ -14,6 +14,7 @@ import { screenHead, wireBack } from '../../../core/ui.js';
export function mountSettings(el) { export function mountSettings(el) {
const lang = store.get('language') || 'ar'; const lang = store.get('language') || 'ar';
const audioOn = !!store.get('audioEnabled'); const audioOn = !!store.get('audioEnabled');
const hapticsOn = store.get('hapticsEnabled') !== false;
el.innerHTML = ` el.innerHTML = `
<div class="screen st"> <div class="screen st">
...@@ -28,6 +29,13 @@ export function mountSettings(el) { ...@@ -28,6 +29,13 @@ export function mountSettings(el) {
<span class="st-knob"></span> <span class="st-knob"></span>
</button> </button>
</div> </div>
<div class="row st-row">
<span class="st-tile">${gicon('hand', 34)}</span>
<span class="row-main"><span class="row-title" id="st-haptic-label">${t('settings.vibration')}</span></span>
<button class="st-switch" id="toggle-haptics" role="switch" aria-checked="${hapticsOn}" aria-labelledby="st-haptic-label">
<span class="st-knob"></span>
</button>
</div>
<button class="row st-row st-btn" id="toggle-lang"> <button class="row st-row st-btn" id="toggle-lang">
<span class="st-tile">${gicon('chat', 34)}</span> <span class="st-tile">${gicon('chat', 34)}</span>
<span class="row-main"><span class="row-title">${t('settings.language')}</span></span> <span class="row-main"><span class="row-title">${t('settings.language')}</span></span>
...@@ -121,6 +129,13 @@ export function mountSettings(el) { ...@@ -121,6 +129,13 @@ export function mountSettings(el) {
el.querySelector('#toggle-audio')?.focus(); el.querySelector('#toggle-audio')?.focus();
}); });
el.querySelector('#toggle-haptics').addEventListener('click', () => {
store.set('hapticsEnabled', store.get('hapticsEnabled') === false);
if (navigator.vibrate && store.get('hapticsEnabled')) navigator.vibrate(20);
mountSettings(el);
el.querySelector('#toggle-haptics')?.focus();
});
el.querySelector('#toggle-lang').addEventListener('click', () => { el.querySelector('#toggle-lang').addEventListener('click', () => {
const current = store.get('language') || 'ar'; const current = store.get('language') || 'ar';
const next = current === 'ar' ? 'en' : 'ar'; const next = current === 'ar' ? 'en' : 'ar';
......
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