瀏覽代碼

feat(home): add music

zhaoyadi 11 月之前
父節點
當前提交
d07db56c9d

+ 3 - 3
android/app/build.gradle

@@ -29,8 +29,8 @@ android {
     ndkVersion = flutter.ndkVersion
 
     compileOptions {
-        sourceCompatibility = JavaVersion.VERSION_1_8
-        targetCompatibility = JavaVersion.VERSION_1_8
+        sourceCompatibility = JavaVersion.VERSION_11
+        targetCompatibility = JavaVersion.VERSION_11
     }
 
     defaultConfig {
@@ -38,7 +38,7 @@ android {
         applicationId = "com.zaojiao.battle"
         // You can update the following values to match your application needs.
         // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
-        minSdk = flutter.minSdkVersion
+        minSdk = 28
         targetSdk = flutter.targetSdkVersion
         versionCode = flutterVersionCode.toInteger()
         versionName = flutterVersionName

+ 13 - 0
android/app/src/main/res/drawable/ic_launcher_background.xml

@@ -0,0 +1,13 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+  <group>
+    <clip-path
+        android:pathData="M0,0h108v108h-108z"/>
+    <path
+        android:pathData="M0,0h108v108h-108z"
+        android:fillColor="#819FFF"/>
+  </group>
+</vector>

+ 37 - 0
android/app/src/main/res/drawable/ic_launcher_foreground.xml

@@ -0,0 +1,37 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+  <group>
+    <clip-path
+        android:pathData="M0,0h108v108h-108z"/>
+    <path
+        android:pathData="M30,26L30,82C30,83.1 30.9,84 32,84L76,84C77.1,84 78,83.1 78,82L78,26C78,24.9 77.1,24 76,24L66,24L66,75C66,75.55 65.55,76 65,76L34,76C33.45,76 33,75.55 33,75L33,24L32,24C30.9,24 30,24.9 30,26ZM69,33.5C69,33.22 69.22,33 69.5,33L74,33C74.55,33 75,33.45 75,34L75,77C75,77.55 74.55,78 74,78L65,78L65,81L64,81L64,78L59,78L59,81L58,81L58,78L53,78L53,81L52,81L52,78L47,78L47,81L46,81L46,78L41,78L41,81L40,81L40,78L35,78L35,81L34,81L34,78C34,77.45 34.45,77 35,77L74,77L74,74L69.5,74C69.22,74 69,73.78 69,73.5C69,73.22 69.22,73 69.5,73L74,73L74,66L69.5,66C69.22,66 69,65.78 69,65.5C69,65.22 69.22,65 69.5,65L74,65L74,58L69.5,58C69.22,58 69,57.78 69,57.5C69,57.22 69.22,57 69.5,57L74,57L74,50L69.5,50C69.22,50 69,49.78 69,49.5C69,49.22 69.22,49 69.5,49L74,49L74,42L69.5,42C69.22,42 69,41.78 69,41.5C69,41.22 69.22,41 69.5,41L74,41L74,34L69.5,34C69.22,34 69,33.78 69,33.5Z"
+        android:fillColor="#1200E4"
+        android:fillAlpha="0.5"
+        android:fillType="evenOdd"/>
+    <path
+        android:pathData="M33,80.5a1.5,1.5 0,1 0,3 0a1.5,1.5 0,1 0,-3 0z"
+        android:fillColor="#FF0000"/>
+    <path
+        android:pathData="M39,80.5a1.5,1.5 0,1 0,3 0a1.5,1.5 0,1 0,-3 0z"
+        android:fillColor="#FF8000"/>
+    <path
+        android:pathData="M45,80.5a1.5,1.5 0,1 0,3 0a1.5,1.5 0,1 0,-3 0z"
+        android:fillColor="#FFFF00"/>
+    <path
+        android:pathData="M51,80.5a1.5,1.5 0,1 0,3 0a1.5,1.5 0,1 0,-3 0z"
+        android:fillColor="#00FF00"/>
+    <path
+        android:pathData="M57,80.5a1.5,1.5 0,1 0,3 0a1.5,1.5 0,1 0,-3 0z"
+        android:fillColor="#0000FF"/>
+    <path
+        android:pathData="M63,80.5a1.5,1.5 0,1 0,3 0a1.5,1.5 0,1 0,-3 0z"
+        android:fillColor="#FF00FF"/>
+    <path
+        android:pathData="M33,24C33,24 33,24 33,24L66,24C66,24 66,24 66,24L66,75C66,75.55 65.55,76 65,76L34,76C33.45,76 33,75.55 33,75Z"
+        android:fillColor="#FFFFFF"
+        android:fillAlpha="0.75"/>
+  </group>
+</vector>

二進制
android/app/src/main/res/mipmap-hdpi/ic_launcher.png


二進制
android/app/src/main/res/mipmap-mdpi/ic_launcher.png


二進制
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png


二進制
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png


二進制
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


+ 6 - 0
android/app/src/main/res/mipmap/ic_launcher.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

二進制
assets/audios/counted.mp3


二進制
assets/audios/counting1.mp3


二進制
assets/audios/counting2.mp3


二進制
assets/audios/counting3.mp3


+ 3 - 1
lib/app/battle_app.dart

@@ -21,7 +21,9 @@ class _BattleAppState extends State<BattleApp> {
         routerConfig: _router,
         theme: ThemeData(
           useMaterial3: true,
-          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
+          colorScheme: ColorScheme.fromSeed(
+            seedColor: Colors.blueAccent,
+          ),
         ),
       ),
     );

+ 5 - 0
lib/button/button_controller.dart

@@ -1,6 +1,7 @@
 import 'package:battle/button/button_color.dart';
 import 'package:battle/data/card.dart';
 import 'package:flutter/cupertino.dart';
+import 'package:widget/widget.dart';
 
 import '../utils/utils.dart';
 
@@ -22,6 +23,8 @@ class ButtonInfo {
 
   bool pauseRight;
 
+  List<PathOp> ops;
+
   ButtonInfo({
     required this.color,
     required this.startIndex,
@@ -31,6 +34,7 @@ class ButtonInfo {
     this.pauseBottom = false,
     this.pauseInline = false,
     this.pauseRight = false,
+    this.ops = const [],
   });
 }
 
@@ -54,6 +58,7 @@ class ButtonController with ChangeNotifier {
 
     _animationController.addListener(() {
       int index = _animationController.value ~/ 1;
+      if (index == 6) return;
       double process = _animationController.value % 1.0;
 
       buttonList[_moveIndex[index]].process = Curves.easeInOutQuint.transform(process);

+ 1 - 1
lib/button/button_painter.dart

@@ -16,7 +16,7 @@ class ButtonPainter extends CustomPainter {
       ..strokeWidth = 4;
 
     for (var button in controller.buttonList) {
-      var path = fromBottomToCenter(size, button.startIndex, button.endIndex);
+      var path = fromBottomToCenter(size, button.startIndex, button.endIndex,button.ops);
       var pms = path.computeMetrics();
       for (var pm in pms) {
         Tangent? tangent = pm.getTangentForOffset(pm.length * button.process);

+ 0 - 0
lib/button/time_op.dart


+ 3 - 1
lib/main.dart

@@ -6,7 +6,9 @@ import 'app/battle_app.dart';
 void main() {
   WidgetsFlutterBinding.ensureInitialized();
 
-  SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
+  SystemChrome.setEnabledSystemUIMode(
+    SystemUiMode.immersiveSticky,
+  );
 
   runApp(const BattleApp());
 }

+ 6 - 3
lib/page/game_page.dart

@@ -7,6 +7,7 @@ import 'package:widget/widget.dart';
 
 import '../button/button_painter.dart';
 import 'setting_page.dart';
+import 'view/count_down_dialog.dart';
 
 class GamePage extends StatefulWidget {
   final CardItem card;
@@ -122,7 +123,7 @@ class _GamePageState extends State<GamePage> with SingleTickerProviderStateMixin
                             ),
                           ),
                         ),
-                        SizedBox(height: 32),
+                        const SizedBox(height: 32),
                         Padding(
                           padding: const EdgeInsets.symmetric(horizontal: 16),
                           child: ButtonBar(
@@ -130,8 +131,10 @@ class _GamePageState extends State<GamePage> with SingleTickerProviderStateMixin
                             children: [
                               FilledButton(
                                 onPressed: () {
-                                  _createTimer();
-                                  _controller.startOrReset();
+                                  showCountDown(context).then((_) {
+                                    _createTimer();
+                                    _controller.startOrReset();
+                                  });
                                 },
                                 child: const Text('开始'),
                               ),

+ 1 - 1
lib/page/home_page.dart

@@ -95,7 +95,7 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
         answer: card.answer,
       ).push(context),
       child: Card(
-        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
+        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
         clipBehavior: Clip.antiAlias,
         child: Column(
           crossAxisAlignment: CrossAxisAlignment.start,

+ 90 - 0
lib/page/view/count_down_dialog.dart

@@ -0,0 +1,90 @@
+import 'dart:async';
+
+import 'package:audioplayers/audioplayers.dart';
+import 'package:flutter/material.dart';
+
+Future<void> showCountDown(BuildContext context) {
+  return Navigator.push(context, RawDialogRoute(
+    pageBuilder: (_, __, ___) {
+      return const _CountDownContent();
+    },
+  ));
+}
+
+class _CountDownContent extends StatefulWidget {
+  const _CountDownContent({super.key});
+
+  @override
+  State<_CountDownContent> createState() => _CountDownContentState();
+}
+
+class _CountDownContentState extends State<_CountDownContent> {
+  int _count = 4;
+
+  Timer? _timer;
+
+  late AudioPlayer _audioPlayer;
+
+  @override
+  void initState() {
+    super.initState();
+    _audioPlayer = AudioPlayer();
+    _timer = Timer.periodic(const Duration(seconds: 1), (t) {
+      if (t.tick < 4) {
+        if (mounted) {
+          setState(() {
+            _count = 4 - t.tick;
+          });
+        }
+        _audioPlayer.play(AssetSource('audios/counting${t.tick}.mp3'), mode: PlayerMode.lowLatency);
+      } else {
+        _audioPlayer.play(AssetSource('audios/counted.mp3'), mode: PlayerMode.lowLatency);
+        _timer?.cancel();
+        _timer = null;
+
+        if (mounted) {
+          Future.delayed(const Duration(milliseconds: 200), () => Navigator.pop(context));
+        }
+      }
+    });
+  }
+
+  @override
+  void dispose() {
+    _timer?.cancel();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Material(
+      type: MaterialType.transparency,
+      child: Center(
+        child: Container(
+          constraints: const BoxConstraints(maxWidth: 300),
+          child: Column(
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              Container(
+                width: double.infinity,
+                height: 300,
+                alignment: Alignment.center,
+                decoration: BoxDecoration(
+                  color: Theme.of(context).colorScheme.primaryContainer,
+                  borderRadius: BorderRadius.circular(8),
+                ),
+                child: Text(
+                  _count >= 4 ? 'Ready' : '$_count',
+                  style: TextStyle(
+                    fontSize: 33,
+                    color: Theme.of(context).colorScheme.onPrimaryContainer,
+                  ),
+                ),
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 111 - 0
lib/route.g.dart

@@ -0,0 +1,111 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'route.dart';
+
+// **************************************************************************
+// GoRouterGenerator
+// **************************************************************************
+
+List<RouteBase> get $appRoutes => [
+      $homeRoute,
+      $settingRoute,
+      $gameRouter,
+      $aboutRouter,
+    ];
+
+RouteBase get $homeRoute => GoRouteData.$route(
+      path: '/',
+      factory: $HomeRouteExtension._fromState,
+    );
+
+extension $HomeRouteExtension on HomeRoute {
+  static HomeRoute _fromState(GoRouterState state) => HomeRoute();
+
+  String get location => GoRouteData.$location(
+        '/',
+      );
+
+  void go(BuildContext context) => context.go(location);
+
+  Future<T?> push<T>(BuildContext context) => context.push<T>(location);
+
+  void pushReplacement(BuildContext context) =>
+      context.pushReplacement(location);
+
+  void replace(BuildContext context) => context.replace(location);
+}
+
+RouteBase get $settingRoute => GoRouteData.$route(
+      path: '/setting',
+      factory: $SettingRouteExtension._fromState,
+    );
+
+extension $SettingRouteExtension on SettingRoute {
+  static SettingRoute _fromState(GoRouterState state) => SettingRoute();
+
+  String get location => GoRouteData.$location(
+        '/setting',
+      );
+
+  void go(BuildContext context) => context.go(location);
+
+  Future<T?> push<T>(BuildContext context) => context.push<T>(location);
+
+  void pushReplacement(BuildContext context) =>
+      context.pushReplacement(location);
+
+  void replace(BuildContext context) => context.replace(location);
+}
+
+RouteBase get $gameRouter => GoRouteData.$route(
+      path: '/game',
+      factory: $GameRouterExtension._fromState,
+    );
+
+extension $GameRouterExtension on GameRouter {
+  static GameRouter _fromState(GoRouterState state) => GameRouter(
+        cardName: state.uri.queryParameters['card-name']!,
+        assetPath: state.uri.queryParameters['asset-path']!,
+        answer: state.uri.queryParametersAll['answer']!.map(int.parse).toList(),
+      );
+
+  String get location => GoRouteData.$location(
+        '/game',
+        queryParams: {
+          'card-name': cardName,
+          'asset-path': assetPath,
+          'answer': answer.map((e) => e.toString()).toList(),
+        },
+      );
+
+  void go(BuildContext context) => context.go(location);
+
+  Future<T?> push<T>(BuildContext context) => context.push<T>(location);
+
+  void pushReplacement(BuildContext context) =>
+      context.pushReplacement(location);
+
+  void replace(BuildContext context) => context.replace(location);
+}
+
+RouteBase get $aboutRouter => GoRouteData.$route(
+      path: '/about',
+      factory: $AboutRouterExtension._fromState,
+    );
+
+extension $AboutRouterExtension on AboutRouter {
+  static AboutRouter _fromState(GoRouterState state) => AboutRouter();
+
+  String get location => GoRouteData.$location(
+        '/about',
+      );
+
+  void go(BuildContext context) => context.go(location);
+
+  Future<T?> push<T>(BuildContext context) => context.push<T>(location);
+
+  void pushReplacement(BuildContext context) =>
+      context.pushReplacement(location);
+
+  void replace(BuildContext context) => context.replace(location);
+}

+ 0 - 17
lib/utils/utils.dart

@@ -1,8 +1,5 @@
-
 import 'dart:math';
 
-import 'package:flutter/animation.dart';
-
 List<int> generateIntList() {
   List<int> list = List.generate(6, (i) => i);
 
@@ -18,17 +15,3 @@ List<int> generateIntList() {
 
   return list;
 }
-
-// List<Curve> curves = <Curve>[
-//   Curves.easeIn,
-//   Curves.easeInCirc,
-//   Curves.easeIn,
-//   Curves.easeIn,
-//   Curves.easeIn,
-//   Curves.easeIn,
-//   Curves.easeIn,
-// ];
-//
-// double curveProcess(double value){
-//
-// }

+ 8 - 3
packages/widget/lib/src/battle_path.dart

@@ -1,19 +1,22 @@
 import 'dart:ui';
 
 import 'constant.dart';
+import 'path_op.dart';
 
 ///本质是求五个点的位置,依次lineTo起来形成一条路径
 
-Path fromBottomToCenter(Size size, int start, int end) {
+Path fromBottomToCenter(Size size, int start, int end, List<PathOp> opList) {
   final path = Path();
 
   final double line = size.width * kButtonLineAspectRatio;
 
+  /// 左下角的位置
   final double l1 = size.width * kBottomButtonLeftAspectRatio;
   final double r1 = size.width * kBottomButtonRightAspectRatio;
   final double t1 = size.height * kBottomButtonTopAspectRatio;
   final double b1 = size.height * kBottomButtonBottomAspectRatio;
 
+  /// 右上角的位置
   final double l2 = size.width * kRightButtonLeftAspectRatio;
   final double r2 = size.width * kRightButtonRightAspectRatio;
   final double t2 = size.height * kRightButtonTopAspectRatio;
@@ -39,10 +42,12 @@ Path fromBottomToCenter(Size size, int start, int end) {
 
   path.moveTo(p1x, p1y);
   path.lineTo(p2x, p2y);
-  assert(cr - line / 2 == p3x);
-  assert(p2y == cb + line / 2);
+
+  PauseOp().handlePath(path, size, start, end);
+
   path.lineTo(cr - line / 2, p2y);
   path.lineTo(p3x, cb + line / 2);
+
   path.lineTo(p3x, p3y);
   path.lineTo(p4x, p4y);
 

+ 181 - 0
packages/widget/lib/src/path_op.dart

@@ -0,0 +1,181 @@
+import 'dart:ui';
+
+import 'constant.dart';
+
+sealed class PathOp {
+  void handlePath(Path path, Size size, int start, int end);
+}
+
+final class BackOp extends PathOp {
+  @override
+  void handlePath(Path path, Size size, int start, int end) {
+    final double line = size.width * kButtonLineAspectRatio;
+
+    /// 左下角的位置
+    final double l1 = size.width * kBottomButtonLeftAspectRatio;
+    final double r1 = size.width * kBottomButtonRightAspectRatio;
+    final double t1 = size.height * kBottomButtonTopAspectRatio;
+    final double b1 = size.height * kBottomButtonBottomAspectRatio;
+
+    /// 右上角的位置
+    final double l2 = size.width * kRightButtonLeftAspectRatio;
+    final double r2 = size.width * kRightButtonRightAspectRatio;
+    final double t2 = size.height * kRightButtonTopAspectRatio;
+    final double b2 = size.height * kRightButtonBottomAspectRatio;
+
+    final double cr = size.width * kRightButtonRightAspectRatio;
+    final double cb = size.height * kBottomButtonTopAspectRatio;
+
+    final double spacing1 = ((r1 - l1) - line * 6) / 5 + line;
+    final double spacing2 = ((b2 - t2) - line * 6) / 5 + line;
+
+    final double p1x = l1 + spacing1 * start + line / 2;
+    final double p1y = b1 - line * 3 / 2;
+
+    final double p2x = p1x;
+    final double p2y = t1 + line / 2;
+
+    final double p3x = r2 - line / 2;
+    final double p3y = t2 + spacing2 * end + line / 2;
+
+    final double p4x = l2 + line / 2;
+    final double p4y = p3y;
+  }
+}
+
+final class PauseOp extends PathOp {
+  @override
+  void handlePath(Path path, Size size, int start, int end) {
+    final double line = size.width * kButtonLineAspectRatio;
+
+    /// 左下角的位置
+    final double l1 = size.width * kBottomButtonLeftAspectRatio;
+    final double r1 = size.width * kBottomButtonRightAspectRatio;
+    final double t1 = size.height * kBottomButtonTopAspectRatio;
+    final double b1 = size.height * kBottomButtonBottomAspectRatio;
+
+    final double spacing1 = ((r1 - l1) - line * 6) / 5 + line;
+
+    final double p1x = l1 + spacing1 * start + line / 2;
+    final double p2x = p1x;
+    final double p2y = t1 + line / 2;
+
+    if (start == 0) {
+      for (int i = 0; i < 2; i++) {
+        path.lineTo(p2x + spacing1, p2y);
+        path.lineTo(p2x, p2y);
+      }
+    }
+  }
+}
+
+final class WrongAnswerOp extends PathOp {
+  @override
+  void handlePath(Path path, Size size, int start, int end) {
+    final double line = size.width * kButtonLineAspectRatio;
+
+    /// 左下角的位置
+    final double l1 = size.width * kBottomButtonLeftAspectRatio;
+    final double r1 = size.width * kBottomButtonRightAspectRatio;
+    final double t1 = size.height * kBottomButtonTopAspectRatio;
+    final double b1 = size.height * kBottomButtonBottomAspectRatio;
+
+    /// 右上角的位置
+    final double l2 = size.width * kRightButtonLeftAspectRatio;
+    final double r2 = size.width * kRightButtonRightAspectRatio;
+    final double t2 = size.height * kRightButtonTopAspectRatio;
+    final double b2 = size.height * kRightButtonBottomAspectRatio;
+
+    final double cr = size.width * kRightButtonRightAspectRatio;
+    final double cb = size.height * kBottomButtonTopAspectRatio;
+
+    final double spacing1 = ((r1 - l1) - line * 6) / 5 + line;
+    final double spacing2 = ((b2 - t2) - line * 6) / 5 + line;
+
+    final double p1x = l1 + spacing1 * start + line / 2;
+    final double p1y = b1 - line * 3 / 2;
+
+    final double p2x = p1x;
+    final double p2y = t1 + line / 2;
+
+    final double p3x = r2 - line / 2;
+    final double p3y = t2 + spacing2 * end + line / 2;
+
+    final double p4x = l2 + line / 2;
+    final double p4y = p3y;
+  }
+}
+
+final class AboveOp extends PathOp {
+  @override
+  void handlePath(Path path, Size size, int start, int end) {
+    final double line = size.width * kButtonLineAspectRatio;
+
+    /// 左下角的位置
+    final double l1 = size.width * kBottomButtonLeftAspectRatio;
+    final double r1 = size.width * kBottomButtonRightAspectRatio;
+    final double t1 = size.height * kBottomButtonTopAspectRatio;
+    final double b1 = size.height * kBottomButtonBottomAspectRatio;
+
+    /// 右上角的位置
+    final double l2 = size.width * kRightButtonLeftAspectRatio;
+    final double r2 = size.width * kRightButtonRightAspectRatio;
+    final double t2 = size.height * kRightButtonTopAspectRatio;
+    final double b2 = size.height * kRightButtonBottomAspectRatio;
+
+    final double cr = size.width * kRightButtonRightAspectRatio;
+    final double cb = size.height * kBottomButtonTopAspectRatio;
+
+    final double spacing1 = ((r1 - l1) - line * 6) / 5 + line;
+    final double spacing2 = ((b2 - t2) - line * 6) / 5 + line;
+
+    final double p1x = l1 + spacing1 * start + line / 2;
+    final double p1y = b1 - line * 3 / 2;
+
+    final double p2x = p1x;
+    final double p2y = t1 + line / 2;
+
+    final double p3x = r2 - line / 2;
+    final double p3y = t2 + spacing2 * end + line / 2;
+
+    final double p4x = l2 + line / 2;
+    final double p4y = p3y;
+  }
+}
+
+final class BottomOp extends PathOp {
+  @override
+  void handlePath(Path path, Size size, int start, int end) {
+    final double line = size.width * kButtonLineAspectRatio;
+
+    /// 左下角的位置
+    final double l1 = size.width * kBottomButtonLeftAspectRatio;
+    final double r1 = size.width * kBottomButtonRightAspectRatio;
+    final double t1 = size.height * kBottomButtonTopAspectRatio;
+    final double b1 = size.height * kBottomButtonBottomAspectRatio;
+
+    /// 右上角的位置
+    final double l2 = size.width * kRightButtonLeftAspectRatio;
+    final double r2 = size.width * kRightButtonRightAspectRatio;
+    final double t2 = size.height * kRightButtonTopAspectRatio;
+    final double b2 = size.height * kRightButtonBottomAspectRatio;
+
+    final double cr = size.width * kRightButtonRightAspectRatio;
+    final double cb = size.height * kBottomButtonTopAspectRatio;
+
+    final double spacing1 = ((r1 - l1) - line * 6) / 5 + line;
+    final double spacing2 = ((b2 - t2) - line * 6) / 5 + line;
+
+    final double p1x = l1 + spacing1 * start + line / 2;
+    final double p1y = b1 - line * 3 / 2;
+
+    final double p2x = p1x;
+    final double p2y = t1 + line / 2;
+
+    final double p3x = r2 - line / 2;
+    final double p3y = t2 + spacing2 * end + line / 2;
+
+    final double p4x = l2 + line / 2;
+    final double p4y = p3y;
+  }
+}

+ 1 - 1
packages/widget/lib/widget.dart

@@ -2,4 +2,4 @@ library widget;
 
 export 'src/battle_layout.dart';
 export 'src/battle_path.dart';
-
+export 'src/path_op.dart';

+ 176 - 0
pubspec.lock

@@ -33,6 +33,62 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.11.0"
+  audioplayers:
+    dependency: "direct main"
+    description:
+      name: audioplayers
+      sha256: "752039d6aa752597c98ec212e9759519061759e402e7da59a511f39d43aa07d2"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "6.0.0"
+  audioplayers_android:
+    dependency: transitive
+    description:
+      name: audioplayers_android
+      sha256: de576b890befe27175c2f511ba8b742bec83765fa97c3ce4282bba46212f58e4
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "5.0.0"
+  audioplayers_darwin:
+    dependency: transitive
+    description:
+      name: audioplayers_darwin
+      sha256: e507887f3ff18d8e5a10a668d7bedc28206b12e10b98347797257c6ae1019c3b
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "6.0.0"
+  audioplayers_linux:
+    dependency: transitive
+    description:
+      name: audioplayers_linux
+      sha256: "3d3d244c90436115417f170426ce768856d8fe4dfc5ed66a049d2890acfa82f9"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.0.0"
+  audioplayers_platform_interface:
+    dependency: transitive
+    description:
+      name: audioplayers_platform_interface
+      sha256: "6834dd48dfb7bc6c2404998ebdd161f79cd3774a7e6779e1348d54a3bfdcfaa5"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "7.0.0"
+  audioplayers_web:
+    dependency: transitive
+    description:
+      name: audioplayers_web
+      sha256: db8fc420dadf80da18e2286c18e746fb4c3b2c5adbf0c963299dde046828886d
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "5.0.0"
+  audioplayers_windows:
+    dependency: transitive
+    description:
+      name: audioplayers_windows
+      sha256: "8605762dddba992138d476f6a0c3afd9df30ac5b96039929063eceed416795c2"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.0.0"
   boolean_selector:
     dependency: transitive
     description:
@@ -185,6 +241,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.3.1"
+  ffi:
+    dependency: transitive
+    description:
+      name: ffi
+      sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.2"
   file:
     dependency: transitive
     description:
@@ -272,6 +336,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.3.1"
+  http:
+    dependency: transitive
+    description:
+      name: http
+      sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.2.1"
   http_multi_server:
     dependency: transitive
     description:
@@ -400,6 +472,70 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.9.0"
+  path_provider:
+    dependency: transitive
+    description:
+      name: path_provider
+      sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.3"
+  path_provider_android:
+    dependency: transitive
+    description:
+      name: path_provider_android
+      sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.2.6"
+  path_provider_foundation:
+    dependency: transitive
+    description:
+      name: path_provider_foundation
+      sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.4.0"
+  path_provider_linux:
+    dependency: transitive
+    description:
+      name: path_provider_linux
+      sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.2.1"
+  path_provider_platform_interface:
+    dependency: transitive
+    description:
+      name: path_provider_platform_interface
+      sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.2"
+  path_provider_windows:
+    dependency: transitive
+    description:
+      name: path_provider_windows
+      sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.2.1"
+  platform:
+    dependency: transitive
+    description:
+      name: platform
+      sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.1.5"
+  plugin_platform_interface:
+    dependency: transitive
+    description:
+      name: plugin_platform_interface
+      sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.8"
   pool:
     dependency: transitive
     description:
@@ -484,6 +620,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.10.0"
+  sprintf:
+    dependency: transitive
+    description:
+      name: sprintf
+      sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "7.0.0"
   stack_trace:
     dependency: transitive
     description:
@@ -524,6 +668,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.2.0"
+  synchronized:
+    dependency: transitive
+    description:
+      name: synchronized
+      sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "3.1.0+1"
   term_glyph:
     dependency: transitive
     description:
@@ -556,6 +708,14 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "1.3.2"
+  uuid:
+    dependency: transitive
+    description:
+      name: uuid
+      sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8"
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "4.4.0"
   vector_math:
     dependency: transitive
     description:
@@ -611,6 +771,22 @@ packages:
       relative: true
     source: path
     version: "0.0.1"
+  win32:
+    dependency: transitive
+    description:
+      name: win32
+      sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "5.5.1"
+  xdg_directories:
+    dependency: transitive
+    description:
+      name: xdg_directories
+      sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.4"
   yaml:
     dependency: transitive
     description:

+ 6 - 0
pubspec.yaml

@@ -16,6 +16,7 @@ dependencies:
 
   riverpod: ^2.5.1
   flutter_riverpod: ^2.5.1
+  audioplayers: ^6.0.0
 
   repository:
     path: packages/repository
@@ -46,3 +47,8 @@ flutter:
     - assets/cards/css2-2.jpg
     - assets/cards/css2-11.jpg
     - assets/cards/css2-13.jpg
+    # 音乐
+    - assets/audios/counting1.mp3
+    - assets/audios/counting2.mp3
+    - assets/audios/counting3.mp3
+    - assets/audios/counted.mp3