重构在线游戏机制,升级到 1.0.1 和 0.0.2(服务端)
This commit is contained in:
69
README.md
69
README.md
@ -56,7 +56,6 @@ frog_game/
|
||||
│ │ └── handlers/ # 请求处理器
|
||||
│ ├── bin/server.dart # 服务器入口
|
||||
│ ├── Dockerfile # Docker配置
|
||||
│ └── deploy.sh # 部署脚本
|
||||
└── images/ # 资源文件
|
||||
```
|
||||
|
||||
@ -76,7 +75,7 @@ flutter pub get
|
||||
flutter pub run build_runner build
|
||||
|
||||
# 4. 运行应用,不提供则使用默认 devServerUrl
|
||||
flutter run --dart-define=SERVER_URL=ws://your-server.com/
|
||||
flutter run --dart-define=SERVER_URL=ws://localhost:8080/frog
|
||||
```
|
||||
|
||||
### 服务器开发
|
||||
@ -92,7 +91,7 @@ dart pub get
|
||||
dart pub run build_runner build
|
||||
|
||||
# 本地运行
|
||||
./deploy.sh dev
|
||||
dart run bin/server.dart
|
||||
```
|
||||
|
||||
服务器将在 `http://localhost:8080` 启动。
|
||||
@ -101,8 +100,8 @@ dart pub run build_runner build
|
||||
|
||||
```bash
|
||||
cd server
|
||||
docker build -t corkine/frog-game:0.0.1 .
|
||||
docker run -p 8080:8080 corkine/frog-game:0.0.1
|
||||
docker build -t corkine/frog-game:0.0.2 .
|
||||
docker run -p 8080:8080 corkine/frog-game:0.0.2
|
||||
```
|
||||
|
||||
### 客户端部署
|
||||
@ -118,36 +117,58 @@ flutter build web --release --base-href=/frog/
|
||||
|
||||
#### 连接
|
||||
```
|
||||
WS ws://server-domain/ws
|
||||
WS ws://server-domain/frog
|
||||
```
|
||||
|
||||
#### 消息格式
|
||||
|
||||
所有消息都遵循一个基础的JSON结构。
|
||||
|
||||
**客户端 -> 服务器 (C→S)**
|
||||
```json
|
||||
{
|
||||
"type": "MessageType",
|
||||
"roomId": "123456",
|
||||
"roomId": "123456", // 可选,取决于消息类型
|
||||
"playerId": "player123",
|
||||
"data": {...},
|
||||
"error": "error message",
|
||||
"data": { ... }, // 可选,具体结构看消息类型
|
||||
"timestamp": 1640995200000
|
||||
}
|
||||
```
|
||||
|
||||
**服务器 -> 客户端 (S→C)**
|
||||
```json
|
||||
{
|
||||
"type": "MessageType",
|
||||
"roomId": "123456", // 可选
|
||||
"playerId": "player123", // 可选
|
||||
"data": { // 结构取决于消息类型
|
||||
"roomInfo": { ... },
|
||||
"gameState": { ... }
|
||||
// 或 "message" 用于错误
|
||||
},
|
||||
"timestamp": 1640995200000
|
||||
}
|
||||
```
|
||||
|
||||
#### 消息类型
|
||||
|
||||
| 类型 | 方向 | 说明 |
|
||||
|------|------|------|
|
||||
| `createRoom` | C→S | 创建房间 |
|
||||
| `joinRoom` | C→S | 加入房间 |
|
||||
| `leaveRoom` | C→S | 离开房间 |
|
||||
| `gameMove` | C→S | 游戏移动 |
|
||||
| `gameReset` | C→S | 重置游戏 |
|
||||
| `roomCreated` | S→C | 房间创建成功 |
|
||||
| `roomJoined` | S→C | 加入房间成功 |
|
||||
| `playerJoined` | S→C | 玩家加入通知 |
|
||||
| `playerLeft` | S→C | 玩家离开通知 |
|
||||
| `gameUpdate` | S→C | 游戏状态更新 |
|
||||
| `error` | S→C | 错误消息 |
|
||||
| 类型 | 方向 | `data` 负载说明 |
|
||||
|--------------|-------|---------------------------------------------------------------------|
|
||||
| `ping` | C→S | 无 `data` |
|
||||
| `pong` | S→C | 无 `data` |
|
||||
| `createRoom` | C→S | `{'playerName': 'string'}` |
|
||||
| `joinRoom` | C→S | `{'playerName': 'string'}` |
|
||||
| `leaveRoom` | C→S | 无 `data` |
|
||||
| `gameMove` | C→S | `{'position': int, 'player': 'X'|'O', ...}` |
|
||||
| `gameReset` | C→S | 无 `data` |
|
||||
| `roomCreated` | S→C | `{'roomInfo': RoomInfo, 'gameState': GameState}` |
|
||||
| `roomJoined` | S→C | `{'roomInfo': RoomInfo, 'gameState': GameState}` |
|
||||
| `playerJoined` | S→C | `{'roomInfo': RoomInfo, 'gameState': GameState}` |
|
||||
| `playerLeft` | S→C | `{'roomInfo': RoomInfo, 'gameState': GameState}` |
|
||||
| `gameUpdate` | S→C | `{'roomInfo': RoomInfo, 'gameState': GameState}` |
|
||||
| `gameOver` | S→C | `{'roomInfo': RoomInfo, 'gameState': GameState}` |
|
||||
| `gameReset` | S→C | `{'roomInfo': RoomInfo, 'gameState': GameState}` |
|
||||
| `error` | S→C | `{'message': 'string'}` |
|
||||
|
||||
### HTTP 端点
|
||||
|
||||
@ -156,7 +177,7 @@ WS ws://server-domain/ws
|
||||
| `/` | GET | 服务器首页 |
|
||||
| `/health` | GET | 健康检查 |
|
||||
| `/stats` | GET | 服务器统计 |
|
||||
| `/ws` | GET | WebSocket升级 |
|
||||
| `/frog` | GET | WebSocket升级 |
|
||||
|
||||
## 🛠️ 开发指南
|
||||
|
||||
@ -208,7 +229,6 @@ dart pub run build_runner build --delete-conflicting-outputs
|
||||
### 部署
|
||||
- 阿里云账号
|
||||
- 容器镜像服务
|
||||
- 函数计算服务
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
@ -226,7 +246,6 @@ dart pub run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- [Flutter 官方文档](https://flutter.dev/docs)
|
||||
- [Dart 官方文档](https://dart.dev/guides)
|
||||
- [阿里云函数计算](https://www.aliyun.com/product/fc)
|
||||
- [Riverpod 状态管理](https://riverpod.dev/)
|
||||
|
||||
## 🎮 游戏规则
|
||||
|
@ -1,10 +1,10 @@
|
||||
/// 应用程序配置类
|
||||
class AppConfig {
|
||||
/// 开发环境配置
|
||||
static const String devServerUrl = 'wss://frogme.mazhangjing.com/ws';
|
||||
static const String devServerUrl = 'ws://localhost:8080/frog';
|
||||
|
||||
/// 生产环境配置 - 替换为您的实际服务器地址
|
||||
static const String prodServerUrl = 'wss://frogme.mazhangjing.com/ws';
|
||||
static const String prodServerUrl = 'wss://frogme.mazhangjing.com/frog';
|
||||
|
||||
/// 当前环境
|
||||
static const bool isDebug = bool.fromEnvironment('dart.vm.product') == false;
|
||||
@ -32,5 +32,5 @@ class AppConfig {
|
||||
|
||||
/// UI配置
|
||||
static const String appName = '青蛙跳井';
|
||||
static const String appVersion = '1.0.0';
|
||||
static const String appVersion = '1.0.1 · 由 AI 驱动开发';
|
||||
}
|
||||
|
@ -1,9 +1,15 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../models/game_state.dart';
|
||||
|
||||
class GameNotifier extends StateNotifier<GameState> {
|
||||
GameNotifier() : super(const GameState());
|
||||
part 'game.g.dart';
|
||||
|
||||
@riverpod
|
||||
class Game extends _$Game {
|
||||
@override
|
||||
GameState build() {
|
||||
return const GameState();
|
||||
}
|
||||
|
||||
/// 重新开始游戏
|
||||
void resetGame({bool aiMode = false}) {
|
||||
@ -102,8 +108,4 @@ class GameNotifier extends StateNotifier<GameState> {
|
||||
return bestScore;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final gameProvider = StateNotifierProvider<GameNotifier, GameState>((ref) {
|
||||
return GameNotifier();
|
||||
});
|
||||
}
|
25
lib/providers/game.g.dart
Normal file
25
lib/providers/game.g.dart
Normal file
@ -0,0 +1,25 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'game.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$gameHash() => r'e26c8993132a1ed1442b0be32cd1c18fb0e18e45';
|
||||
|
||||
/// See also [Game].
|
||||
@ProviderFor(Game)
|
||||
final gameProvider = AutoDisposeNotifierProvider<Game, GameState>.internal(
|
||||
Game.new,
|
||||
name: r'gameProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$gameHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$Game = AutoDisposeNotifier<GameState>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
281
lib/providers/online_game.dart
Normal file
281
lib/providers/online_game.dart
Normal file
@ -0,0 +1,281 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:random_name_generator/random_name_generator.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../models/game_state.dart';
|
||||
import '../models/network_message.dart';
|
||||
import '../config/app_config.dart';
|
||||
import '../services/websocket_service.dart';
|
||||
|
||||
part 'online_game.freezed.dart';
|
||||
part 'online_game.g.dart';
|
||||
|
||||
/// 连接状态的枚举
|
||||
enum ConnectionStatus {
|
||||
initial,
|
||||
connecting,
|
||||
connected,
|
||||
disconnected,
|
||||
}
|
||||
|
||||
/// 在线游戏的整体状态,使用Freezed进行状态管理
|
||||
@freezed
|
||||
class OnlineGameState with _$OnlineGameState {
|
||||
const factory OnlineGameState({
|
||||
@Default(GameState()) GameState gameState,
|
||||
@Default(ConnectionStatus.initial) ConnectionStatus connectionStatus,
|
||||
RoomInfo? roomInfo,
|
||||
String? error,
|
||||
PlayerInfo? currentPlayer,
|
||||
Player? mySymbol,
|
||||
@Default(false) bool isJoiningRoom,
|
||||
}) = _OnlineGameState;
|
||||
|
||||
// Freezed会自动处理构造函数,这里我们保留私有构造函数以添加自定义getter
|
||||
const OnlineGameState._();
|
||||
|
||||
/// 计算当前玩家是否轮到自己
|
||||
bool get isMyTurn {
|
||||
if (mySymbol == null ||
|
||||
roomInfo == null ||
|
||||
gameState.status != GameStatus.playing) {
|
||||
return false;
|
||||
}
|
||||
return gameState.currentPlayer == mySymbol;
|
||||
}
|
||||
}
|
||||
|
||||
/// 在线游戏的StateNotifier,负责所有业务逻辑
|
||||
@riverpod
|
||||
class OnlineGame extends _$OnlineGame {
|
||||
StreamSubscription? _socketSubscription;
|
||||
String? _playerId;
|
||||
|
||||
@override
|
||||
OnlineGameState build() {
|
||||
return const OnlineGameState();
|
||||
}
|
||||
|
||||
WebSocketService get _webSocketService => ref.read(webSocketServiceProvider);
|
||||
|
||||
String _generatePlayerId() {
|
||||
final random = Random();
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
return List.generate(12, (index) => chars[random.nextInt(chars.length)])
|
||||
.join();
|
||||
}
|
||||
|
||||
/// 连接到服务器
|
||||
Future<void> connect() async {
|
||||
if (state.connectionStatus == ConnectionStatus.connecting ||
|
||||
state.connectionStatus == ConnectionStatus.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
connectionStatus: ConnectionStatus.connecting, error: null);
|
||||
_playerId = _generatePlayerId();
|
||||
|
||||
final stream = _webSocketService.connect(
|
||||
AppConfig.serverUrl,
|
||||
onDone: () {
|
||||
if (state.connectionStatus != ConnectionStatus.disconnected) {
|
||||
state = state.copyWith(
|
||||
connectionStatus: ConnectionStatus.disconnected,
|
||||
// 保留错误信息(如果有的话)
|
||||
);
|
||||
_socketSubscription?.cancel();
|
||||
_socketSubscription = null;
|
||||
}
|
||||
},
|
||||
onError: (error, stackTrace) {
|
||||
if (state.connectionStatus != ConnectionStatus.disconnected) {
|
||||
state = state.copyWith(
|
||||
connectionStatus: ConnectionStatus.disconnected,
|
||||
error: '连接发生错误: $error',
|
||||
);
|
||||
_socketSubscription?.cancel();
|
||||
_socketSubscription = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (stream != null) {
|
||||
state = state.copyWith(connectionStatus: ConnectionStatus.connected);
|
||||
_socketSubscription = stream.listen((data) {
|
||||
_handleRawMessage(data);
|
||||
});
|
||||
// 发送一个ping来"确认"连接并获取玩家ID
|
||||
_sendMessage(MessageType.ping);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
connectionStatus: ConnectionStatus.disconnected,
|
||||
error: '无法连接到服务器',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 断开连接
|
||||
void disconnect() {
|
||||
_webSocketService.disconnect();
|
||||
_socketSubscription?.cancel();
|
||||
_socketSubscription = null;
|
||||
state = const OnlineGameState(
|
||||
connectionStatus: ConnectionStatus.disconnected);
|
||||
}
|
||||
|
||||
/// 离开界面时清理状态
|
||||
void cleanup() {
|
||||
disconnect();
|
||||
// 重置为完全初始状态
|
||||
state = const OnlineGameState();
|
||||
}
|
||||
|
||||
// --- 用户操作 ---
|
||||
|
||||
void createRoom(String playerName) {
|
||||
final name =
|
||||
playerName.trim().isEmpty ? RandomNames(Zone.us).name() : playerName;
|
||||
_sendMessage(MessageType.createRoom, data: {'playerName': name});
|
||||
}
|
||||
|
||||
void joinRoom(String roomId, String playerName) {
|
||||
state = state.copyWith(isJoiningRoom: true, error: null);
|
||||
final name =
|
||||
playerName.trim().isEmpty ? RandomNames(Zone.us).name() : playerName;
|
||||
_sendMessage(MessageType.joinRoom,
|
||||
roomId: roomId, data: {'playerName': name});
|
||||
|
||||
// 超时处理
|
||||
Timer(const Duration(seconds: 10), () {
|
||||
if (state.isJoiningRoom) {
|
||||
state = state.copyWith(isJoiningRoom: false, error: '加入房间超时');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void leaveRoom() {
|
||||
if (state.roomInfo?.roomId == null) return;
|
||||
_sendMessage(MessageType.leaveRoom, roomId: state.roomInfo!.roomId);
|
||||
}
|
||||
|
||||
void makeMove(int position) {
|
||||
if (!state.isMyTurn ||
|
||||
state.mySymbol == null ||
|
||||
!state.gameState.canMakeMove(position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final moveData = GameMoveData(
|
||||
position: position,
|
||||
player: state.mySymbol!,
|
||||
playerId: _playerId!,
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
_sendMessage(MessageType.gameMove,
|
||||
roomId: state.roomInfo!.roomId, data: moveData.toJson());
|
||||
}
|
||||
|
||||
void resetGame() {
|
||||
if (state.roomInfo?.roomId == null) return;
|
||||
_sendMessage(MessageType.gameReset, roomId: state.roomInfo!.roomId);
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
state = state.copyWith(error: null);
|
||||
}
|
||||
|
||||
// --- 消息处理 ---
|
||||
|
||||
void _sendMessage(MessageType type,
|
||||
{String? roomId, Map<String, dynamic>? data}) {
|
||||
if (_playerId == null ||
|
||||
state.connectionStatus != ConnectionStatus.connected) return;
|
||||
_webSocketService.sendMessage(NetworkMessage(
|
||||
type: type,
|
||||
playerId: _playerId!,
|
||||
roomId: roomId,
|
||||
data: data,
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
));
|
||||
}
|
||||
|
||||
void _handleRawMessage(dynamic data) {
|
||||
try {
|
||||
final message = NetworkMessage.fromJson(jsonDecode(data as String));
|
||||
|
||||
if (kDebugMode) {
|
||||
print('接收消息: ${message.type}');
|
||||
}
|
||||
|
||||
if (message.type == MessageType.ping) {
|
||||
_sendMessage(MessageType.pong);
|
||||
return;
|
||||
}
|
||||
|
||||
state = _getNextState(message);
|
||||
} catch (e, stackTrace) {
|
||||
if (kDebugMode) {
|
||||
print('解析消息失败: $e\n$stackTrace');
|
||||
}
|
||||
state = state.copyWith(error: '处理消息时发生意外错误。');
|
||||
}
|
||||
}
|
||||
|
||||
OnlineGameState _getNextState(NetworkMessage message) {
|
||||
switch (message.type) {
|
||||
case MessageType.roomCreated:
|
||||
case MessageType.roomJoined:
|
||||
final roomInfo = RoomInfo.fromJson(message.data!['roomInfo']);
|
||||
final me = roomInfo.players.firstWhere((p) => p.playerId == _playerId,
|
||||
orElse: () => throw Exception("Could not find myself in player list"));
|
||||
return state.copyWith(
|
||||
roomInfo: roomInfo,
|
||||
currentPlayer: me,
|
||||
mySymbol: me.playerSymbol,
|
||||
isJoiningRoom: false,
|
||||
error: null,
|
||||
gameState: GameState.fromJson(message.data!['gameState']),
|
||||
);
|
||||
|
||||
case MessageType.roomLeft:
|
||||
return state.copyWith(
|
||||
roomInfo: null,
|
||||
currentPlayer: null,
|
||||
mySymbol: null,
|
||||
gameState: const GameState(),
|
||||
);
|
||||
|
||||
case MessageType.playerJoined:
|
||||
case MessageType.playerLeft:
|
||||
final roomInfo = RoomInfo.fromJson(message.data!['roomInfo']);
|
||||
return state.copyWith(
|
||||
roomInfo: roomInfo,
|
||||
gameState: GameState.fromJson(message.data!['gameState']),
|
||||
);
|
||||
|
||||
case MessageType.gameUpdate:
|
||||
case MessageType.gameMove:
|
||||
case MessageType.gameReset:
|
||||
case MessageType.gameOver:
|
||||
return state.copyWith(
|
||||
gameState: GameState.fromJson(message.data!['gameState']),
|
||||
roomInfo: RoomInfo.fromJson(message.data!['roomInfo']),
|
||||
);
|
||||
|
||||
case MessageType.error:
|
||||
return state.copyWith(
|
||||
error: message.data?['message'] ?? '来自服务器的未知错误',
|
||||
isJoiningRoom: false,
|
||||
);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
366
lib/providers/online_game.freezed.dart
Normal file
366
lib/providers/online_game.freezed.dart
Normal file
@ -0,0 +1,366 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'online_game.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
|
||||
);
|
||||
|
||||
/// @nodoc
|
||||
mixin _$OnlineGameState {
|
||||
GameState get gameState => throw _privateConstructorUsedError;
|
||||
ConnectionStatus get connectionStatus => throw _privateConstructorUsedError;
|
||||
RoomInfo? get roomInfo => throw _privateConstructorUsedError;
|
||||
String? get error => throw _privateConstructorUsedError;
|
||||
PlayerInfo? get currentPlayer => throw _privateConstructorUsedError;
|
||||
Player? get mySymbol => throw _privateConstructorUsedError;
|
||||
bool get isJoiningRoom => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of OnlineGameState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$OnlineGameStateCopyWith<OnlineGameState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $OnlineGameStateCopyWith<$Res> {
|
||||
factory $OnlineGameStateCopyWith(
|
||||
OnlineGameState value,
|
||||
$Res Function(OnlineGameState) then,
|
||||
) = _$OnlineGameStateCopyWithImpl<$Res, OnlineGameState>;
|
||||
@useResult
|
||||
$Res call({
|
||||
GameState gameState,
|
||||
ConnectionStatus connectionStatus,
|
||||
RoomInfo? roomInfo,
|
||||
String? error,
|
||||
PlayerInfo? currentPlayer,
|
||||
Player? mySymbol,
|
||||
bool isJoiningRoom,
|
||||
});
|
||||
|
||||
$GameStateCopyWith<$Res> get gameState;
|
||||
$RoomInfoCopyWith<$Res>? get roomInfo;
|
||||
$PlayerInfoCopyWith<$Res>? get currentPlayer;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$OnlineGameStateCopyWithImpl<$Res, $Val extends OnlineGameState>
|
||||
implements $OnlineGameStateCopyWith<$Res> {
|
||||
_$OnlineGameStateCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of OnlineGameState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? gameState = null,
|
||||
Object? connectionStatus = null,
|
||||
Object? roomInfo = freezed,
|
||||
Object? error = freezed,
|
||||
Object? currentPlayer = freezed,
|
||||
Object? mySymbol = freezed,
|
||||
Object? isJoiningRoom = null,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
gameState: null == gameState
|
||||
? _value.gameState
|
||||
: gameState // ignore: cast_nullable_to_non_nullable
|
||||
as GameState,
|
||||
connectionStatus: null == connectionStatus
|
||||
? _value.connectionStatus
|
||||
: connectionStatus // ignore: cast_nullable_to_non_nullable
|
||||
as ConnectionStatus,
|
||||
roomInfo: freezed == roomInfo
|
||||
? _value.roomInfo
|
||||
: roomInfo // ignore: cast_nullable_to_non_nullable
|
||||
as RoomInfo?,
|
||||
error: freezed == error
|
||||
? _value.error
|
||||
: error // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
currentPlayer: freezed == currentPlayer
|
||||
? _value.currentPlayer
|
||||
: currentPlayer // ignore: cast_nullable_to_non_nullable
|
||||
as PlayerInfo?,
|
||||
mySymbol: freezed == mySymbol
|
||||
? _value.mySymbol
|
||||
: mySymbol // ignore: cast_nullable_to_non_nullable
|
||||
as Player?,
|
||||
isJoiningRoom: null == isJoiningRoom
|
||||
? _value.isJoiningRoom
|
||||
: isJoiningRoom // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a copy of OnlineGameState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$GameStateCopyWith<$Res> get gameState {
|
||||
return $GameStateCopyWith<$Res>(_value.gameState, (value) {
|
||||
return _then(_value.copyWith(gameState: value) as $Val);
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of OnlineGameState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$RoomInfoCopyWith<$Res>? get roomInfo {
|
||||
if (_value.roomInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $RoomInfoCopyWith<$Res>(_value.roomInfo!, (value) {
|
||||
return _then(_value.copyWith(roomInfo: value) as $Val);
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of OnlineGameState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$PlayerInfoCopyWith<$Res>? get currentPlayer {
|
||||
if (_value.currentPlayer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $PlayerInfoCopyWith<$Res>(_value.currentPlayer!, (value) {
|
||||
return _then(_value.copyWith(currentPlayer: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$OnlineGameStateImplCopyWith<$Res>
|
||||
implements $OnlineGameStateCopyWith<$Res> {
|
||||
factory _$$OnlineGameStateImplCopyWith(
|
||||
_$OnlineGameStateImpl value,
|
||||
$Res Function(_$OnlineGameStateImpl) then,
|
||||
) = __$$OnlineGameStateImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({
|
||||
GameState gameState,
|
||||
ConnectionStatus connectionStatus,
|
||||
RoomInfo? roomInfo,
|
||||
String? error,
|
||||
PlayerInfo? currentPlayer,
|
||||
Player? mySymbol,
|
||||
bool isJoiningRoom,
|
||||
});
|
||||
|
||||
@override
|
||||
$GameStateCopyWith<$Res> get gameState;
|
||||
@override
|
||||
$RoomInfoCopyWith<$Res>? get roomInfo;
|
||||
@override
|
||||
$PlayerInfoCopyWith<$Res>? get currentPlayer;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$OnlineGameStateImplCopyWithImpl<$Res>
|
||||
extends _$OnlineGameStateCopyWithImpl<$Res, _$OnlineGameStateImpl>
|
||||
implements _$$OnlineGameStateImplCopyWith<$Res> {
|
||||
__$$OnlineGameStateImplCopyWithImpl(
|
||||
_$OnlineGameStateImpl _value,
|
||||
$Res Function(_$OnlineGameStateImpl) _then,
|
||||
) : super(_value, _then);
|
||||
|
||||
/// Create a copy of OnlineGameState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? gameState = null,
|
||||
Object? connectionStatus = null,
|
||||
Object? roomInfo = freezed,
|
||||
Object? error = freezed,
|
||||
Object? currentPlayer = freezed,
|
||||
Object? mySymbol = freezed,
|
||||
Object? isJoiningRoom = null,
|
||||
}) {
|
||||
return _then(
|
||||
_$OnlineGameStateImpl(
|
||||
gameState: null == gameState
|
||||
? _value.gameState
|
||||
: gameState // ignore: cast_nullable_to_non_nullable
|
||||
as GameState,
|
||||
connectionStatus: null == connectionStatus
|
||||
? _value.connectionStatus
|
||||
: connectionStatus // ignore: cast_nullable_to_non_nullable
|
||||
as ConnectionStatus,
|
||||
roomInfo: freezed == roomInfo
|
||||
? _value.roomInfo
|
||||
: roomInfo // ignore: cast_nullable_to_non_nullable
|
||||
as RoomInfo?,
|
||||
error: freezed == error
|
||||
? _value.error
|
||||
: error // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
currentPlayer: freezed == currentPlayer
|
||||
? _value.currentPlayer
|
||||
: currentPlayer // ignore: cast_nullable_to_non_nullable
|
||||
as PlayerInfo?,
|
||||
mySymbol: freezed == mySymbol
|
||||
? _value.mySymbol
|
||||
: mySymbol // ignore: cast_nullable_to_non_nullable
|
||||
as Player?,
|
||||
isJoiningRoom: null == isJoiningRoom
|
||||
? _value.isJoiningRoom
|
||||
: isJoiningRoom // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$OnlineGameStateImpl extends _OnlineGameState
|
||||
with DiagnosticableTreeMixin {
|
||||
const _$OnlineGameStateImpl({
|
||||
this.gameState = const GameState(),
|
||||
this.connectionStatus = ConnectionStatus.initial,
|
||||
this.roomInfo,
|
||||
this.error,
|
||||
this.currentPlayer,
|
||||
this.mySymbol,
|
||||
this.isJoiningRoom = false,
|
||||
}) : super._();
|
||||
|
||||
@override
|
||||
@JsonKey()
|
||||
final GameState gameState;
|
||||
@override
|
||||
@JsonKey()
|
||||
final ConnectionStatus connectionStatus;
|
||||
@override
|
||||
final RoomInfo? roomInfo;
|
||||
@override
|
||||
final String? error;
|
||||
@override
|
||||
final PlayerInfo? currentPlayer;
|
||||
@override
|
||||
final Player? mySymbol;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool isJoiningRoom;
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'OnlineGameState(gameState: $gameState, connectionStatus: $connectionStatus, roomInfo: $roomInfo, error: $error, currentPlayer: $currentPlayer, mySymbol: $mySymbol, isJoiningRoom: $isJoiningRoom)';
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'OnlineGameState'))
|
||||
..add(DiagnosticsProperty('gameState', gameState))
|
||||
..add(DiagnosticsProperty('connectionStatus', connectionStatus))
|
||||
..add(DiagnosticsProperty('roomInfo', roomInfo))
|
||||
..add(DiagnosticsProperty('error', error))
|
||||
..add(DiagnosticsProperty('currentPlayer', currentPlayer))
|
||||
..add(DiagnosticsProperty('mySymbol', mySymbol))
|
||||
..add(DiagnosticsProperty('isJoiningRoom', isJoiningRoom));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$OnlineGameStateImpl &&
|
||||
(identical(other.gameState, gameState) ||
|
||||
other.gameState == gameState) &&
|
||||
(identical(other.connectionStatus, connectionStatus) ||
|
||||
other.connectionStatus == connectionStatus) &&
|
||||
(identical(other.roomInfo, roomInfo) ||
|
||||
other.roomInfo == roomInfo) &&
|
||||
(identical(other.error, error) || other.error == error) &&
|
||||
(identical(other.currentPlayer, currentPlayer) ||
|
||||
other.currentPlayer == currentPlayer) &&
|
||||
(identical(other.mySymbol, mySymbol) ||
|
||||
other.mySymbol == mySymbol) &&
|
||||
(identical(other.isJoiningRoom, isJoiningRoom) ||
|
||||
other.isJoiningRoom == isJoiningRoom));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
gameState,
|
||||
connectionStatus,
|
||||
roomInfo,
|
||||
error,
|
||||
currentPlayer,
|
||||
mySymbol,
|
||||
isJoiningRoom,
|
||||
);
|
||||
|
||||
/// Create a copy of OnlineGameState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$OnlineGameStateImplCopyWith<_$OnlineGameStateImpl> get copyWith =>
|
||||
__$$OnlineGameStateImplCopyWithImpl<_$OnlineGameStateImpl>(
|
||||
this,
|
||||
_$identity,
|
||||
);
|
||||
}
|
||||
|
||||
abstract class _OnlineGameState extends OnlineGameState {
|
||||
const factory _OnlineGameState({
|
||||
final GameState gameState,
|
||||
final ConnectionStatus connectionStatus,
|
||||
final RoomInfo? roomInfo,
|
||||
final String? error,
|
||||
final PlayerInfo? currentPlayer,
|
||||
final Player? mySymbol,
|
||||
final bool isJoiningRoom,
|
||||
}) = _$OnlineGameStateImpl;
|
||||
const _OnlineGameState._() : super._();
|
||||
|
||||
@override
|
||||
GameState get gameState;
|
||||
@override
|
||||
ConnectionStatus get connectionStatus;
|
||||
@override
|
||||
RoomInfo? get roomInfo;
|
||||
@override
|
||||
String? get error;
|
||||
@override
|
||||
PlayerInfo? get currentPlayer;
|
||||
@override
|
||||
Player? get mySymbol;
|
||||
@override
|
||||
bool get isJoiningRoom;
|
||||
|
||||
/// Create a copy of OnlineGameState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$OnlineGameStateImplCopyWith<_$OnlineGameStateImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
28
lib/providers/online_game.g.dart
Normal file
28
lib/providers/online_game.g.dart
Normal file
@ -0,0 +1,28 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'online_game.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$onlineGameHash() => r'e7e7e21c2feb1aa717ee891add499a18ea683e43';
|
||||
|
||||
/// 在线游戏的StateNotifier,负责所有业务逻辑
|
||||
///
|
||||
/// Copied from [OnlineGame].
|
||||
@ProviderFor(OnlineGame)
|
||||
final onlineGameProvider =
|
||||
AutoDisposeNotifierProvider<OnlineGame, OnlineGameState>.internal(
|
||||
OnlineGame.new,
|
||||
name: r'onlineGameProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$onlineGameHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$OnlineGame = AutoDisposeNotifier<OnlineGameState>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
@ -1,410 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../models/game_state.dart';
|
||||
import '../models/network_message.dart';
|
||||
import '../services/websocket_service.dart';
|
||||
import 'package:random_name_generator/random_name_generator.dart';
|
||||
|
||||
/// 在线游戏状态
|
||||
class OnlineGameState {
|
||||
final GameState gameState;
|
||||
final RoomInfo? roomInfo;
|
||||
final bool isConnected;
|
||||
final bool isConnecting;
|
||||
final String? error;
|
||||
final PlayerInfo? currentPlayer;
|
||||
final Player? mySymbol;
|
||||
final bool isMyTurn;
|
||||
final bool isJoiningRoom;
|
||||
|
||||
const OnlineGameState({
|
||||
required this.gameState,
|
||||
this.roomInfo,
|
||||
required this.isConnected,
|
||||
required this.isConnecting,
|
||||
this.error,
|
||||
this.currentPlayer,
|
||||
this.mySymbol,
|
||||
required this.isMyTurn,
|
||||
this.isJoiningRoom = false,
|
||||
});
|
||||
|
||||
OnlineGameState copyWith({
|
||||
GameState? gameState,
|
||||
RoomInfo? roomInfo,
|
||||
bool? isConnected,
|
||||
bool? isConnecting,
|
||||
String? error,
|
||||
PlayerInfo? currentPlayer,
|
||||
Player? mySymbol,
|
||||
bool? isMyTurn,
|
||||
bool? isJoiningRoom,
|
||||
bool clearRoomInfo = false,
|
||||
bool clearError = false,
|
||||
bool clearCurrentPlayer = false,
|
||||
bool clearMySymbol = false,
|
||||
}) {
|
||||
return OnlineGameState(
|
||||
gameState: gameState ?? this.gameState,
|
||||
roomInfo: clearRoomInfo ? null : roomInfo ?? this.roomInfo,
|
||||
isConnected: isConnected ?? this.isConnected,
|
||||
isConnecting: isConnecting ?? this.isConnecting,
|
||||
error: clearError ? null : error ?? this.error,
|
||||
currentPlayer: clearCurrentPlayer
|
||||
? null
|
||||
: currentPlayer ?? this.currentPlayer,
|
||||
mySymbol: clearMySymbol ? null : mySymbol ?? this.mySymbol,
|
||||
isMyTurn: isMyTurn ?? this.isMyTurn,
|
||||
isJoiningRoom: isJoiningRoom ?? this.isJoiningRoom,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OnlineGameNotifier extends StateNotifier<OnlineGameState> {
|
||||
final WebSocketService _webSocketService = WebSocketService();
|
||||
StreamSubscription? _messageSubscription;
|
||||
StreamSubscription? _connectionSubscription;
|
||||
StreamSubscription? _errorSubscription;
|
||||
|
||||
OnlineGameNotifier()
|
||||
: super(
|
||||
const OnlineGameState(
|
||||
gameState: GameState(),
|
||||
isConnected: false,
|
||||
isConnecting: false,
|
||||
isMyTurn: false,
|
||||
isJoiningRoom: false,
|
||||
),
|
||||
) {
|
||||
_setupSubscriptions();
|
||||
}
|
||||
|
||||
/// 设置事件监听
|
||||
void _setupSubscriptions() {
|
||||
_messageSubscription = _webSocketService.messageStream.listen(
|
||||
_handleMessage,
|
||||
);
|
||||
_connectionSubscription = _webSocketService.connectionStream.listen(
|
||||
_handleConnection,
|
||||
);
|
||||
_errorSubscription = _webSocketService.errorStream.listen(_handleError);
|
||||
}
|
||||
|
||||
/// 连接服务器
|
||||
Future<bool> connect({String? serverUrl}) async {
|
||||
// 避免在 widget 构建期间立即修改状态
|
||||
await Future.microtask(() {
|
||||
state = state.copyWith(isConnecting: true, clearError: true);
|
||||
});
|
||||
|
||||
final success = await _webSocketService.connect(serverUrl: serverUrl);
|
||||
if (!success) {
|
||||
state = state.copyWith(isConnecting: false, error: '连接失败');
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/// 断开连接
|
||||
Future<void> disconnect() async {
|
||||
await _webSocketService.disconnect();
|
||||
state = const OnlineGameState(
|
||||
gameState: GameState(),
|
||||
isConnected: false,
|
||||
isConnecting: false,
|
||||
isMyTurn: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// 创建房间
|
||||
Future<void> createRoom(String playerName) async {
|
||||
if (!state.isConnected) return;
|
||||
var finalPlayerName = playerName;
|
||||
if (finalPlayerName.isEmpty) {
|
||||
finalPlayerName = RandomNames(Zone.us).name();
|
||||
}
|
||||
await _webSocketService.createRoom(finalPlayerName);
|
||||
}
|
||||
|
||||
/// 加入房间
|
||||
Future<void> joinRoom(String roomId, String playerName) async {
|
||||
if (!state.isConnected) return;
|
||||
var finalPlayerName = playerName;
|
||||
if (finalPlayerName.isEmpty) {
|
||||
finalPlayerName = RandomNames(Zone.us).name();
|
||||
}
|
||||
await Future.microtask(() {
|
||||
state = state.copyWith(isJoiningRoom: true, clearError: true);
|
||||
});
|
||||
await _webSocketService.joinRoom(roomId, finalPlayerName);
|
||||
|
||||
// 添加超时机制
|
||||
Timer(const Duration(seconds: 5), () {
|
||||
if (state.isJoiningRoom) {
|
||||
state = state.copyWith(isJoiningRoom: false, error: '加入房间超时');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 离开房间
|
||||
Future<void> leaveRoom() async {
|
||||
await _webSocketService.leaveRoom();
|
||||
|
||||
// 添加超时机制,如果3秒内没有收到服务器确认,强制清除状态
|
||||
Timer(const Duration(seconds: 3), () {
|
||||
if (state.roomInfo != null) {
|
||||
// 如果3秒后仍然在房间中,强制清除状态
|
||||
state = state.copyWith(
|
||||
clearRoomInfo: true,
|
||||
clearCurrentPlayer: true,
|
||||
clearMySymbol: true,
|
||||
gameState: const GameState(),
|
||||
isMyTurn: false,
|
||||
isJoiningRoom: false,
|
||||
clearError: true,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 发送游戏移动
|
||||
Future<void> makeMove(int position) async {
|
||||
if (!state.isMyTurn ||
|
||||
state.mySymbol == null ||
|
||||
!state.gameState.canMakeMove(position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _webSocketService.sendGameMove(position, state.mySymbol!);
|
||||
}
|
||||
|
||||
/// 重置游戏
|
||||
Future<void> resetGame() async {
|
||||
await _webSocketService.resetGame();
|
||||
}
|
||||
|
||||
/// 处理网络消息
|
||||
void _handleMessage(NetworkMessage message) {
|
||||
switch (message.type) {
|
||||
case MessageType.roomCreated:
|
||||
_handleRoomCreated(message);
|
||||
break;
|
||||
case MessageType.roomJoined:
|
||||
_handleRoomJoined(message);
|
||||
break;
|
||||
case MessageType.roomLeft:
|
||||
_handleRoomLeft(message);
|
||||
break;
|
||||
case MessageType.playerJoined:
|
||||
_handlePlayerJoined(message);
|
||||
break;
|
||||
case MessageType.playerLeft:
|
||||
_handlePlayerLeft(message);
|
||||
break;
|
||||
case MessageType.gameUpdate:
|
||||
_handleGameUpdate(message);
|
||||
break;
|
||||
case MessageType.gameMove:
|
||||
_handleGameMove(message);
|
||||
break;
|
||||
case MessageType.gameReset:
|
||||
_handleGameReset(message);
|
||||
break;
|
||||
case MessageType.gameOver:
|
||||
_handleGameOver(message);
|
||||
break;
|
||||
case MessageType.error:
|
||||
_handleServerError(message);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理房间创建成功
|
||||
void _handleRoomCreated(NetworkMessage message) {
|
||||
final data = message.data;
|
||||
if (data != null) {
|
||||
final roomInfo = RoomInfo.fromJson(data);
|
||||
final myPlayer = roomInfo.players.firstWhere(
|
||||
(p) => p.playerId == _webSocketService.playerId,
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
roomInfo: roomInfo,
|
||||
currentPlayer: myPlayer,
|
||||
mySymbol: myPlayer.playerSymbol,
|
||||
isMyTurn: roomInfo.gameState?.currentPlayer == myPlayer.playerSymbol,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理加入房间成功
|
||||
void _handleRoomJoined(NetworkMessage message) {
|
||||
final data = message.data;
|
||||
if (data != null) {
|
||||
final roomInfo = RoomInfo.fromJson(data);
|
||||
final myPlayer = roomInfo.players.firstWhere(
|
||||
(p) => p.playerId == _webSocketService.playerId,
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
roomInfo: roomInfo,
|
||||
currentPlayer: myPlayer,
|
||||
mySymbol: myPlayer.playerSymbol,
|
||||
gameState: roomInfo.gameState ?? const GameState(),
|
||||
isMyTurn:
|
||||
(roomInfo.gameState?.currentPlayer ?? Player.x) ==
|
||||
myPlayer.playerSymbol,
|
||||
isJoiningRoom: false,
|
||||
clearError: true,
|
||||
);
|
||||
} else {
|
||||
state = state.copyWith(isJoiningRoom: false, error: '加入房间失败:无效数据');
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理离开房间成功
|
||||
void _handleRoomLeft(NetworkMessage message) {
|
||||
// 清除房间相关状态
|
||||
state = state.copyWith(
|
||||
clearRoomInfo: true,
|
||||
clearCurrentPlayer: true,
|
||||
clearMySymbol: true,
|
||||
gameState: const GameState(),
|
||||
isMyTurn: false,
|
||||
isJoiningRoom: false,
|
||||
clearError: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// 处理玩家加入
|
||||
void _handlePlayerJoined(NetworkMessage message) {
|
||||
final data = message.data;
|
||||
if (data != null && state.roomInfo != null) {
|
||||
final newPlayer = PlayerInfo.fromJson(data);
|
||||
final updatedPlayers = [...state.roomInfo!.players, newPlayer];
|
||||
final updatedRoom = state.roomInfo!.copyWith(players: updatedPlayers);
|
||||
|
||||
state = state.copyWith(roomInfo: updatedRoom);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理玩家离开
|
||||
void _handlePlayerLeft(NetworkMessage message) {
|
||||
final playerId = message.playerId;
|
||||
if (playerId != null && state.roomInfo != null) {
|
||||
final updatedPlayers = state.roomInfo!.players
|
||||
.where((p) => p.playerId != playerId)
|
||||
.toList();
|
||||
final updatedRoom = state.roomInfo!.copyWith(players: updatedPlayers);
|
||||
|
||||
state = state.copyWith(roomInfo: updatedRoom);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理游戏更新
|
||||
void _handleGameUpdate(NetworkMessage message) {
|
||||
final data = message.data;
|
||||
if (data != null) {
|
||||
final gameState = GameState.fromJson(data);
|
||||
final isMyTurn = gameState.currentPlayer == state.mySymbol;
|
||||
|
||||
state = state.copyWith(gameState: gameState, isMyTurn: isMyTurn);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理游戏移动
|
||||
void _handleGameMove(NetworkMessage message) {
|
||||
final data = message.data;
|
||||
if (data != null) {
|
||||
final moveData = GameMoveData.fromJson(data);
|
||||
|
||||
// 更新游戏棋盘
|
||||
final newBoard = List<Player?>.from(state.gameState.board);
|
||||
newBoard[moveData.position] = moveData.player;
|
||||
|
||||
final newGameState = state.gameState.copyWith(board: newBoard);
|
||||
final updatedGameState = newGameState.copyWith(
|
||||
currentPlayer: newGameState.currentPlayer == Player.x
|
||||
? Player.o
|
||||
: Player.x,
|
||||
status: newGameState.checkWinStatus(),
|
||||
);
|
||||
|
||||
final isMyTurn = updatedGameState.currentPlayer == state.mySymbol;
|
||||
|
||||
state = state.copyWith(gameState: updatedGameState, isMyTurn: isMyTurn);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理游戏重置
|
||||
void _handleGameReset(NetworkMessage message) {
|
||||
state = state.copyWith(
|
||||
gameState: const GameState(),
|
||||
isMyTurn: state.mySymbol == Player.x,
|
||||
);
|
||||
}
|
||||
|
||||
/// 处理游戏结束
|
||||
void _handleGameOver(NetworkMessage message) {
|
||||
final data = message.data;
|
||||
if (data != null) {
|
||||
final gameState = GameState.fromJson(data);
|
||||
state = state.copyWith(gameState: gameState, isMyTurn: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理服务器错误
|
||||
void _handleServerError(NetworkMessage message) {
|
||||
state = state.copyWith(
|
||||
error: message.error,
|
||||
isConnecting: false,
|
||||
isJoiningRoom: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// 处理连接状态变化
|
||||
void _handleConnection(bool connected) {
|
||||
state = state.copyWith(isConnected: connected, isConnecting: false);
|
||||
|
||||
if (!connected) {
|
||||
state = state.copyWith(
|
||||
clearRoomInfo: true,
|
||||
clearCurrentPlayer: true,
|
||||
clearMySymbol: true,
|
||||
gameState: const GameState(),
|
||||
isMyTurn: false,
|
||||
isJoiningRoom: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理错误
|
||||
void _handleError(String error) {
|
||||
state = state.copyWith(
|
||||
error: error,
|
||||
isConnecting: false,
|
||||
isJoiningRoom: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// 清除错误
|
||||
void clearError() {
|
||||
state = state.copyWith(clearError: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_messageSubscription?.cancel();
|
||||
_connectionSubscription?.cancel();
|
||||
_errorSubscription?.cancel();
|
||||
_webSocketService.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// 在线游戏状态提供者
|
||||
final onlineGameProvider =
|
||||
StateNotifierProvider<OnlineGameNotifier, OnlineGameState>((ref) {
|
||||
return OnlineGameNotifier();
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../models/game_state.dart';
|
||||
import '../providers/game_provider.dart';
|
||||
import '../providers/game.dart';
|
||||
import '../widgets/game_board.dart';
|
||||
import '../widgets/victory_dialog.dart';
|
||||
import '../widgets/game_menu.dart';
|
||||
@ -81,7 +81,7 @@ class _GameScreenState extends ConsumerState<GameScreen> {
|
||||
Widget _buildAppBar(
|
||||
BuildContext context,
|
||||
GameState gameState,
|
||||
GameNotifier gameNotifier,
|
||||
Game gameNotifier,
|
||||
) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
@ -120,7 +120,7 @@ class _GameScreenState extends ConsumerState<GameScreen> {
|
||||
void _showGameMenu(
|
||||
BuildContext context,
|
||||
GameState gameState,
|
||||
GameNotifier gameNotifier,
|
||||
Game gameNotifier,
|
||||
) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@ -135,15 +135,15 @@ class _GameScreenState extends ConsumerState<GameScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showVictoryDialog(GameState gameState, GameNotifier gameNotifier) {
|
||||
void _showVictoryDialog(GameState gameState, Game gameNotifier) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) => VictoryDialog(
|
||||
gameStatus: gameState.status,
|
||||
isAiMode: gameState.isAiMode,
|
||||
onPlayAgain: () => gameNotifier.resetGame(aiMode: gameState.isAiMode),
|
||||
onChangeMode: () => gameNotifier.resetGame(aiMode: !gameState.isAiMode),
|
||||
onGoToMenu: () => Navigator.of(context).pop(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../models/network_message.dart';
|
||||
import '../providers/online_game_provider.dart';
|
||||
import '../providers/online_game.dart';
|
||||
import '../models/game_state.dart';
|
||||
import '../widgets/game_board.dart';
|
||||
import '../widgets/online_victory_dialog.dart';
|
||||
|
@ -1,11 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../config/app_config.dart';
|
||||
import '../providers/online_game_provider.dart';
|
||||
import 'online_game_screen.dart';
|
||||
import 'package:random_name_generator/random_name_generator.dart';
|
||||
|
||||
import '../config/app_config.dart';
|
||||
import '../providers/online_game.dart';
|
||||
import 'online_game_screen.dart';
|
||||
|
||||
class OnlineLobbyScreen extends ConsumerStatefulWidget {
|
||||
const OnlineLobbyScreen({super.key});
|
||||
|
||||
@ -13,51 +14,52 @@ class OnlineLobbyScreen extends ConsumerStatefulWidget {
|
||||
ConsumerState<OnlineLobbyScreen> createState() => _OnlineLobbyScreenState();
|
||||
}
|
||||
|
||||
class _OnlineLobbyScreenState extends ConsumerState<OnlineLobbyScreen> {
|
||||
class _OnlineLobbyScreenState extends ConsumerState<OnlineLobbyScreen>
|
||||
with WidgetsBindingObserver {
|
||||
final _playerNameController = TextEditingController();
|
||||
final _roomIdController = TextEditingController();
|
||||
bool _isConnecting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 自动为玩家生成一个随机名字
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
_playerNameController.text = RandomNames(Zone.us).name();
|
||||
// 监听文本变化以更新按钮状态
|
||||
_playerNameController.addListener(() => setState(() {}));
|
||||
_roomIdController.addListener(() => setState(() {}));
|
||||
// 延迟连接以避免在 widget 构建期间修改 provider
|
||||
Future(() => _connectToServer());
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_connectIfNeeded();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_playerNameController.dispose();
|
||||
_roomIdController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 清理当前状态(例如用户点击返回主菜单时)
|
||||
void _cleanupState() {
|
||||
// 如果当前在房间中,先离开房间
|
||||
final onlineState = ref.read(onlineGameProvider);
|
||||
if (onlineState.roomInfo != null) {
|
||||
ref.read(onlineGameProvider.notifier).leaveRoom();
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed && mounted) {
|
||||
// 当应用恢复时,尝试连接(如果需要)
|
||||
_connectIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
/// 连接到服务器
|
||||
Future<void> _connectToServer() async {
|
||||
setState(() => _isConnecting = true);
|
||||
/// 如果尚未连接,则连接到服务器
|
||||
Future<void> _connectIfNeeded() async {
|
||||
// Notifier中的connect方法现在是幂等的,可以安全地多次调用
|
||||
await ref.read(onlineGameProvider.notifier).connect();
|
||||
}
|
||||
|
||||
final notifier = ref.read(onlineGameProvider.notifier);
|
||||
final success = await notifier.connect(serverUrl: AppConfig.serverUrl);
|
||||
|
||||
setState(() => _isConnecting = false);
|
||||
|
||||
if (!success && mounted) {
|
||||
_showErrorDialog('连接失败', '无法连接到游戏服务器,请检查网络连接');
|
||||
}
|
||||
/// 清理当前状态(例如用户点击返回主菜单时)
|
||||
void _cleanupState() {
|
||||
// Notifier现在有一个专用的清理方法
|
||||
ref.read(onlineGameProvider.notifier).cleanup();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -67,9 +69,12 @@ class _OnlineLobbyScreenState extends ConsumerState<OnlineLobbyScreen> {
|
||||
// 检查是否已经在房间中,如果是则直接跳转到游戏屏幕
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (onlineState.roomInfo != null && onlineState.currentPlayer != null) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => const OnlineGameScreen()),
|
||||
);
|
||||
// 避免在build过程中导航
|
||||
if (ModalRoute.of(context)?.isCurrent ?? false) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => const OnlineGameScreen()),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -85,7 +90,7 @@ class _OnlineLobbyScreenState extends ConsumerState<OnlineLobbyScreen> {
|
||||
}
|
||||
|
||||
// 显示错误信息
|
||||
if (next.error != null) {
|
||||
if (next.error != null && previous?.error != next.error) {
|
||||
_showErrorDialog('错误', next.error!);
|
||||
// 延迟清除错误状态
|
||||
Future.microtask(() {
|
||||
@ -109,35 +114,23 @@ class _OnlineLobbyScreenState extends ConsumerState<OnlineLobbyScreen> {
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// 标题
|
||||
_buildTitle(),
|
||||
_buildTitleAndStatusInfo(onlineState),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// 连接状态
|
||||
_buildConnectionStatus(onlineState),
|
||||
|
||||
// 开发模式:显示服务器地址
|
||||
if (AppConfig.isDebug) _buildServerInfo(),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 玩家姓名输入
|
||||
_buildPlayerNameInput(),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// 房间操作
|
||||
if (onlineState.isConnected) ...[
|
||||
_buildCreateRoomSection(),
|
||||
const SizedBox(height: 20),
|
||||
_buildJoinRoomSection(),
|
||||
],
|
||||
|
||||
const SizedBox(height: 40),
|
||||
_buildCreateRoomSection(),
|
||||
const SizedBox(height: 10),
|
||||
_buildJoinRoomSection(),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
// 返回按钮
|
||||
_buildBackButton(),
|
||||
],
|
||||
@ -150,118 +143,141 @@ class _OnlineLobbyScreenState extends ConsumerState<OnlineLobbyScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTitle() {
|
||||
Widget _buildTitleAndStatusInfo(OnlineGameState onlineState) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.4),
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Text(
|
||||
'🌐 在线对战',
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
'🌐 在线对战',
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
// 连接状态
|
||||
_buildConnectionStatus(onlineState),
|
||||
|
||||
// 开发模式:显示服务器地址
|
||||
if (AppConfig.isDebug) _buildServerInfo(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConnectionStatus(OnlineGameState state) {
|
||||
final Widget statusWidget;
|
||||
final Color statusColor;
|
||||
final String statusText;
|
||||
|
||||
if (_isConnecting || state.isConnecting) {
|
||||
statusColor = Colors.orange;
|
||||
statusText = '正在连接...';
|
||||
statusWidget = const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
);
|
||||
} else if (state.isConnected) {
|
||||
statusColor = Colors.green;
|
||||
statusText = '已连接服务器';
|
||||
statusWidget = const Icon(Icons.check_circle, size: 20);
|
||||
} else {
|
||||
statusColor = Colors.red;
|
||||
statusText = '连接断开';
|
||||
statusWidget = const Icon(Icons.error, size: 20);
|
||||
switch (state.connectionStatus) {
|
||||
case ConnectionStatus.initial:
|
||||
case ConnectionStatus.connecting:
|
||||
statusText = '正在连接...';
|
||||
statusWidget = const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.orange,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case ConnectionStatus.connected:
|
||||
statusText = '已连接服务器';
|
||||
statusWidget = const Icon(
|
||||
Icons.check_circle,
|
||||
size: 20,
|
||||
color: Colors.green,
|
||||
);
|
||||
break;
|
||||
case ConnectionStatus.disconnected:
|
||||
statusText = '连接断开';
|
||||
statusWidget = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.error, size: 20, color: Colors.red),
|
||||
const SizedBox(width: 4),
|
||||
TextButton(
|
||||
onPressed: _connectIfNeeded,
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
minimumSize: Size.zero,
|
||||
),
|
||||
child: const Text(
|
||||
'重连',
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(color: statusColor.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
statusWidget,
|
||||
const SizedBox(width: 8),
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (statusWidget is! Row) statusWidget,
|
||||
if (statusWidget is Row) statusWidget else const SizedBox(width: 8),
|
||||
if (statusWidget is! Row)
|
||||
Text(
|
||||
statusText,
|
||||
style: TextStyle(color: statusColor, fontWeight: FontWeight.w600),
|
||||
style: TextStyle(
|
||||
color: statusWidget is Icon
|
||||
? (statusWidget.color)
|
||||
: Colors.orange,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildServerInfo() {
|
||||
final onlineState = ref.watch(onlineGameProvider);
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 15),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.blue.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 16, color: Colors.blue),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'开发模式 - 服务器: ${AppConfig.serverUrl}',
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'开发模式 - 服务器: ${AppConfig.serverUrl}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'调试: 连接=${onlineState.isConnected}, 连接中=${onlineState.isConnecting}, 错误=${onlineState.error ?? "无"}',
|
||||
style: const TextStyle(color: Colors.blue, fontSize: 10),
|
||||
),
|
||||
Text(
|
||||
'按钮: 可创建=${_canCreateRoom()}, 玩家名=${_playerNameController.text.trim()}',
|
||||
style: const TextStyle(color: Colors.blue, fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'调试: status=${onlineState.connectionStatus.name}, joining=${onlineState.isJoiningRoom}',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 10),
|
||||
),
|
||||
Text(
|
||||
'错误=${onlineState.error ?? "无"}',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 10),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -269,11 +285,11 @@ class _OnlineLobbyScreenState extends ConsumerState<OnlineLobbyScreen> {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.95),
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
@ -312,11 +328,11 @@ class _OnlineLobbyScreenState extends ConsumerState<OnlineLobbyScreen> {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.95),
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
@ -324,22 +340,30 @@ class _OnlineLobbyScreenState extends ConsumerState<OnlineLobbyScreen> {
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(Icons.add_circle, size: 48, color: Colors.green),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'创建房间',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF2E3A4B),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.add_circle, size: 48, color: Colors.green),
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'创建房间',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF2E3A4B),
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'创建一个新房间,邀请朋友加入',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'创建一个新房间,邀请朋友加入',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
@ -353,7 +377,7 @@ class _OnlineLobbyScreenState extends ConsumerState<OnlineLobbyScreen> {
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_canCreateRoom() ? '创建房间' : _getCreateRoomButtonText(),
|
||||
_getCreateRoomButtonText(),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -371,11 +395,11 @@ class _OnlineLobbyScreenState extends ConsumerState<OnlineLobbyScreen> {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.95),
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
@ -383,22 +407,31 @@ class _OnlineLobbyScreenState extends ConsumerState<OnlineLobbyScreen> {
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(Icons.meeting_room, size: 48, color: Colors.blue),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'加入房间',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF2E3A4B),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.meeting_room, size: 48, color: Colors.blue),
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'加入房间',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF2E3A4B),
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'输入6位房间号加入游戏',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'输入6位房间号加入游戏',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: _roomIdController,
|
||||
decoration: InputDecoration(
|
||||
@ -412,7 +445,7 @@ class _OnlineLobbyScreenState extends ConsumerState<OnlineLobbyScreen> {
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
@ -426,7 +459,7 @@ class _OnlineLobbyScreenState extends ConsumerState<OnlineLobbyScreen> {
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_canJoinRoom() ? '加入房间' : _getJoinRoomButtonText(),
|
||||
_getJoinRoomButtonText(),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -469,43 +502,56 @@ class _OnlineLobbyScreenState extends ConsumerState<OnlineLobbyScreen> {
|
||||
bool _canCreateRoom() {
|
||||
final onlineState = ref.watch(onlineGameProvider);
|
||||
return _playerNameController.text.trim().isNotEmpty &&
|
||||
onlineState.isConnected &&
|
||||
!onlineState.isConnecting;
|
||||
onlineState.connectionStatus == ConnectionStatus.connected &&
|
||||
!onlineState.isJoiningRoom;
|
||||
}
|
||||
|
||||
bool _canJoinRoom() {
|
||||
final onlineState = ref.watch(onlineGameProvider);
|
||||
return _playerNameController.text.trim().isNotEmpty &&
|
||||
_roomIdController.text.trim().length == 6 &&
|
||||
onlineState.isConnected &&
|
||||
!onlineState.isConnecting;
|
||||
onlineState.connectionStatus == ConnectionStatus.connected &&
|
||||
!onlineState.isJoiningRoom;
|
||||
}
|
||||
|
||||
String _getCreateRoomButtonText() {
|
||||
final onlineState = ref.watch(onlineGameProvider);
|
||||
if (!onlineState.isConnected && !onlineState.isConnecting) {
|
||||
return '未连接服务器';
|
||||
} else if (onlineState.isConnecting) {
|
||||
return '连接中...';
|
||||
} else if (_playerNameController.text.trim().isEmpty) {
|
||||
return '请输入玩家姓名';
|
||||
} else {
|
||||
return '创建房间';
|
||||
switch (onlineState.connectionStatus) {
|
||||
case ConnectionStatus.connecting:
|
||||
case ConnectionStatus.initial:
|
||||
return '连接中...';
|
||||
case ConnectionStatus.disconnected:
|
||||
return '未连接服务器';
|
||||
case ConnectionStatus.connected:
|
||||
if (onlineState.isJoiningRoom) {
|
||||
return '正在加入...';
|
||||
}
|
||||
if (_playerNameController.text.trim().isEmpty) {
|
||||
return '请输入玩家姓名';
|
||||
}
|
||||
return '创建房间';
|
||||
}
|
||||
}
|
||||
|
||||
String _getJoinRoomButtonText() {
|
||||
final onlineState = ref.watch(onlineGameProvider);
|
||||
if (!onlineState.isConnected && !onlineState.isConnecting) {
|
||||
return '未连接服务器';
|
||||
} else if (onlineState.isConnecting) {
|
||||
return '连接中...';
|
||||
} else if (_playerNameController.text.trim().isEmpty) {
|
||||
return '请输入玩家姓名';
|
||||
} else if (_roomIdController.text.trim().length != 6) {
|
||||
return '请输入6位房间号';
|
||||
} else {
|
||||
return '加入房间';
|
||||
switch (onlineState.connectionStatus) {
|
||||
case ConnectionStatus.connecting:
|
||||
case ConnectionStatus.initial:
|
||||
return '连接中...';
|
||||
case ConnectionStatus.disconnected:
|
||||
return '未连接服务器';
|
||||
case ConnectionStatus.connected:
|
||||
if (onlineState.isJoiningRoom) {
|
||||
return '正在加入...';
|
||||
}
|
||||
if (_playerNameController.text.trim().isEmpty) {
|
||||
return '请输入玩家姓名';
|
||||
}
|
||||
if (_roomIdController.text.trim().length != 6) {
|
||||
return '请输入6位房间号';
|
||||
}
|
||||
return '加入房间';
|
||||
}
|
||||
}
|
||||
|
||||
@ -526,6 +572,7 @@ class _OnlineLobbyScreenState extends ConsumerState<OnlineLobbyScreen> {
|
||||
}
|
||||
|
||||
void _showErrorDialog(String title, String message) {
|
||||
if (!mounted) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../providers/game_provider.dart';
|
||||
import '../providers/game.dart';
|
||||
import '../config/app_config.dart';
|
||||
import 'game_screen.dart';
|
||||
import 'online_lobby_screen.dart';
|
||||
@ -20,38 +20,28 @@ class WelcomeScreen extends ConsumerWidget {
|
||||
|
||||
// 内容
|
||||
SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight:
|
||||
MediaQuery.of(context).size.height -
|
||||
MediaQuery.of(context).padding.top -
|
||||
MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(),
|
||||
|
||||
// 游戏标题区域
|
||||
_buildTitle(),
|
||||
// 游戏标题区域
|
||||
_buildTitle(),
|
||||
|
||||
const SizedBox(height: 60),
|
||||
const Spacer(),
|
||||
|
||||
// 游戏模式选择
|
||||
_buildGameModes(context, ref),
|
||||
// 游戏模式选择
|
||||
_buildGameModes(context, ref),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// 版本信息
|
||||
_buildVersionInfo(),
|
||||
// 版本信息
|
||||
_buildVersionInfo(),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -83,7 +73,7 @@ class WelcomeScreen extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 标题
|
||||
Container(
|
||||
@ -119,6 +109,7 @@ class WelcomeScreen extends ConsumerWidget {
|
||||
|
||||
Widget _buildGameModes(BuildContext context, WidgetRef ref) {
|
||||
return Column(
|
||||
spacing: 10,
|
||||
children: [
|
||||
_buildModeButton(
|
||||
context: context,
|
||||
@ -130,8 +121,6 @@ class WelcomeScreen extends ConsumerWidget {
|
||||
onTap: () => _startGame(context, ref, aiMode: true),
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
_buildModeButton(
|
||||
context: context,
|
||||
ref: ref,
|
||||
@ -142,8 +131,6 @@ class WelcomeScreen extends ConsumerWidget {
|
||||
onTap: () => _startGame(context, ref, aiMode: false),
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
_buildModeButton(
|
||||
context: context,
|
||||
ref: ref,
|
||||
@ -192,7 +179,7 @@ class WelcomeScreen extends ConsumerWidget {
|
||||
onTap: enabled ? onTap : null,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: enabled
|
||||
? Colors.white.withValues(alpha: 0.95)
|
||||
@ -217,8 +204,8 @@ class WelcomeScreen extends ConsumerWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: enabled
|
||||
? color.withValues(alpha: 0.15)
|
||||
@ -232,7 +219,7 @@ class WelcomeScreen extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 20),
|
||||
const SizedBox(width: 15),
|
||||
|
||||
Expanded(
|
||||
child: Column(
|
||||
|
@ -1,240 +1,100 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import '../models/network_message.dart';
|
||||
import '../models/game_state.dart';
|
||||
import '../config/app_config.dart';
|
||||
|
||||
/// 全局唯一的WebSocket服务Provider
|
||||
final webSocketServiceProvider = Provider<WebSocketService>((ref) {
|
||||
return WebSocketService();
|
||||
});
|
||||
|
||||
class WebSocketService {
|
||||
WebSocketChannel? _channel;
|
||||
String? _playerId;
|
||||
String? _currentRoomId;
|
||||
bool _isConnected = false;
|
||||
Stream<dynamic>? _broadcastStream;
|
||||
|
||||
// 事件流控制器
|
||||
final _messageController = StreamController<NetworkMessage>.broadcast();
|
||||
final _connectionController = StreamController<bool>.broadcast();
|
||||
final _errorController = StreamController<String>.broadcast();
|
||||
|
||||
// 公开的流
|
||||
Stream<NetworkMessage> get messageStream => _messageController.stream;
|
||||
Stream<bool> get connectionStream => _connectionController.stream;
|
||||
Stream<String> get errorStream => _errorController.stream;
|
||||
|
||||
// Getters
|
||||
bool get isConnected => _isConnected;
|
||||
String? get playerId => _playerId;
|
||||
String? get currentRoomId => _currentRoomId;
|
||||
|
||||
/// 连接到WebSocket服务器
|
||||
Future<bool> connect({String? serverUrl}) async {
|
||||
/// 连接到WebSocket服务器并返回消息流
|
||||
///
|
||||
/// [serverUrl] 要连接的服务器地址
|
||||
/// [onDone] 连接关闭时的回调
|
||||
/// [onError] 发生错误时的回调
|
||||
Stream<dynamic>? connect(
|
||||
String serverUrl, {
|
||||
void Function()? onDone,
|
||||
void Function(Object, StackTrace)? onError,
|
||||
}) {
|
||||
if (_channel != null) {
|
||||
disconnect();
|
||||
}
|
||||
try {
|
||||
final url = serverUrl ?? AppConfig.serverUrl;
|
||||
_channel = WebSocketChannel.connect(Uri.parse(url));
|
||||
_isConnected = true;
|
||||
_playerId = _generatePlayerId();
|
||||
|
||||
_connectionController.add(true);
|
||||
|
||||
// 监听消息
|
||||
_channel!.stream.listen(
|
||||
_onMessage,
|
||||
onError: _onError,
|
||||
onDone: _onDisconnected,
|
||||
if (kDebugMode) {
|
||||
print('尝试连接到: $serverUrl');
|
||||
}
|
||||
_channel = WebSocketChannel.connect(Uri.parse(serverUrl));
|
||||
|
||||
// 我们需要一个可以被多次监听的流
|
||||
_broadcastStream = _channel!.stream.asBroadcastStream(
|
||||
onCancel: (subscription) {
|
||||
if (kDebugMode) print('WebSocket stream listener canceled.');
|
||||
},
|
||||
onListen: (subscription) {
|
||||
if (kDebugMode) print('WebSocket stream listener added.');
|
||||
},
|
||||
);
|
||||
|
||||
// 发送连接确认
|
||||
_sendMessage(
|
||||
NetworkMessage(
|
||||
type: MessageType.ping,
|
||||
playerId: _playerId,
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
_broadcastStream!.listen(
|
||||
null, // 数据由外部监听者处理
|
||||
onDone: () {
|
||||
if (kDebugMode) print('WebSocket 连接关闭');
|
||||
onDone?.call();
|
||||
_reset();
|
||||
},
|
||||
onError: (error, stackTrace) {
|
||||
if (kDebugMode) print('WebSocket 错误: $error');
|
||||
onError?.call(error, stackTrace);
|
||||
_reset();
|
||||
},
|
||||
);
|
||||
|
||||
return true;
|
||||
|
||||
return _broadcastStream;
|
||||
} catch (e) {
|
||||
_onError('连接失败: $e');
|
||||
return false;
|
||||
if (kDebugMode) {
|
||||
print('WebSocket 连接失败: $e');
|
||||
}
|
||||
_reset();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 断开连接
|
||||
Future<void> disconnect() async {
|
||||
if (_channel != null) {
|
||||
await _channel!.sink.close();
|
||||
void disconnect() {
|
||||
if (kDebugMode) {
|
||||
print('主动断开WebSocket连接');
|
||||
}
|
||||
_channel?.sink.close();
|
||||
_reset();
|
||||
}
|
||||
|
||||
/// 创建房间
|
||||
Future<void> createRoom(String playerName) async {
|
||||
if (!_isConnected) return;
|
||||
|
||||
_sendMessage(
|
||||
NetworkMessage(
|
||||
type: MessageType.createRoom,
|
||||
playerId: _playerId,
|
||||
data: {'playerName': playerName},
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 加入房间
|
||||
Future<void> joinRoom(String roomId, String playerName) async {
|
||||
if (!_isConnected) return;
|
||||
|
||||
_sendMessage(
|
||||
NetworkMessage(
|
||||
type: MessageType.joinRoom,
|
||||
roomId: roomId,
|
||||
playerId: _playerId,
|
||||
data: {'playerName': playerName},
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 离开房间
|
||||
Future<void> leaveRoom() async {
|
||||
if (!_isConnected || _currentRoomId == null) return;
|
||||
|
||||
_sendMessage(
|
||||
NetworkMessage(
|
||||
type: MessageType.leaveRoom,
|
||||
roomId: _currentRoomId,
|
||||
playerId: _playerId,
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 发送游戏移动
|
||||
Future<void> sendGameMove(int position, Player player) async {
|
||||
if (!_isConnected || _currentRoomId == null) return;
|
||||
|
||||
final moveData = GameMoveData(
|
||||
position: position,
|
||||
player: player,
|
||||
playerId: _playerId!,
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
|
||||
_sendMessage(
|
||||
NetworkMessage(
|
||||
type: MessageType.gameMove,
|
||||
roomId: _currentRoomId,
|
||||
playerId: _playerId,
|
||||
data: moveData.toJson(),
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 重置游戏
|
||||
Future<void> resetGame() async {
|
||||
if (!_isConnected || _currentRoomId == null) return;
|
||||
|
||||
_sendMessage(
|
||||
NetworkMessage(
|
||||
type: MessageType.gameReset,
|
||||
roomId: _currentRoomId,
|
||||
playerId: _playerId,
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 发送消息
|
||||
void _sendMessage(NetworkMessage message) {
|
||||
if (_channel != null && _isConnected) {
|
||||
void sendMessage(NetworkMessage message) {
|
||||
if (_channel != null) {
|
||||
final jsonData = jsonEncode(message.toJson());
|
||||
_channel!.sink.add(jsonData);
|
||||
if (kDebugMode) {
|
||||
print('发送消息: $jsonData');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理接收的消息
|
||||
void _onMessage(dynamic data) {
|
||||
try {
|
||||
final jsonData = jsonDecode(data as String);
|
||||
final message = NetworkMessage.fromJson(jsonData);
|
||||
|
||||
_channel!.sink.add(jsonData);
|
||||
} else {
|
||||
if (kDebugMode) {
|
||||
print('接收消息: ${message.type}');
|
||||
print('无法发送消息:WebSocket未连接');
|
||||
}
|
||||
|
||||
// 处理房间状态更新
|
||||
if (message.type == MessageType.roomJoined ||
|
||||
message.type == MessageType.roomCreated) {
|
||||
_currentRoomId = message.roomId;
|
||||
} else if (message.type == MessageType.roomLeft) {
|
||||
_currentRoomId = null;
|
||||
}
|
||||
|
||||
// 发送到消息流
|
||||
_messageController.add(message);
|
||||
|
||||
// 响应ping
|
||||
if (message.type == MessageType.ping) {
|
||||
_sendMessage(
|
||||
NetworkMessage(
|
||||
type: MessageType.pong,
|
||||
playerId: _playerId,
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_onError('解析消息失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理错误
|
||||
void _onError(dynamic error) {
|
||||
final errorMessage = error.toString();
|
||||
if (kDebugMode) {
|
||||
print('WebSocket错误: $errorMessage');
|
||||
}
|
||||
_errorController.add(errorMessage);
|
||||
}
|
||||
|
||||
/// 处理断开连接
|
||||
void _onDisconnected() {
|
||||
if (kDebugMode) {
|
||||
print('WebSocket断开连接');
|
||||
}
|
||||
_reset();
|
||||
}
|
||||
|
||||
/// 重置状态
|
||||
/// 重置内部状态
|
||||
void _reset() {
|
||||
_isConnected = false;
|
||||
_currentRoomId = null;
|
||||
_channel = null;
|
||||
_connectionController.add(false);
|
||||
}
|
||||
|
||||
/// 生成玩家ID
|
||||
String _generatePlayerId() {
|
||||
final random = Random();
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
return List.generate(
|
||||
12,
|
||||
(index) => chars[random.nextInt(chars.length)],
|
||||
).join();
|
||||
}
|
||||
|
||||
/// 清理资源
|
||||
void dispose() {
|
||||
disconnect();
|
||||
_messageController.close();
|
||||
_connectionController.close();
|
||||
_errorController.close();
|
||||
_broadcastStream = null;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../models/game_state.dart';
|
||||
import '../providers/game_provider.dart';
|
||||
import '../providers/game.dart';
|
||||
|
||||
class GameBoard extends ConsumerWidget {
|
||||
final GameState? gameState;
|
||||
|
@ -5,14 +5,14 @@ class VictoryDialog extends StatefulWidget {
|
||||
final GameStatus gameStatus;
|
||||
final bool isAiMode;
|
||||
final VoidCallback onPlayAgain;
|
||||
final VoidCallback onChangeMode;
|
||||
final VoidCallback onGoToMenu;
|
||||
|
||||
const VictoryDialog({
|
||||
super.key,
|
||||
required this.gameStatus,
|
||||
required this.isAiMode,
|
||||
required this.onPlayAgain,
|
||||
required this.onChangeMode,
|
||||
required this.onGoToMenu,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -68,7 +68,7 @@ class _VictoryDialogState extends State<VictoryDialog>
|
||||
child: ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(30),
|
||||
padding: const EdgeInsets.all(25),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
@ -88,8 +88,8 @@ class _VictoryDialogState extends State<VictoryDialog>
|
||||
RotationTransition(
|
||||
turns: _rotationAnimation,
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
width: 70,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor().withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
@ -115,7 +115,7 @@ class _VictoryDialogState extends State<VictoryDialog>
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(height: 5),
|
||||
|
||||
// 副标题
|
||||
Text(
|
||||
@ -124,45 +124,42 @@ class _VictoryDialogState extends State<VictoryDialog>
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// 按钮
|
||||
Row(
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
widget.onChangeMode();
|
||||
},
|
||||
icon: Icon(_getModeIcon()),
|
||||
label: Text(_getModeButtonText()),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
widget.onGoToMenu();
|
||||
},
|
||||
icon: const Icon(Icons.menu),
|
||||
label: const Text('返回菜单'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 15),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
widget.onPlayAgain();
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('再来一局'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _getStatusColor(),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
widget.onPlayAgain();
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('再来一局'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _getStatusColor(),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -227,12 +224,4 @@ class _VictoryDialogState extends State<VictoryDialog>
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getModeIcon() {
|
||||
return widget.isAiMode ? Icons.people : Icons.smart_toy;
|
||||
}
|
||||
|
||||
String _getModeButtonText() {
|
||||
return widget.isAiMode ? '双人对战' : 'VS AI';
|
||||
}
|
||||
}
|
||||
|
248
pubspec.lock
248
pubspec.lock
@ -6,7 +6,7 @@ packages:
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "82.0.0"
|
||||
analyzer:
|
||||
@ -14,15 +14,23 @@ packages:
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.4.5"
|
||||
analyzer_plugin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer_plugin
|
||||
sha256: ee188b6df6c85f1441497c7171c84f1392affadc0384f71089cb10a3bc508cef
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.13.1"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
@ -30,7 +38,7 @@ packages:
|
||||
description:
|
||||
name: async
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
boolean_selector:
|
||||
@ -38,23 +46,23 @@ packages:
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
|
||||
url: "https://pub.dev"
|
||||
sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
version: "2.4.2"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
build_daemon:
|
||||
@ -62,39 +70,39 @@ packages:
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.4"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
|
||||
url: "https://pub.dev"
|
||||
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
version: "2.4.4"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
|
||||
url: "https://pub.dev"
|
||||
sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
version: "2.4.14"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
|
||||
url: "https://pub.dev"
|
||||
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.1.2"
|
||||
version: "8.0.0"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_collection
|
||||
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
built_value:
|
||||
@ -102,7 +110,7 @@ packages:
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "8.10.1"
|
||||
characters:
|
||||
@ -110,7 +118,7 @@ packages:
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
@ -118,7 +126,7 @@ packages:
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
clock:
|
||||
@ -126,7 +134,7 @@ packages:
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_builder:
|
||||
@ -134,7 +142,7 @@ packages:
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.10.1"
|
||||
collection:
|
||||
@ -142,7 +150,7 @@ packages:
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
convert:
|
||||
@ -150,7 +158,7 @@ packages:
|
||||
description:
|
||||
name: convert
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
crypto:
|
||||
@ -158,7 +166,7 @@ packages:
|
||||
description:
|
||||
name: crypto
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
cupertino_icons:
|
||||
@ -166,15 +174,31 @@ packages:
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
custom_lint_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: custom_lint_core
|
||||
sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.5"
|
||||
custom_lint_visitor:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: custom_lint_visitor
|
||||
sha256: cba5b6d7a6217312472bf4468cdf68c949488aed7ffb0eab792cd0b6c435054d
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0+7.4.5"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
fake_async:
|
||||
@ -182,7 +206,7 @@ packages:
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
file:
|
||||
@ -190,7 +214,7 @@ packages:
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
@ -198,7 +222,7 @@ packages:
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
@ -211,7 +235,7 @@ packages:
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
flutter_riverpod:
|
||||
@ -219,7 +243,7 @@ packages:
|
||||
description:
|
||||
name: flutter_riverpod
|
||||
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
flutter_test:
|
||||
@ -232,7 +256,7 @@ packages:
|
||||
description:
|
||||
name: freezed
|
||||
sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.8"
|
||||
freezed_annotation:
|
||||
@ -240,7 +264,7 @@ packages:
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
frontend_server_client:
|
||||
@ -248,7 +272,7 @@ packages:
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
glob:
|
||||
@ -256,7 +280,7 @@ packages:
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
graphs:
|
||||
@ -264,23 +288,15 @@ packages:
|
||||
description:
|
||||
name: graphs
|
||||
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
@ -288,7 +304,7 @@ packages:
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
io:
|
||||
@ -296,7 +312,7 @@ packages:
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
js:
|
||||
@ -304,7 +320,7 @@ packages:
|
||||
description:
|
||||
name: js
|
||||
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
json_annotation:
|
||||
@ -312,7 +328,7 @@ packages:
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
json_serializable:
|
||||
@ -320,7 +336,7 @@ packages:
|
||||
description:
|
||||
name: json_serializable
|
||||
sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.9.5"
|
||||
leak_tracker:
|
||||
@ -328,7 +344,7 @@ packages:
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "10.0.9"
|
||||
leak_tracker_flutter_testing:
|
||||
@ -336,7 +352,7 @@ packages:
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
leak_tracker_testing:
|
||||
@ -344,7 +360,7 @@ packages:
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
lints:
|
||||
@ -352,7 +368,7 @@ packages:
|
||||
description:
|
||||
name: lints
|
||||
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
logging:
|
||||
@ -360,7 +376,7 @@ packages:
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
@ -368,7 +384,7 @@ packages:
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
material_color_utilities:
|
||||
@ -376,7 +392,7 @@ packages:
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
meta:
|
||||
@ -384,7 +400,7 @@ packages:
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
@ -392,7 +408,7 @@ packages:
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
package_config:
|
||||
@ -400,7 +416,7 @@ packages:
|
||||
description:
|
||||
name: package_config
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
@ -408,7 +424,7 @@ packages:
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
pool:
|
||||
@ -416,7 +432,7 @@ packages:
|
||||
description:
|
||||
name: pool
|
||||
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
pub_semver:
|
||||
@ -424,7 +440,7 @@ packages:
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
pubspec_parse:
|
||||
@ -432,7 +448,7 @@ packages:
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
random_name_generator:
|
||||
@ -440,7 +456,7 @@ packages:
|
||||
description:
|
||||
name: random_name_generator
|
||||
sha256: "7c5b91d60f68b30e7b4c53006047cab8474f06563f7d0cab70fb409a0cb5ff61"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
riverpod:
|
||||
@ -448,25 +464,49 @@ packages:
|
||||
description:
|
||||
name: riverpod
|
||||
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
riverpod_analyzer_utils:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod_analyzer_utils
|
||||
sha256: "837a6dc33f490706c7f4632c516bcd10804ee4d9ccc8046124ca56388715fdf3"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.5.9"
|
||||
riverpod_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: riverpod_annotation
|
||||
sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
riverpod_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: riverpod_generator
|
||||
sha256: "120d3310f687f43e7011bb213b90a436f1bbc300f0e4b251a72c39bccb017a4f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.6.4"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.dev"
|
||||
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "2.0.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@ -477,7 +517,7 @@ packages:
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
source_helper:
|
||||
@ -485,7 +525,7 @@ packages:
|
||||
description:
|
||||
name: source_helper
|
||||
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.5"
|
||||
source_span:
|
||||
@ -493,15 +533,23 @@ packages:
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
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:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
state_notifier:
|
||||
@ -509,7 +557,7 @@ packages:
|
||||
description:
|
||||
name: state_notifier
|
||||
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
stream_channel:
|
||||
@ -517,7 +565,7 @@ packages:
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
stream_transform:
|
||||
@ -525,7 +573,7 @@ packages:
|
||||
description:
|
||||
name: stream_transform
|
||||
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
string_scanner:
|
||||
@ -533,7 +581,7 @@ packages:
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
term_glyph:
|
||||
@ -541,7 +589,7 @@ packages:
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
@ -549,7 +597,7 @@ packages:
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
timing:
|
||||
@ -557,7 +605,7 @@ packages:
|
||||
description:
|
||||
name: timing
|
||||
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
typed_data:
|
||||
@ -565,15 +613,23 @@ packages:
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.5.1"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
vm_service:
|
||||
@ -581,7 +637,7 @@ packages:
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "15.0.0"
|
||||
watcher:
|
||||
@ -589,31 +645,31 @@ packages:
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "0.5.1"
|
||||
web_socket_channel:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
||||
url: "https://pub.dev"
|
||||
sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.4.5"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
|
@ -35,7 +35,8 @@ dependencies:
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
# 状态管理
|
||||
flutter_riverpod: ^2.5.1
|
||||
flutter_riverpod: ^2.6.1
|
||||
riverpod_annotation: ^2.6.1
|
||||
# 不可变数据类
|
||||
freezed_annotation: ^2.4.1
|
||||
# JSON序列化
|
||||
@ -58,6 +59,7 @@ dev_dependencies:
|
||||
build_runner: ^2.4.7
|
||||
freezed: ^2.4.7
|
||||
json_serializable: ^6.7.1
|
||||
riverpod_generator: ^2.4.2
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
@ -6,8 +6,8 @@ import 'package:shelf_router/shelf_router.dart';
|
||||
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
||||
import 'package:shelf_cors_headers/shelf_cors_headers.dart';
|
||||
|
||||
import '../lib/services/room_manager.dart';
|
||||
import '../lib/handlers/websocket_handler.dart';
|
||||
import 'package:frog_game_server/services/room_manager.dart';
|
||||
import 'package:frog_game_server/handlers/websocket_handler.dart';
|
||||
|
||||
void main(List<String> args) async {
|
||||
// 设置日志
|
||||
@ -47,7 +47,7 @@ void main(List<String> args) async {
|
||||
|
||||
// WebSocket 端点
|
||||
router.get(
|
||||
'/ws',
|
||||
'/frog',
|
||||
webSocketHandler((webSocket) {
|
||||
logger.info('新的WebSocket连接');
|
||||
webSocketManager.handleConnection(webSocket);
|
||||
@ -81,7 +81,7 @@ void main(List<String> args) async {
|
||||
|
||||
<h2>API 端点</h2>
|
||||
<div class="endpoint">
|
||||
<strong>WebSocket:</strong> <span class="code">ws://localhost:8080/ws</span><br>
|
||||
<strong>WebSocket:</strong> <span class="code">ws://localhost:8080/frog</span><br>
|
||||
用于游戏实时通信
|
||||
</div>
|
||||
<div class="endpoint">
|
||||
@ -111,7 +111,7 @@ void main(List<String> args) async {
|
||||
final handler = Pipeline()
|
||||
.addMiddleware(corsHeaders())
|
||||
.addMiddleware(logRequests())
|
||||
.addHandler(router);
|
||||
.addHandler(router.call);
|
||||
|
||||
// 启动服务器
|
||||
final port = int.parse(Platform.environment['PORT'] ?? '8080');
|
||||
@ -119,7 +119,7 @@ void main(List<String> args) async {
|
||||
|
||||
logger.info('🚀 服务器启动成功!');
|
||||
logger.info('📡 监听地址: http://${server.address.host}:${server.port}');
|
||||
logger.info('🔗 WebSocket: ws://${server.address.host}:${server.port}/ws');
|
||||
logger.info('🔗 WebSocket: ws://${server.address.host}:${server.port}/frog');
|
||||
logger.info('📊 状态页面: http://${server.address.host}:${server.port}/stats');
|
||||
|
||||
// 优雅关闭处理
|
||||
|
217
server/deploy.sh
217
server/deploy.sh
@ -1,217 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 青蛙跳井游戏服务器部署脚本
|
||||
# 支持本地开发、Docker构建和阿里云FC部署
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 日志函数
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 显示帮助信息
|
||||
show_help() {
|
||||
echo "青蛙跳井游戏服务器部署脚本"
|
||||
echo ""
|
||||
echo "用法: $0 [命令] [选项]"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " dev - 本地开发模式运行"
|
||||
echo " build - 构建 Docker 镜像"
|
||||
echo " deploy - 部署到阿里云 FC"
|
||||
echo " push - 推送镜像到阿里云容器镜像服务"
|
||||
echo " all - 执行完整的构建和部署流程"
|
||||
echo ""
|
||||
echo "选项:"
|
||||
echo " -h, --help - 显示此帮助信息"
|
||||
echo " -v, --version - 显示版本信息"
|
||||
echo ""
|
||||
echo "环境变量:"
|
||||
echo " REGISTRY_URL - 容器镜像仓库地址 (默认: registry.cn-hangzhou.aliyuncs.com)"
|
||||
echo " NAMESPACE - 镜像命名空间 (必需)"
|
||||
echo " IMAGE_NAME - 镜像名称 (默认: frog-game-server)"
|
||||
echo " IMAGE_TAG - 镜像标签 (默认: latest)"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 检查必需的工具
|
||||
check_requirements() {
|
||||
local missing_tools=()
|
||||
|
||||
if ! command -v dart &> /dev/null; then
|
||||
missing_tools+=("dart")
|
||||
fi
|
||||
|
||||
if ! command -v docker &> /dev/null; then
|
||||
missing_tools+=("docker")
|
||||
fi
|
||||
|
||||
if [ ${#missing_tools[@]} -ne 0 ]; then
|
||||
log_error "缺少必需的工具: ${missing_tools[*]}"
|
||||
log_info "请安装缺少的工具后重试"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 设置环境变量
|
||||
setup_env() {
|
||||
export REGISTRY_URL=${REGISTRY_URL:-"registry.cn-hangzhou.aliyuncs.com"}
|
||||
export IMAGE_NAME=${IMAGE_NAME:-"frog-game-server"}
|
||||
export IMAGE_TAG=${IMAGE_TAG:-"latest"}
|
||||
|
||||
if [ -z "$NAMESPACE" ]; then
|
||||
log_warning "未设置 NAMESPACE 环境变量,使用默认值 'default'"
|
||||
export NAMESPACE="default"
|
||||
fi
|
||||
|
||||
export FULL_IMAGE_NAME="${REGISTRY_URL}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
|
||||
log_info "镜像信息: ${FULL_IMAGE_NAME}"
|
||||
}
|
||||
|
||||
# 本地开发模式
|
||||
run_dev() {
|
||||
log_info "启动本地开发服务器..."
|
||||
|
||||
# 生成代码
|
||||
log_info "生成 Dart 代码..."
|
||||
dart pub get
|
||||
dart pub run build_runner build --delete-conflicting-outputs
|
||||
|
||||
# 启动服务器
|
||||
log_info "启动服务器..."
|
||||
dart run bin/server.dart
|
||||
}
|
||||
|
||||
# 构建 Docker 镜像
|
||||
build_docker() {
|
||||
log_info "构建 Docker 镜像: ${FULL_IMAGE_NAME}"
|
||||
|
||||
# 检查 Dockerfile 是否存在
|
||||
if [ ! -f "Dockerfile" ]; then
|
||||
log_error "Dockerfile 不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 构建镜像
|
||||
docker build -t "${FULL_IMAGE_NAME}" .
|
||||
|
||||
log_success "Docker 镜像构建完成"
|
||||
}
|
||||
|
||||
# 推送镜像到阿里云容器镜像服务
|
||||
push_image() {
|
||||
log_info "推送镜像到阿里云容器镜像服务..."
|
||||
|
||||
# 检查是否已登录 Docker
|
||||
if ! docker info &> /dev/null; then
|
||||
log_error "Docker 守护进程未运行或未登录"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 推送镜像
|
||||
docker push "${FULL_IMAGE_NAME}"
|
||||
|
||||
log_success "镜像推送完成"
|
||||
}
|
||||
|
||||
# 部署到阿里云 FC
|
||||
deploy_fc() {
|
||||
log_info "部署到阿里云函数计算..."
|
||||
|
||||
# 检查是否安装了 Funcraft
|
||||
if ! command -v fun &> /dev/null; then
|
||||
log_warning "未找到 Funcraft,请使用 Serverless Devs 或手动部署"
|
||||
log_info "Serverless Devs 安装: npm install -g @serverless-devs/s"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 检查模板文件
|
||||
if [ ! -f "template.yml" ]; then
|
||||
log_error "template.yml 不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 部署
|
||||
fun deploy
|
||||
|
||||
log_success "部署完成"
|
||||
}
|
||||
|
||||
# 完整流程
|
||||
run_all() {
|
||||
log_info "执行完整的构建和部署流程..."
|
||||
|
||||
build_docker
|
||||
push_image
|
||||
deploy_fc
|
||||
|
||||
log_success "所有步骤完成!"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
case "${1:-}" in
|
||||
"dev")
|
||||
check_requirements
|
||||
run_dev
|
||||
;;
|
||||
"build")
|
||||
check_requirements
|
||||
setup_env
|
||||
build_docker
|
||||
;;
|
||||
"push")
|
||||
check_requirements
|
||||
setup_env
|
||||
push_image
|
||||
;;
|
||||
"deploy")
|
||||
deploy_fc
|
||||
;;
|
||||
"all")
|
||||
check_requirements
|
||||
setup_env
|
||||
run_all
|
||||
;;
|
||||
"-h"|"--help")
|
||||
show_help
|
||||
;;
|
||||
"-v"|"--version")
|
||||
echo "版本 1.0.0"
|
||||
;;
|
||||
"")
|
||||
log_error "请指定命令"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
log_error "未知命令: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 执行主函数
|
||||
main "$@"
|
@ -110,7 +110,10 @@ class WebSocketManager {
|
||||
type: MessageType.roomCreated,
|
||||
roomId: roomId,
|
||||
playerId: message.playerId,
|
||||
data: room.toRoomInfo().toJson(),
|
||||
data: {
|
||||
'roomInfo': room.toRoomInfo().toJson(),
|
||||
'gameState': room.gameState.toJson(),
|
||||
},
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
|
||||
@ -147,22 +150,28 @@ class WebSocketManager {
|
||||
type: MessageType.roomJoined,
|
||||
roomId: message.roomId,
|
||||
playerId: message.playerId,
|
||||
data: room.toRoomInfo().toJson(),
|
||||
data: {
|
||||
'roomInfo': room.toRoomInfo().toJson(),
|
||||
'gameState': room.gameState.toJson(),
|
||||
},
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
_sendMessage(webSocket, response);
|
||||
|
||||
// 通知房间内其他玩家
|
||||
final newPlayer = room.players.last;
|
||||
final notification = NetworkMessage(
|
||||
type: MessageType.playerJoined,
|
||||
roomId: message.roomId!,
|
||||
playerId: message.playerId,
|
||||
data: {
|
||||
'roomInfo': room.toRoomInfo().toJson(),
|
||||
'gameState': room.gameState.toJson(),
|
||||
},
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
_broadcastToRoom(
|
||||
message.roomId!,
|
||||
NetworkMessage(
|
||||
type: MessageType.playerJoined,
|
||||
roomId: message.roomId,
|
||||
playerId: message.playerId,
|
||||
data: newPlayer.toJson(),
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
notification,
|
||||
excludePlayer: message.playerId,
|
||||
);
|
||||
|
||||
@ -197,6 +206,10 @@ class WebSocketManager {
|
||||
type: MessageType.playerLeft,
|
||||
roomId: room.roomId,
|
||||
playerId: playerId,
|
||||
data: {
|
||||
'roomInfo': room.toRoomInfo().toJson(),
|
||||
'gameState': room.gameState.toJson(),
|
||||
},
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
@ -228,28 +241,30 @@ class WebSocketManager {
|
||||
switch (result) {
|
||||
case GameMoveSuccess(:final room, :final moveData):
|
||||
// 广播移动给房间内所有玩家
|
||||
_broadcastToRoom(
|
||||
room.roomId,
|
||||
NetworkMessage(
|
||||
type: MessageType.gameMove,
|
||||
roomId: room.roomId,
|
||||
playerId: message.playerId,
|
||||
data: moveData.toJson(),
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
final gameUpdateMessage = NetworkMessage(
|
||||
type: MessageType.gameUpdate,
|
||||
roomId: room.roomId,
|
||||
playerId: message.playerId,
|
||||
data: {
|
||||
'roomInfo': room.toRoomInfo().toJson(),
|
||||
'gameState': room.gameState.toJson(),
|
||||
},
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
_broadcastToRoom(room.roomId, gameUpdateMessage);
|
||||
|
||||
// 如果游戏结束,发送游戏结束消息
|
||||
if (room.gameState.status != GameStatus.playing) {
|
||||
_broadcastToRoom(
|
||||
room.roomId,
|
||||
NetworkMessage(
|
||||
type: MessageType.gameOver,
|
||||
roomId: room.roomId,
|
||||
data: room.gameState.toJson(),
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
final gameOverMessage = NetworkMessage(
|
||||
type: MessageType.gameOver,
|
||||
roomId: room.roomId,
|
||||
data: {
|
||||
'roomInfo': room.toRoomInfo().toJson(),
|
||||
'gameState': room.gameState.toJson(),
|
||||
},
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
_broadcastToRoom(room.roomId, gameOverMessage);
|
||||
}
|
||||
|
||||
case GameMoveError(:final message):
|
||||
@ -273,7 +288,10 @@ class WebSocketManager {
|
||||
NetworkMessage(
|
||||
type: MessageType.gameReset,
|
||||
roomId: room.roomId,
|
||||
playerId: message.playerId,
|
||||
data: {
|
||||
'roomInfo': room.toRoomInfo().toJson(),
|
||||
'gameState': room.gameState.toJson(),
|
||||
},
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
@ -298,6 +316,10 @@ class WebSocketManager {
|
||||
type: MessageType.playerLeft,
|
||||
roomId: room.roomId,
|
||||
playerId: playerId,
|
||||
data: {
|
||||
'roomInfo': room.toRoomInfo().toJson(),
|
||||
'gameState': room.gameState.toJson(),
|
||||
},
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
|
@ -5,32 +5,32 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f
|
||||
url: "https://pub.dev"
|
||||
sha256: c81659312e021e3b780a502206130ea106487b34793bce61e26dc0f9b84807af
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "82.0.0"
|
||||
version: "83.0.0"
|
||||
adaptive_number:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: adaptive_number
|
||||
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0"
|
||||
url: "https://pub.dev"
|
||||
sha256: "9c35a79bf2a150b3ea0d40010fbbb45b5ebea143d47096e0f82fd922a324b49b"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.4.5"
|
||||
version: "7.4.6"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
@ -38,7 +38,7 @@ packages:
|
||||
description:
|
||||
name: async
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
boolean_selector:
|
||||
@ -46,7 +46,7 @@ packages:
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
build:
|
||||
@ -54,7 +54,7 @@ packages:
|
||||
description:
|
||||
name: build
|
||||
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
build_config:
|
||||
@ -62,7 +62,7 @@ packages:
|
||||
description:
|
||||
name: build_config
|
||||
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
build_daemon:
|
||||
@ -70,7 +70,7 @@ packages:
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.4"
|
||||
build_resolvers:
|
||||
@ -78,7 +78,7 @@ packages:
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
build_runner:
|
||||
@ -86,7 +86,7 @@ packages:
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
build_runner_core:
|
||||
@ -94,7 +94,7 @@ packages:
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.1.2"
|
||||
built_collection:
|
||||
@ -102,7 +102,7 @@ packages:
|
||||
description:
|
||||
name: built_collection
|
||||
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
built_value:
|
||||
@ -110,7 +110,7 @@ packages:
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "8.10.1"
|
||||
checked_yaml:
|
||||
@ -118,7 +118,7 @@ packages:
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
clock:
|
||||
@ -126,7 +126,7 @@ packages:
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_builder:
|
||||
@ -134,7 +134,7 @@ packages:
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.10.1"
|
||||
collection:
|
||||
@ -142,7 +142,7 @@ packages:
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
convert:
|
||||
@ -150,7 +150,7 @@ packages:
|
||||
description:
|
||||
name: convert
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
crypto:
|
||||
@ -158,7 +158,7 @@ packages:
|
||||
description:
|
||||
name: crypto
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
dart_jsonwebtoken:
|
||||
@ -166,7 +166,7 @@ packages:
|
||||
description:
|
||||
name: dart_jsonwebtoken
|
||||
sha256: "00a0812d2aeaeb0d30bcbc4dd3cee57971dbc0ab2216adf4f0247f37793f15ef"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.17.0"
|
||||
dart_style:
|
||||
@ -174,7 +174,7 @@ packages:
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
ed25519_edwards:
|
||||
@ -182,7 +182,7 @@ packages:
|
||||
description:
|
||||
name: ed25519_edwards
|
||||
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
file:
|
||||
@ -190,7 +190,7 @@ packages:
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
@ -198,7 +198,7 @@ packages:
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
freezed:
|
||||
@ -206,7 +206,7 @@ packages:
|
||||
description:
|
||||
name: freezed
|
||||
sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.8"
|
||||
freezed_annotation:
|
||||
@ -214,7 +214,7 @@ packages:
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
frontend_server_client:
|
||||
@ -222,7 +222,7 @@ packages:
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
glob:
|
||||
@ -230,7 +230,7 @@ packages:
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
graphs:
|
||||
@ -238,7 +238,7 @@ packages:
|
||||
description:
|
||||
name: graphs
|
||||
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
http:
|
||||
@ -246,7 +246,7 @@ packages:
|
||||
description:
|
||||
name: http
|
||||
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
http_methods:
|
||||
@ -254,7 +254,7 @@ packages:
|
||||
description:
|
||||
name: http_methods
|
||||
sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
http_multi_server:
|
||||
@ -262,7 +262,7 @@ packages:
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
@ -270,7 +270,7 @@ packages:
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
io:
|
||||
@ -278,7 +278,7 @@ packages:
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
js:
|
||||
@ -286,7 +286,7 @@ packages:
|
||||
description:
|
||||
name: js
|
||||
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
json_annotation:
|
||||
@ -294,7 +294,7 @@ packages:
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
json_serializable:
|
||||
@ -302,7 +302,7 @@ packages:
|
||||
description:
|
||||
name: json_serializable
|
||||
sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.9.5"
|
||||
lints:
|
||||
@ -310,7 +310,7 @@ packages:
|
||||
description:
|
||||
name: lints
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
logging:
|
||||
@ -318,7 +318,7 @@ packages:
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
@ -326,7 +326,7 @@ packages:
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
meta:
|
||||
@ -334,7 +334,7 @@ packages:
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
mime:
|
||||
@ -342,7 +342,7 @@ packages:
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
package_config:
|
||||
@ -350,7 +350,7 @@ packages:
|
||||
description:
|
||||
name: package_config
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
@ -358,7 +358,7 @@ packages:
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
pointycastle:
|
||||
@ -366,7 +366,7 @@ packages:
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.9.1"
|
||||
pool:
|
||||
@ -374,7 +374,7 @@ packages:
|
||||
description:
|
||||
name: pool
|
||||
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
pub_semver:
|
||||
@ -382,7 +382,7 @@ packages:
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
pubspec_parse:
|
||||
@ -390,7 +390,7 @@ packages:
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
shelf:
|
||||
@ -398,7 +398,7 @@ packages:
|
||||
description:
|
||||
name: shelf
|
||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
shelf_cors_headers:
|
||||
@ -406,7 +406,7 @@ packages:
|
||||
description:
|
||||
name: shelf_cors_headers
|
||||
sha256: a127c80f99bbef3474293db67a7608e3a0f1f0fcdb171dad77fa9bd2cd123ae4
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.5"
|
||||
shelf_router:
|
||||
@ -414,7 +414,7 @@ packages:
|
||||
description:
|
||||
name: shelf_router
|
||||
sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.4"
|
||||
shelf_web_socket:
|
||||
@ -422,7 +422,7 @@ packages:
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
source_gen:
|
||||
@ -430,7 +430,7 @@ packages:
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
source_helper:
|
||||
@ -438,7 +438,7 @@ packages:
|
||||
description:
|
||||
name: source_helper
|
||||
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.5"
|
||||
source_span:
|
||||
@ -446,7 +446,7 @@ packages:
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
stack_trace:
|
||||
@ -454,7 +454,7 @@ packages:
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
@ -462,7 +462,7 @@ packages:
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
stream_transform:
|
||||
@ -470,7 +470,7 @@ packages:
|
||||
description:
|
||||
name: stream_transform
|
||||
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
string_scanner:
|
||||
@ -478,7 +478,7 @@ packages:
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
term_glyph:
|
||||
@ -486,7 +486,7 @@ packages:
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
@ -494,7 +494,7 @@ packages:
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.6"
|
||||
timing:
|
||||
@ -502,7 +502,7 @@ packages:
|
||||
description:
|
||||
name: timing
|
||||
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
typed_data:
|
||||
@ -510,7 +510,7 @@ packages:
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
watcher:
|
||||
@ -518,7 +518,7 @@ packages:
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
web:
|
||||
@ -526,7 +526,7 @@ packages:
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket_channel:
|
||||
@ -534,7 +534,7 @@ packages:
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
yaml:
|
||||
@ -542,7 +542,7 @@ packages:
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
|
Reference in New Issue
Block a user