feat: add sounds
This commit is contained in:
@@ -4,13 +4,14 @@ import 'dart:math' as math;
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/effects.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_audio/flame_audio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../../core/service/app_sounds.dart';
|
||||
import '../../../domain/entities/game_entities.dart';
|
||||
import '../../bloc/game_bloc.dart';
|
||||
import 'card_codes.dart';
|
||||
import 'card_component.dart';
|
||||
import 'shuffle_animation.dart';
|
||||
import 'table_pieces.dart';
|
||||
|
||||
/// صحنهی میز حکم با انیمیشن. شما همیشه پایین میز (rel=0) هستید.
|
||||
@@ -42,6 +43,9 @@ class HokmGame extends FlameGame {
|
||||
double _t = 0;
|
||||
List<String> _prevTrick = const [];
|
||||
int _prevHandSize = 0; // برای تشخیصِ پخشِ کارت (دستِ جدید) جهت صدای بُر زدن
|
||||
bool _silent = false; // هنگام خروج از میز، دیگر صدایی پخش نشود
|
||||
bool _introActive = true; // تا پایانِ نمایشِ «جستجوی حریف»
|
||||
bool _pendingShuffle = false; // بُرِ دستِ اول که تا پایانِ اینترو به تعویق افتاده
|
||||
|
||||
HokmGame(this.cubit);
|
||||
|
||||
@@ -51,14 +55,20 @@ class HokmGame extends FlameGame {
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
add(Felt());
|
||||
// پیشبارگذاریِ صدای بُر زدن (در صورت نبودِ فایل، بیصدا رد میشود).
|
||||
FlameAudio.audioCache.load('shuffle.mp3').catchError((_) => Uri());
|
||||
_sub = cubit.stream.listen((ui) {
|
||||
if (ui.state == null) return;
|
||||
final s = ui.state!;
|
||||
_checkCut(s);
|
||||
// شروعِ دستِ جدید (پخشِ کارت): صدای بُر زدن.
|
||||
if (_prevHandSize == 0 && s.yourHand.isNotEmpty) _playShuffle();
|
||||
_checkTrickSounds(s);
|
||||
// شروعِ دستِ جدید (پخشِ کارت): انیمیشن و صدای بُر زدن.
|
||||
// دستِ اول تا پایانِ نمایشِ «جستجوی حریف» به تعویق میافتد تا انیمیشن
|
||||
// زیرِ اوورلی پنهان نشود و صدا با تصویر هماهنگ باشد.
|
||||
if (_prevHandSize == 0 && s.yourHand.isNotEmpty && !_silent) {
|
||||
if (_introActive) {
|
||||
_pendingShuffle = true;
|
||||
} else {
|
||||
_shuffleNow();
|
||||
}
|
||||
}
|
||||
_prevHandSize = s.yourHand.length;
|
||||
_s = s;
|
||||
_relayout();
|
||||
@@ -71,11 +81,6 @@ class HokmGame extends FlameGame {
|
||||
}
|
||||
}
|
||||
|
||||
void _playShuffle() {
|
||||
// اگر فایل صدا نباشد، خطا نادیده گرفته میشود (بازی بیصدا).
|
||||
FlameAudio.play('shuffle.mp3', volume: 0.7).then((_) {}, onError: (_) {});
|
||||
}
|
||||
|
||||
@override
|
||||
void onGameResize(Vector2 size) {
|
||||
super.onGameResize(size);
|
||||
@@ -88,24 +93,57 @@ class HokmGame extends FlameGame {
|
||||
super.onRemove();
|
||||
}
|
||||
|
||||
// ===== افکتِ بریدن با حکم (تکان + رعد) =====
|
||||
// ===== صداها و افکتِ بریدن با حکم (تکان + رعد) =====
|
||||
|
||||
// اگر دقیقاً یک کارتِ تازه به زمین اضافه شده و آن کارت «بُرِش با حکم» باشد
|
||||
// (خالِ زمینه آتو نیست ولی کارتِ تازه آتوست)، همان لحظه تکان + رعد.
|
||||
void _checkCut(GameState s) {
|
||||
// با هر تغییرِ زمین: افزودنِ کارت ⇒ صدای گذاشتن/انداختن (و بریدن با حکم ⇒
|
||||
// شمشیر + تکان + رعد)؛ خالیشدنِ زمین ⇒ صدای جمعشدنِ دست.
|
||||
void _checkTrickSounds(GameState s) {
|
||||
final now = s.trick.map((t) => t.card).toList();
|
||||
final addedOne = now.length == _prevTrick.length + 1 && _isPrefix(_prevTrick, now);
|
||||
if (addedOne &&
|
||||
s.trump != null &&
|
||||
s.leadSuit != null &&
|
||||
s.leadSuit != s.trump &&
|
||||
suitName(now.last) == s.trump) {
|
||||
_shake = 1.0;
|
||||
add(Lightning());
|
||||
if (_silent) {
|
||||
_prevTrick = now;
|
||||
return;
|
||||
}
|
||||
final addedOne =
|
||||
now.length == _prevTrick.length + 1 && _isPrefix(_prevTrick, now);
|
||||
if (addedOne) {
|
||||
final card = s.trick.last;
|
||||
final isCut = s.trump != null &&
|
||||
s.leadSuit != null &&
|
||||
s.leadSuit != s.trump &&
|
||||
suitName(card.card) == s.trump;
|
||||
if (isCut) {
|
||||
_shake = 1.0;
|
||||
add(Lightning());
|
||||
AppSounds.cut(); // بریدن با حکم (شمشیر)
|
||||
} else {
|
||||
AppSounds.placeCard(); // کارت روی میز نشست (همهی بازیکنان)
|
||||
}
|
||||
} else if (now.isEmpty && _prevTrick.isNotEmpty) {
|
||||
AppSounds.takeCard(); // دستِ کامل جمع شد
|
||||
}
|
||||
_prevTrick = now;
|
||||
}
|
||||
|
||||
/// قطعِ صداهای بازی هنگام خروج از میز (جلوگیری از پخشِ صدای کارت پس از ترک).
|
||||
void silence() => _silent = true;
|
||||
|
||||
/// پایانِ نمایشِ «جستجوی حریف»؛ اگر بُرِ دستِ اول به تعویق افتاده بود، اکنون
|
||||
/// (که اوورلی کنار رفته) با صدا و تصویرِ هماهنگ اجرا میشود.
|
||||
void endIntro() {
|
||||
_introActive = false;
|
||||
if (_pendingShuffle && !_silent) {
|
||||
_pendingShuffle = false;
|
||||
_shuffleNow();
|
||||
}
|
||||
}
|
||||
|
||||
/// اجرای همزمانِ انیمیشن و صدای بُر زدن (صدا با پایانِ انیمیشن قطع میشود).
|
||||
void _shuffleNow() {
|
||||
if (_silent) return;
|
||||
AppSounds.shuffle(cutoffMs: ShuffleAnimation.totalMs);
|
||||
add(ShuffleAnimation(size));
|
||||
}
|
||||
|
||||
static bool _isPrefix(List<String> a, List<String> b) {
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
if (a[i] != b[i]) return false;
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/effects.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// انیمیشنِ بُر زدن: دو دسته کارتِ پشترو در مرکز ریفل میشوند، کمی بالا/پایین
|
||||
/// میروند (بُر) و سپس به سمت چهار بازیکن پخش میشوند. مدتِ کل [totalMs] است
|
||||
/// تا صدای بُر زدن دقیقاً همزمان با پایانِ انیمیشن قطع شود.
|
||||
class ShuffleAnimation extends PositionComponent {
|
||||
final Vector2 area;
|
||||
ShuffleAnimation(this.area);
|
||||
|
||||
/// مدتِ کلِ انیمیشن (مرجعِ هماهنگیِ صدا).
|
||||
static const int totalMs = 2000;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
priority = 6000;
|
||||
final cw = area.x * 0.16;
|
||||
final ch = cw * 1.45;
|
||||
final center = Vector2(area.x / 2, area.y / 2);
|
||||
const n = 10;
|
||||
|
||||
// جهتِ پخش به سمتِ ۴ بازیکن (پایین، چپ، بالا، راست).
|
||||
final dirs = [
|
||||
Vector2(0, area.y * 0.42),
|
||||
Vector2(-area.x * 0.42, 0),
|
||||
Vector2(0, -area.y * 0.42),
|
||||
Vector2(area.x * 0.42, 0),
|
||||
];
|
||||
|
||||
for (var i = 0; i < n; i++) {
|
||||
final left = i.isEven;
|
||||
final stack = center + Vector2((i - n / 2) * cw * 0.05, 0);
|
||||
final delay = i * 0.02; // ریفلِ پلکانی
|
||||
|
||||
final card = _Back(Vector2(cw, ch))
|
||||
..anchor = Anchor.center
|
||||
..position = center + Vector2((left ? -1 : 1) * cw, 0)
|
||||
..angle = (left ? -1 : 1) * 0.22;
|
||||
add(card);
|
||||
|
||||
// صافشدنِ زاویه همزمان با ریفل.
|
||||
card.add(RotateEffect.to(
|
||||
0, EffectController(duration: 0.55, startDelay: delay)));
|
||||
|
||||
// دنباله: ریفل به مرکز ← بالا (بُر) ← نشستن ← پخش به سمت بازیکن.
|
||||
card.add(SequenceEffect([
|
||||
MoveToEffect(stack,
|
||||
EffectController(duration: 0.55, curve: Curves.easeOut, startDelay: delay)),
|
||||
MoveToEffect(stack + Vector2(0, -ch * 0.18),
|
||||
EffectController(duration: 0.35, curve: Curves.easeInOut)),
|
||||
MoveToEffect(stack,
|
||||
EffectController(duration: 0.35, curve: Curves.easeInOut)),
|
||||
MoveToEffect(center + dirs[i % 4],
|
||||
EffectController(duration: 0.55, curve: Curves.easeIn)),
|
||||
]));
|
||||
|
||||
// کوچکشدن هنگامِ پخش (همزمان با حرکتِ آخرِ دنباله).
|
||||
card.add(ScaleEffect.to(
|
||||
Vector2.all(0.5),
|
||||
EffectController(duration: 0.55, startDelay: 1.25 + delay),
|
||||
));
|
||||
}
|
||||
|
||||
add(RemoveEffect(delay: totalMs / 1000));
|
||||
}
|
||||
}
|
||||
|
||||
/// پشتِ یک کارت (مستطیلِ گرد با حاشیهی طلایی).
|
||||
class _Back extends PositionComponent {
|
||||
_Back(Vector2 s) : super(size: s);
|
||||
|
||||
static final _fill = Paint()..color = const Color(0xFF7A1020);
|
||||
static final _border = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2
|
||||
..color = const Color(0xFFE9C76A);
|
||||
static final _inner = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.5
|
||||
..color = const Color(0x88E9C76A);
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
final rect = size.toRect();
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(rect, const Radius.circular(8)), _fill);
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(rect, const Radius.circular(8)), _border);
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(rect.deflate(6), const Radius.circular(6)),
|
||||
_inner);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user