import 'dart:async'; import 'dart:io'; import 'package:assets_audio_player/assets_audio_player.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:trackoffical_app/generated/assets.dart'; import 'package:trackoffical_app/model.dart'; import 'package:trackoffical_app/pb.dart' as pb; import 'package:trackoffical_app/service/app.dart'; import 'package:trackoffical_app/service/game/game_model.dart'; import 'package:trackoffical_app/service/sport_wear.dart'; import 'package:screen_brightness/screen_brightness.dart'; import '../game_settings.dart'; import 'dialog/dialog_base.dart'; import 'dialog/dialog_confirm_finish.dart'; import 'dialog/dialog_cp_order_err.dart'; import 'dialog/dialog_check_rich2.dart'; import 'dialog/dialog_check_text.dart'; import 'package:trackoffical_app/widget/compass2.dart'; import '../../model/m_control_point.dart'; import 'settlement_view.dart'; import '../../logger.dart'; import 'package:nfc_manager/nfc_manager.dart'; import '../../service/game/game.dart'; import '../../utils.dart'; import 'package:nfc_manager/platform_tags.dart'; import 'package:vector_math/vector_math_64.dart' as vec; import 'package:sensor/sensor.dart' as sensor; class InGameController extends GetxController { static double bottomBarHeight = 120.0; final service = GameService.to; bool get isShowCompass => service.isShowCompass.value; DateTime? get startAt => GameService.to.startAt; Duration get maxDuration => GameService.to.maxPassDuration; double compassAngleDegree = 0.0; DateTime? lastGyroscopeTime; bool get isNfcScanUseDialog => GameService.to.isNfcScanUseDialog; List get checkedPointHistory => GameService.to.checkedPointsHistory; final isShowTimeBar = true.obs; final phoneX = 0.obs; final phoneY = 0.obs; final legend = GameService.to.gameState.pbGameData.legendImage.toModel(); Function? disposeListen; final GameModel _model = Get.find(); StreamSubscription? _compassSubscription; StreamSubscription? _myPositionSubscription; final compassLevel = Compass2.levelMin.obs; final isMapRotateAtCompassCenter = false.obs; /// 是否显示下一个点的方向 final isShowNextCPRadians = true.obs; final isShowRuler = false.obs; var _compassSizeIndex = 0; final _compassSizeList = [160.0, 200, 240, 280]; final uiMode = App.to.userProfile.gameSettingsUIMode.value.obs; final _isUseRealNorth = Get.find().isUseRealNorth.obs; bool get isUseRealNorth => _isUseRealNorth.value; set isUseRealNorth(v){ _isUseRealNorth.value=v; _model.isUseRealNorth=v; } final _isInGameUISimplifyMode = App.to.userProfile.isInGameUISimplifyMode.val.obs; bool get isInGameUISimplifyMode => _isInGameUISimplifyMode.value; set isInGameUISimplifyMode(v){ _isInGameUISimplifyMode.value=v; App.to.userProfile.isInGameUISimplifyMode.val=v; } final _isEnableOutBoundaryWarn = App.to.userProfile.isEnableOutBoundaryWarn.val.obs; bool get isEnableOutBoundaryWarn => _isEnableOutBoundaryWarn.value; set isEnableOutBoundaryWarn(v){ _isEnableOutBoundaryWarn.value=v; App.to.userProfile.isEnableOutBoundaryWarn.val=v; } final _audioPlayer = AssetsAudioPlayer.newPlayer(); bool get isEnableUserLocation=>_model.isEnableUserLocation; set isEnableUserLocation(v){ _model.isEnableUserLocation=v; } final isStartShowWarn = _StorageValue(App.to.userProfile.isStartShowWarn); final isEnableGameSound = _StorageValue(App.to.userProfile.isEnableGameSound); final isEnableGameVibrate = _StorageValue(App.to.userProfile.isEnableGameVibrate); final isEnableGameCulture = _StorageValue(App.to.userProfile.isEnableGameCulture); final isEnableInGameStartCPBubble = _StorageValue(App.to.userProfile.isEnableInGameStartCPBubble); bool get isShowOutBoundaryWarn => isEnableOutBoundaryWarn && service.isOutBoundary; bool get isBrightnessMax => App.to.userProfile.isEnableInGameBrightnessMax.val; Future setIsBrightnessMax(bool v)async{ App.to.userProfile.isEnableInGameBrightnessMax.val=v; if (v) { await ScreenBrightness().setScreenBrightness(1); } else { await ScreenBrightness().resetScreenBrightness(); } } Future toSettings()async{ await Get.to(() => const GameSettingsView(isInGame: true)); } double? get nextControlPointDistanceKm{ final real = _model.nextPlanCPDistanceKm; if(real == null){ return null; } var show = real.km - _model.controlPointEffectiveAreaRadiusKm + GameModel.controlPointEffectiveAreaRadiusKmOffset; if(show < 0 ){ show = 0; } return show; } final _trajectorySeconds = App.to.userProfile.inGameTrajectorySeconds.val.obs; int get trajectorySeconds => _trajectorySeconds.value; set trajectorySeconds(v){ _trajectorySeconds.value=v; App.to.userProfile.inGameTrajectorySeconds.val=v; } Offset get compassCenter => Offset( App.to.screenSize.width/2, App.to.screenSize.height - bottomBarHeight - compassDiameter/2); double get compassDiameter => _model.compassDiameter.value; sensor.Orientation get orientation => _model.orientation.value; bool get isPhoneHorizontal{ final x = vec.degrees(orientation.x); final y = vec.degrees(orientation.y); if (x.abs() > 30 || y.abs()>30){ return false; }else{ return true; } } bool get isShowPhoneHorizontalWarn => (!isPhoneHorizontal)&&(!_model.isMoving); final isNoMapRulerScaleMode = false.obs; bool get isShowCheckCPButton{ final next = service.getNextWantPoint(0); final nextPlan = _model.nextPlanPoint; return isNfcScanUseDialog || next?.type==MControlPointType.gps || nextPlan?.type==MControlPointType.gps; } bool get isCheckCPButtonEnable { final next = service.getNextWantPoint(0); final nextPlan = _model.nextPlanPoint; final iosEnable = isNfcScanUseDialog && (next?.type==MControlPointType.nfc || nextPlan?.type==MControlPointType.nfc); return (_model.isInPlanControlPointArea ||_model.isInWantControlPointArea || iosEnable) && isShowCheckCPButton; } bool get isCheckCPButtonWarn => _model.isInWantControlPointArea && isCheckCPButtonEnable; bool get isSportWearConnected => Get.find().connectedSportWear.value != null; /// 地图比例尺 1:[mapScale] final Rx mapScale = Rx(null); String get beginDurationStr { final startAt = this.startAt; if (startAt == null) { return '00:00:00'; } else { final countDown = GameService.to.beginDuration.value; var s = countDown.inSeconds; final h = (s / 3600).floor(); final hStr = h.twoDigits(); final m = ((s - h * 3600) / 60).floor(); final mStr = m.twoDigits(); final sStr = (s - h * 3600 - m * 60).twoDigits(); return '$hStr:$mStr:$sStr'; } } /// 下个点方向弧度 double? get nextCPRadians { final p0 = _model.myPosition.value; final p1 = _model.nextPlanPoint?.position; if(p0 == null || p1 == null){ return null; } return _model.compassRadiansFused.value + p0.directionTo(p1); } void setRotateCenterToScreenCenter(){ _model.setRotateCenterToScreenCenter(); } void setRotateCenterToCompassCenter(){ _model.mapRotateCenter = compassCenter; } void flushRotateCenter(){ if (isMapRotateAtCompassCenter.value) { setRotateCenterToCompassCenter(); } else { setRotateCenterToScreenCenter(); } if(_model.isLockScreenCenterToMyPositionSystem){ service.showLocation(); } } void showCompassSwitch() { service.isShowCompass.value = !service.isShowCompass.value; } void showTimeSwitch(){ isShowTimeBar.value = !isShowTimeBar.value; } var _lastRelocateTime = DateTime.now(); @override void onReady() { final box = GetStorage(); disposeListen = box.listen((){ uiMode.value = App.to.userProfile.gameSettingsUIMode.value; }); setIsBrightnessMax(isBrightnessMax); onRequestData().then((value) => info('数据加载成功')); final gameService = GameService.to; final mapStatus = gameService.mapStatus; _compassSubscription = _model.compassRadiansFused.listen((v) { if (mapStatus.mapMode.value == MapMode.compass) { gameService.setBeginMatrix(); mapStatus.setRotate(v); } }); _myPositionSubscription = _model.myPosition.listen((p0) { if(p0 != null && _model.isLockScreenCenterToMyPositionSystem){ final now = DateTime.now(); if(now.difference(_lastRelocateTime) > 100.milliseconds){ gameService.showLocation(); _lastRelocateTime = now; } } }); if (!isNfcScanUseDialog){ NfcManager.instance.startSession( onDiscovered: _onNfcDiscovered, alertMessage: '保持NFC靠近'); info('Nfc开始扫描'); } workPlayDistanceSound(); } Future _onNfcDiscovered(NfcTag tag) async { info("NFC: \n ${tag.data}"); String identifier = ""; if (Platform.isAndroid) { identifier = (NfcA.from(tag)?.identifier ?? NfcB.from(tag)?.identifier ?? NfcF.from(tag)?.identifier ?? NfcV.from(tag)?.identifier ?? Uint8List(0)) .toHexString(); } if (Platform.isIOS) { identifier = (Iso15693.from(tag)?.identifier ?? Iso7816.from(tag)?.identifier ?? MiFare.from(tag)?.identifier ?? Uint8List(0)) .toHexString(); } info('Id: $identifier'); GameService.to.checkPointNFC( identifier, _onChecked, _onConfirmFinish, _onProjectPoint, _onNoPoint ); } Future onCheckControlPoint() async { final next = _model.getNextWantPoint(0); if(next != null){ switch (next.type){ case MControlPointType.nfc: await onIosNfcStart(); break; case MControlPointType.gps: await GameService.to.checkPointGps( _onChecked, _onConfirmFinish ); break; } } } Future onCheckGpsControlPoint() async { GameService.to.checkPointGps( _onChecked, _onConfirmFinish ); } Future onSwitchCompassSize() async { _compassSizeIndex++; if(_compassSizeIndex>=_compassSizeList.length){ _compassSizeIndex=0; } _model.compassDiameter.value = _compassSizeList[_compassSizeIndex]; flushRotateCenter(); } _closeExistDialog(){ if(Get.isOverlaysOpen){ Get.back(); } } _playCheckSound(MControlPoint cp) async{ if(cp.isFinish){ return; } await Future.delayed(200.milliseconds); var src = cp.isSuccess? 'assets/sound/ok.wav': 'assets/sound/fail.wav'; if(!cp.isSuccess && cp.isPlan){ src = Assets.soundPlanOk; } if(isEnableGameSound.value) { AssetsAudioPlayer.newPlayer().open( Audio(src), autoStart: true, showNotification: false, ); } } Future _onConfirmFinish()async{ return true; return await dialogAskConfirmFinish(); } _onChecked(MControlPoint cp){ _closeExistDialog(); final status = GameService.to.status; info('状态:$status'); final isOver = [GameStatus.settlement, GameStatus.idle].contains(status); final next = GameService.to.getNextWantPoint(0); onConfirm(){ if(isOver){ SettlementView.show(); } } _playCheckSound(cp); if(cp.isSuccess){ if(cp.isStart){ Get.dialog(DialogCheckText(text: '开始', color: Colors.white, autoPlayAfter: 200.milliseconds,)); }else if(cp.isFinish){ onConfirm(); }else{ showDialogCheckRich(cp, _model.gameQuestionShowDuration).then((value) { service.save(); onConfirm(); } ); } }else{ if(isOver){ onConfirm(); }else{ if(cp.isPlan){ Get.dialog(const DialogCheckText( text: '已打点', color: Color(0xffff870d), )); }else if(next!= null){ dialogCPOrderErr(next); }else{ Get.dialog(const DialogCheckText(text: '打点错误', color: Colors.red)); } } } } _onProjectPoint(pb.ControlPointSimple point){ _closeExistDialog(); _playCheckSound(MControlPoint() ..isSuccess=false); Get.dialog(dialogTitle( '非线路检查点', Colors.red, const Text('请检查地图线路'), offAfter: 3.seconds )); } _onNoPoint(){ _closeExistDialog(); _playCheckSound(MControlPoint() ..isSuccess=false); Get.dialog(dialogTitle( '打点错误', Colors.red, const Text('不是检查点'), offAfter: 3.seconds )); } Future onIosNfcStart() async { if (!(await App.to.isNfcAvailable)) { Get.showSnackbar(const GetSnackBar( message: 'NFC不可用', )); return; } NfcManager.instance.startSession( alertMessage: '请靠近打卡点', onDiscovered: (tag) async { try { await _onNfcDiscovered(tag); await NfcManager.instance.stopSession(alertMessage: '打卡成功'); } catch (e) { await NfcManager.instance.stopSession(errorMessage: '$e'); } }, ); } Future onRequestData() async {} Future forceExit() async { await GameService.to.gameGiveUpAndToFinishView(); } Future workPlayDistanceSound()async{ while(!isClosed){ final disKm = nextControlPointDistanceKm; if(disKm != null && service.endAt==null){ final disM = disKm * 1000; var d = 5200.milliseconds; String? src = 'assets/sound/beep.wav'; if(disM < 30){ d = 1200.milliseconds; } if(disM < 10){ d = 700.milliseconds; } if((_model.isInPlanControlPointArea && _model.nextPlanPoint?.type == MControlPointType.gps) || (_model.isInWantControlPointArea&& _model.getNextWantPoint(0)?.type == MControlPointType.gps)){ d = 3400.milliseconds; src = 'assets/sound/punch_alarm.mp3'; } if(isEnableGameSound.value){ await _audioPlayer.open( Audio(src), autoStart: false, showNotification: false, ); await _audioPlayer.play(); } await Future.delayed(d); }else{ await Future.delayed(500.milliseconds); } } } @override void onClose() { disposeListen?.call(); _audioPlayer.stop(); _myPositionSubscription?.cancel(); _compassSubscription?.cancel(); if (!isNfcScanUseDialog) { NfcManager.instance.stopSession(); } debug('停止指南'); } } class _StorageValue{ _StorageValue(this._data): _state=_data.val.obs; final ReadWriteValue _data; final Rx _state; T get value => _state.value; set value(T v){ _state.value = v; _data.val = v; } } class InGameControllerMock extends InGameController { @override Future onRequestData() async {} }