import 'dart:async'; import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:fixnum/fixnum.dart'; import 'package:grpc/grpc.dart'; import 'package:trackoffical_app/model/game_map.dart'; import 'package:trackoffical_app/model/settlement.dart'; import 'package:trackoffical_app/pb.dart' as pb; import 'package:trackoffical_app/service/app_map.dart'; import 'package:trackoffical_app/service/game/game_model.dart'; import 'package:trackoffical_app/service/game/plug.dart'; import 'package:trackoffical_app/service/game/plug_location.dart'; import 'package:trackoffical_app/service/game/plug_sport_wear.dart'; import 'package:trackoffical_app/service/game/rule_in_order.dart'; import 'package:trackoffical_app/service/game/show_position_controller.dart'; import 'package:trackoffical_app/utils.dart'; import 'package:trackoffical_app/view/ingame/settlement_view.dart'; import 'package:vibration/vibration.dart'; import 'package:wakelock/wakelock.dart'; import '../../logger.dart'; import '../api.dart'; import '../../model.dart'; import '../app.dart'; import '../database.dart'; import 'map_status.dart'; import 'plug_orientation.dart'; import 'rule.dart'; import '../image.dart'; enum GameStatus { idle, // 准备进入游戏 preparing, loading, playing, settlement, } class GameService extends GetxService { static GameService get to => Get.find(); final DatabaseService _database = Get.find(); final GameModel _model = Get.find(); final App _app = Get.find(); final errorMsg = ''.obs; final name = "".obs; var activityId = 0; var mapRouteId = Int64(0); var _status = GameStatus.idle; Rule rule = RuleMock(); GameStatus get status => _status; set status(GameStatus v) { info('游戏状态:[$v]'); _status = v; } pb.GameSettlement _lastGameSettlement = pb.GameDetailReply(); pb.GameSettlement get lastGameSettlement => _lastGameSettlement; MPosition? get myPosition => _model.myPosition.value; Offset? get positionOnMap => _model.myPositionOnMap.value; double get myPositionHistoryLenKm => _model.myPositionHistoryLen.value.km; final _plugs = []; GameState get gameState => _model.gameSrcState.value; var isShowCompass = true.obs; static const _progressMap = 0.6; static const _progressApi = 0.2; static const _progressFinal = 0.2; final _loadProgress = 0.0.obs; double get loadProgress => _loadProgress.value; List get controlPointWantSequence => _model.controlPointWantSequence; bool get isNfcScanUseDialog { final platform = _app.platformInfo; if (platform is PlatformInfoIOS) { if (platform.deviceVersion >= 8) { return true; } } return false; } // 创建时间 DateTime get createTime => gameState.createTime; // 关门时间 Duration get maxPassDuration => gameState.pbGameData.maxDuration.toDuration(); // 强制结束时间 Duration get maxForcedEndDuration => gameState.pbGameData.maxForcedEndDuration.toDuration(); DateTime get now => _app.now; // 打开始点的时间 DateTime? get startAt => _model.startAt; DateTime? get endAt => _model.endAt; List get _controlPointWantSequence => gameState.pbGameData.controlPointSortedList; List get _controlPointAll => gameState.pbGameData.controlPointAll; var _lastCheckedPoint = Int64(0); var _lastCheckedPointTime = DateTime(0); List get checkedPointsHistory => _model.checkedPointsHistory; final beginDuration = 0.seconds.obs; final mapStatus = MapStatus(); Timer? _beginDurationTicker; Timer? _checkStopTicker; int get checkedCount => _model.checkedCount; // 所有计分点数量 int get controlPointAllNum => _model.validCPAllNum; // 打卡进度 double get checkProgress => _model.checkProgress; bool get isOutBoundary { final p = _model.myPositionOnMap.value; if (p != null) { final mapWidth = mapStatus.gameMapData.width; final mapHeight = mapStatus.gameMapData.height; if (p.dx <= 0 || p.dx >= mapWidth || p.dy <= 0 || p.dy >= mapHeight) { return true; } } return false; } AnimationController? showLocationController; showLocation() { final p = positionOnMap; if (p != null) { final dst = _model.mapRotateCenter; if (_model.isEnableUserLocation && !_model.isAlwaysShowMyLocation) { Get.find().show(); } if (dst != null) { mapStatus.movePicPointTo(p, dst); } } } _setStartAt(DateTime? time) { if (time != null) { gameState.pbGameSave.startAt = time.toPb(); } else { gameState.pbGameSave.clearStartAt(); } _model.gameSrcState.refresh(); } _setEndAt(DateTime? time) { if (time != null) { gameState.pbGameSave.stopAt = time.toPb(); } else { gameState.pbGameSave.clearStopAt(); } _model.gameSrcState.refresh(); } _recordLastPoint(Int64 id) { _lastCheckedPoint = id; _lastCheckedPointTime = now; } bool _isCheckTooFast(Int64 id) { return _lastCheckedPoint == id && now.difference(_lastCheckedPointTime) <= 10.seconds; } bool isPointStart(pb.GameSaveControlPoint p) { final seq = _controlPointWantSequence; if (seq.isNotEmpty) { return p.controlPointId == seq.first.id; } return false; } bool isPointFinish(pb.GameSaveControlPoint p) { final seq = _controlPointWantSequence; if (seq.isNotEmpty) { return p.controlPointId == seq.last.id; } return false; } void setBeginMatrix() { if (mapStatus.isSetBeginMatrix) { return; } final next = _model.nextPlanPoint; final f = _model.mapRotateCenter; if (next != null && f != null) { mapStatus.movePicPointTo(next.onMap, f); mapStatus.isSetBeginMatrix = true; } } pb.ControlPointSimple gameSaveControlPointFindInProject( pb.GameSaveControlPoint p) { final all = gameState.pbGameData.controlPointAll; for (final one in all) { if (one.id == p.controlPointId) { return one; } } return pb.ControlPointSimple(); } Future> _getControlPointWantSequence() async { final out = []; final checkedIndex = checkedCount; for (var i = 0; i < _controlPointWantSequence.length; i++) { final value = _controlPointWantSequence[i]; final one = value.toModel(); one.sn = '$i'; if (i == 0) { one.isStart = true; } if (i == _controlPointWantSequence.length - 1) { one.isFinish = true; } one.isSuccess = i < checkedIndex; one.isNext = i == checkedIndex; one.onMap = await mapStatus.gameMapData.worldToPixel(value.mapPosition.toModel()); final extraInfo = one.extraInfo; if(extraInfo is CPExtraInfoChoiceQuestion){ extraInfo.beanCount = gameState.pbGameData.answerSysPoint; await extraInfo.image?.loadMemory(); } out.add(one); } return out; } MControlPoint? getNextWantPoint(int offset) => _model.getNextWantPoint(offset); MControlPoint? getLastCheckedPoint() { if (checkedPointsHistory.isNotEmpty) { return checkedPointsHistory.last; } else { return null; } } void _checkGameStop({bool willToSettlementView = false}) { if ([ GameStatus.preparing, GameStatus.playing, ].contains(status)) { if (now.difference(createTime) > maxForcedEndDuration) { gameGiveUp(willToSettlementView: willToSettlementView); } } } Future _saveToDatabase() async { await DatabaseService.to.saveGameState(gameState); } Future _saveToServer() async { while (!isClosed) { try { final save = gameState.pbGameSave; await ApiService.to .gameSaveUpload(pb.GameSaveUploadRequest()..gameSave= save); return; } on GrpcError catch (e) { warn('上传失败:', e); if (e.code != StatusCode.unavailable) { return; } } catch (e) { warn('上传失败:', e); return; } await Future.delayed(500.milliseconds); } } void _checkHistoryAdd(MControlPoint cp) { checkedPointsHistory.add(cp); gameState.pbGameSave.checkedSortedList.add(cp.toPbSave()); } void _updateCheckHistory(){ gameState.pbGameSave.checkedSortedList.clear(); gameState.pbGameSave.checkedSortedList.addAll(checkedPointsHistory.map((e) => e.toPbSave()).toList()); } void save() { _updateCheckHistory(); _saveToDatabase().then((value) => info('本地存档完成')); _saveToServer().then((value) => info('上传存档完成')); } Future gameGiveUp({bool willToSettlementView = false}) async { // if(status == GameStatus.idle || status == GameStatus.settlement){ // return; // } _gameStopSetValues(); _updateCheckHistory(); _saveToDatabase().then((value) => info('本地存档完成')); _settlement(isGiveUp: true, willToSettlementView: willToSettlementView); } void _gameStopSetValues({DateTime? now}) { info('游戏结束'); if (now != null) { _setEndAt(now); } else { _setEndAt(this.now); } } void _updateNewCPState(MControlPoint point, {DateTime? now}) { now = now ?? this.now; point.checkAfterStart = now.difference(startAt ?? now); point.checkAfterPrev = point.checkAfterStart; point.checkDistanceAfterStart = _model.myPositionHistoryLen.value; point.checkDistanceAfterPrev = point.checkDistanceAfterStart; final last = getLastCheckedPoint(); if (last != null) { point.checkAfterPrev = point.checkAfterStart - last.checkAfterStart; point.checkDistanceAfterPrev = point.checkDistanceAfterStart - last.checkDistanceAfterStart; } } checkPointNFC( String identifier, void Function(MControlPoint cp) onChecked, Future Function () onConfirmFinish, void Function(pb.ControlPointSimple point) onProjectPoint, VoidCallback onNoPoint, ) { final checkedPoint = _findControlPointInRouteByNfcId(identifier); if (checkedPoint != null) { checkPoint(checkedPoint, onChecked, onConfirmFinish); } else { final point = _findControlPointInProjectByNfcId(identifier); if (point != null) { if (_isCheckTooFast(point.id)) { return; } final result = point.toModel(); _updateNewCPState(result); result.isSuccess = false; _checkHistoryAdd(result); save(); _recordLastPoint(point.id); onProjectPoint(point); } else { onNoPoint(); } } } checkPointGps( void Function(MControlPoint cp) onChecked, Future Function () onConfirmFinish, ) { if(_model.isInPlanControlPointArea){ pb.ControlPoint? checkedPoint; final point = _model.nextPlanPoint; if (point != null) { checkedPoint = _findControlPointInRouteByM(point); } checkPoint(checkedPoint!, onChecked, onConfirmFinish); }else if (_model.isInWantControlPointArea) { pb.ControlPoint? checkedPoint; final point = _model.getNextWantPoint(0); if (point != null) { checkedPoint = _findControlPointInRouteByM(point); } checkPoint(checkedPoint!, onChecked, onConfirmFinish); } } Future checkPoint( pb.ControlPoint checkedPoint, void Function (MControlPoint cp) onChecked, /// 打了结束点,询问是否结束 Future Function () onConfirmFinish, ) async{ final cp = checkedPoint.toModel(); if (rule.checkNeedReturn(cp)){ return; } final now = this.now; final result = rule.checkPoint(checkedPoint.toModel()); var isFinish = result.isSuccess && result.isFinish; if(!result.isSuccess && result.isFinish){ isFinish = await onConfirmFinish(); if(!isFinish){ return; } } _updateNewCPState(result, now: now); if(result.isStart && result.isSuccess){ _setStartAt(now); } if(isFinish){ _gameStopSetValues(now: now); } _checkHistoryAdd(result); if(startAt!=null){ gameState.pbGameSave.duration = now.difference(startAt!).toPb(); if(endAt != null){ gameState.pbGameSave.duration = endAt!.difference(startAt!).toPb(); } } _model.gameSrcState.refresh(); if(isFinish){ _settlement(); }else{ save(); } rule.recordLastPoint(cp); onChecked(result); } void showNextPoint() { final next = _model.nextPlanPoint; final focalPoint = _model.mapRotateCenter; if (next != null && focalPoint != null) { mapStatus.movePicPointTo(next.onMap, focalPoint); } } Future gameGiveUpAndToFinishView({GameState? state}) async { if (state != null) { _model.gameSrcState.value = state; } gameGiveUp(); SettlementView.show(); // Get.offAll(() => const GameFinishView(), // binding: GameFinishView.bindings()); } Future _gameReset() async { errorMsg.value = ''; _model.clear(); await _plugsClear(); } Future gameStart() async { info('项目Id[$activityId]准备开始'); await _gameReset(); final data = await ApiService.to.gameStart(activityId, mapRouteId); _model.gameSrcState.value = data.toGameState(); status = GameStatus.preparing; _saveToDatabase(); info('活动Id[$activityId],游戏Id[${gameState.pbGameData.gameId}]已开始'); } Future gameLoad() async { info('载入项目[$activityId], Id[${gameState.pbGameData.gameId}]'); await _plugsClear(); _loadProgress.value = 0; status = GameStatus.loading; _app.userProfile.cleanGameSettingsLock(); _app.userProfile.gameSettingsLoadLock(gameState.pbGameData.ruleList); /// 游戏规则 // rule = RuleInOrder(); _model.compassRealNorthOffset = gameState.pbGameData.declination * pi / 180; _loadProgress.value = _progressApi; final gameMap = gameState.pbGameData.mapZip.toGameMap(); mapStatus.gameMapData = gameMap; await gameMap.loadMemory(onReceiveProgress: (c, a) { if (a > 0) { var p = c.toDouble() / a; p = p * _progressMap + _progressApi; _loadProgress.value = p; } }); mapStatus.mapImageData.value = gameMap.pic!; // --- 确定地图图片首次缩放比例 --- final screenSize = _app.screenSize; final fitted = applyBoxFit( BoxFit.contain, Size(gameMap.width, gameMap.height), Size(screenSize.width, screenSize.height)); mapStatus.picFirstScale = fitted.destination.width / fitted.source.width; // -------------- _model.mapRotateCenter = Offset(screenSize.width / 2, screenSize.height / 2); await mapStatus.resetMatrix(); _model.controlPointWantSequence.value = await _getControlPointWantSequence(); _gameLoadCheckedCP(); final plugLocation = PlugLocation( gameMap: gameMap, lastInfo: _model.gameSrcState.value.pbGameSave.gameGpsInfos); _plugs.add(plugLocation); _plugs.add(PlugSportWear( lastHr: _model.gameSrcState.value.pbGameSave.gameHrInfos)); _plugs.add(PlugOrientation(mapStatus: mapStatus)); await _plugsAllInit(); _loadProgress.value = 1; status = GameStatus.playing; Wakelock.enable(); info('载入完成'); } void _gameLoadCheckedCP() { final controlPointWantIdValueMap = {}; final controlPointAllIdValueMap = {}; for (var one in _model.controlPointWantSequence) { controlPointWantIdValueMap[one.intId] = one; } for (var one in gameState.pbGameData.controlPointAll) { controlPointAllIdValueMap[one.id] = one; } for (var i = 0; i < gameState.pbGameSave.checkedSortedList.length; i++) { final one = gameState.pbGameSave.checkedSortedList[i]; final his = one.toModel(); final hisInfo1 = controlPointWantIdValueMap[his.intId]; final hisInfo2 = controlPointAllIdValueMap[his.intId]; if (hisInfo2 != null) { his.updateBySimple(hisInfo2); } if (hisInfo1 != null) { his.updateBy(hisInfo1); his.type = hisInfo1.type; } checkedPointsHistory.add(his); } for (var i = 0; i < checkedPointsHistory.length; i++) { final his = checkedPointsHistory[i]; if (i > 0) { final last = checkedPointsHistory[i - 1]; his.checkAfterPrev = his.checkAfterStart - last.checkAfterStart; his.checkDistanceAfterPrev = his.checkDistanceAfterStart - last.checkDistanceAfterStart; } else { his.checkAfterPrev = his.checkAfterStart; his.checkDistanceAfterPrev = his.checkDistanceAfterStart; } } } Future _plugsAllInit() async { for (var one in _plugs) { await one.init(); } } Future _plugsClear() async { for (var one in _plugs) { one.close(); } for (var one in _plugs) { await one.join(); } _plugs.clear(); } Settlement getSettlement() { return Settlement( data: gameState.pbGameData, save: gameState.pbGameSave, durationAfterStartCheck: beginDuration.value); } pb.ControlPoint? _findControlPointInRouteByNfcId(String identifier) { pb.ControlPoint? found; for (var one in _controlPointWantSequence) { for (var nfcId in one.nfcIdList) { if (nfcId.toUpperCase() == identifier.toUpperCase()) { found = one; break; } } } return found; } pb.ControlPoint? _findControlPointInRouteByM(MControlPoint point) { pb.ControlPoint? found; for (var one in _controlPointWantSequence) { if (one.id == point.intId) { found = one; break; } } return found; } pb.ControlPointSimple? _findControlPointInProjectByNfcId(String identifier) { for (var one in _controlPointAll) { for (var nfcId in one.nfcIdList) { if (nfcId.toUpperCase() == identifier.toUpperCase()) { return one; } } } return null; } Future _settlementDeal(bool isGiveUp) async { while (!isClosed) { try { final save = gameState.pbGameSave; await ApiService.to .gameSaveUpload(pb.GameSaveUploadRequest()..gameSave= save); info('进度已上传'); break; } on GrpcError catch (e) { if (e.code == StatusCode.unavailable) { errorMsg.value = '无法连接至服务器,正在重试'; } else { break; } error(e); } catch (e) { error(e); break; } } await _plugsClear(); while (!isClosed) { try { info('正在结算'); _lastGameSettlement = await ApiService.to .gameFinish(gameState.pbGameData.gameId, isGiveUp); info('结算完成'); break; } on GrpcError catch (e) { if (e.code == StatusCode.unavailable) { errorMsg.value = '网络错误,正在重试'; } else { break; } error(e); Future.delayed(500.milliseconds); } catch (e) { error(e); break; } } await _saveToDatabase(); status = GameStatus.idle; } _settlement({bool isGiveUp = false, bool willToSettlementView = false}) { _updateCheckHistory(); status = GameStatus.settlement; errorMsg.value = ''; _settlementDeal(isGiveUp).then((value) => info('结算完成')); if (willToSettlementView) { SettlementView.show(); } } @override void onReady() { _beginDurationTicker = Timer.periodic(100.milliseconds, (timer) { final startAt = this.startAt; if (startAt != null) { final end = endAt ?? now; beginDuration.value = end.difference(startAt); } else { beginDuration.value = 0.seconds; } }); } @override void onClose() { _checkStopTicker?.cancel(); _beginDurationTicker?.cancel(); } Future loadOnlineUnFinishGame() async { try { final online = await ApiService.to.getInGameData(); info('存在线上未完成游戏'); status = GameStatus.preparing; _model.gameSrcState.value = GameState() ..pbGameData = online.data ..pbGameSave = online.save; _checkGameStop(); return true; } on GrpcError catch (e) { if (e.code != StatusCode.notFound) { warn(e); } return false; } catch (e) { warn(e); return false; } } void workAutoSave() async { while (!isClosed) { await Future.delayed(500.milliseconds); if (_model.gameSrcState.value.pbGameData.gameId > 0 && _model.isStarted && !_model.isFinish) { await _saveToDatabase(); } } } void workTrajectory() async { while (!isClosed) { await Future.delayed(1000.milliseconds); final duration = _app.userProfile.inGameTrajectorySeconds.val.seconds; var out = []; for (var i = _model.myPositionHistory.length - 1; i >= 0; i--) { var one = _model.myPositionHistory[i]; if (now.difference(one.timestamp) > duration) { break; } out.add(await mapStatus.gameMapData.worldToPixel(one)); } _model.trajectoryPoints.clear(); _model.trajectoryPoints.addAll(out); } } void workPace() async { while (!isClosed) { await Future.delayed(1000.milliseconds); final startDuration = _model.startedDuration; if (startDuration.inMilliseconds == 0) { continue; } _model.paceSecondKm.value = pacePerKm(myPositionHistoryLenKm.km, startDuration); if (checkedPointsHistory.isEmpty) { _model.paceSecondKmFromLastCP.value = _model.paceSecondKm.value; } else { final cp = checkedPointsHistory.last; _model.paceSecondKmFromLastCP.value = pacePerKm( _model.myPositionHistoryLenFromLastCP, startDuration - cp.checkAfterStart); } } } static Future init() async { final gs = GameService(); gs.mapStatus.canVibrate = await Vibration.hasVibrator() ?? false; final save = await gs._database.getExistGameData(); if (save != null) { info('存在本地未完成游戏'); gs.status = GameStatus.preparing; gs._model.gameSrcState.value = save.toState(); gs._checkGameStop(); } else { gs.loadOnlineUnFinishGame(); } gs._checkStopTicker = Timer.periodic(100.milliseconds, (timer) { gs._checkGameStop(willToSettlementView: true); }); gs.workAutoSave(); gs.workTrajectory(); gs.workPace(); return gs; } }