import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:get_storage/get_storage.dart'; import 'package:trackoffical_app/model/game_map.dart'; import 'package:trackoffical_app/model/game_person_data.dart'; import 'package:trackoffical_app/service/app.dart'; import 'package:trackoffical_app/service/game/game_instance.dart'; import 'package:trackoffical_app/service/game/game_instance_std/game_instance_std.dart'; import 'package:trackoffical_app/service/user_profile.dart'; import 'package:trackoffical_app/utils.dart'; import 'package:trackoffical_app/view/ingame/game_compass/game_compass_base.dart'; import 'package:nfc_manager/nfc_manager.dart'; import 'package:nfc_manager/platform_tags.dart'; import '../../../generated/assets.dart'; import '../../../logger.dart'; import '../../../widget/matrix_gesture_detector.dart'; import '../dialog/dialog_base.dart'; import '../dialog/dialog_check_rich2.dart'; import '../dialog/dialog_check_text.dart'; import '../dialog/dialog_cp_order_err.dart'; import 'route_planning.dart'; import 'dialog_finish.dart'; import 'package:screen_brightness/screen_brightness.dart'; import '../../../model/m_control_point.dart'; import '../../../widget/compass2.dart'; import '../../game_settings.dart'; import '../layer/layer_controller.dart'; export '../layer/layer.dart'; import 'package:sensor/sensor.dart' as sensor; import 'package:vector_math/vector_math_64.dart' as vec; import 'utils.dart'; class GameStdController extends LayerController { GameStdController(this.instance); void _listenSettings() { final box = GetStorage(); cancelListen = box.listen(_updateSettings); } void _updateSettings() { final profile = _app.userProfile; isSimpleDashboard.value = profile.gameSettingsSimpleDashboard.value; isEnableRoutePreview.value =profile.gameSettingsRoutePreview.value; isEnableUserLocationState.value = isEnableUserLocation; gameUIMode.value = profile.gameSettingsUIMode.value; } bool get isNfcScanUseDialog { final platform = _app.platformInfo; if (platform is PlatformInfoIOS) { if (platform.deviceVersion >= 8) { return true; } } return false; } bool get isShowCheckCPButton { final next = instance.model.nextWantPoint; final nextPlan = instance.model.nextPlanPoint; return isNfcScanUseDialog || next?.type == MControlPointType.gps || nextPlan?.type == MControlPointType.gps; } bool get isCheckCPButtonEnable { final next = instance.model.nextWantPoint; final nextPlan = instance.model.nextPlanPoint; final iosEnable = isNfcScanUseDialog && (next?.type == MControlPointType.nfc || nextPlan?.type == MControlPointType.nfc); return (instance.model.isInPlanControlPointArea || instance.model.isInWantControlPointArea || iosEnable) && isShowCheckCPButton; } /// 下个点方向弧度 double? get nextCPRadians { final p0 = instance.model.myPosition; final p1 = instance.model.nextPlanPoint?.position; if(p0 == null || p1 == null){ return null; } return instance.compassRadiansFused.value + p0.directionTo(p1); } void showMyLocation() { if (isEnableUserLocation) { final p = instance.model.myPositionOnMap; if (p != null) { final dst = mapRotateCenter.value; moveOnMapPointToScreen(p, dst); } } } void setRotateCenterToScreenCenter() { final size = mapWidgetSize.value; if (size != null) { final screenCenter = Offset(size.width / 2, size.height / 2); mapRotateCenter.value = screenCenter; } } void setRotateCenterToCompassCenter() { mapRotateCenter.value = compassCenter; } @override void onMapSizeChange(Size size) { super.onMapSizeChange(size); flushRotateCenter(); if (!isLockScreenCenterToMyPositionSystem) { showNextPoint(); } mapDoScale(2.3); } void flushRotateCenter() { if (isMapRotateAtCompassCenter.value) { setRotateCenterToCompassCenter(); } else { setRotateCenterToScreenCenter(); } if (isLockScreenCenterToMyPositionSystem) { showMyLocation(); } } void mapModeSwitch() { switch (mapRotationMode.value) { case MapMode.original: _setMapMode(MapMode.compass); break; case MapMode.compass: _setMapMode(MapMode.original); break; } } void _setMapMode(MapMode mode) { resetMatrix(); mapRotationMode.value = mode; } @override bool get isEnableUserTouchRotation { if (mapRotationMode.value == MapMode.compass) { return false; } return true; } @override bool get isEnableUserTouchTranslation { if (isLockScreenCenterToMyPositionSystem) { return false; } return true; } void showNextPoint() { final next = instance.model.nextPlanPoint; if (next != null) { moveOnMapPointToScreen(next.onMap, mapRotateCenter.value); } } Future toSettings() async { await Get.to(() => const GameSettingsView(isInGame: true)); } Future showCheckedCP() async { showCheckedPoints(instance.checkedPointsHistory); } Future onSwitchCompassSize() async { _compassSizeIndex++; if (_compassSizeIndex >= _compassSizeList.length) { _compassSizeIndex = 0; } compassDiameter.value = _compassSizeList[_compassSizeIndex]; flushRotateCenter(); } void showCompassSwitch() { isShowCompass.value = !isShowCompass.value; } void forceExit() { instance.gameGiveUp(); } _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 (isClosed) { return; } await _app.soundPlayAsset(src); } _closeExistDialog() { if (Get.isOverlaysOpen) { Get.back(); } } void dialogRoutePlanning(){ Get.dialog(Center( child: Obx((){ final model = instance.model; return RoutePlanning( want: model.controlPointWantSequence, nextPlanPoint: model.nextPlanPoint, nextWantPoint: model.nextWantPoint, onClick: (point){ model.nextPlanPoint=point; }); }) ), ); } _onChecked(MControlPoint cp) { _closeExistDialog(); final isEnablePunchErrorPrompt = _app.userProfile.gameSettingsPunchErrorPrompt.value; final next = instance.model.nextWantPoint; _playCheckSound(cp); if (cp.isSuccess) { if (cp.isStart) { Get.dialog(DialogCheckText( text: '开始', color: Colors.white, autoPlayAfter: 200.milliseconds, )); } else { showDialogCheckRich(cp, instance.model.gameQuestionShowDuration) .then((value) { instance.save(); }); } } else { if (cp.isPlan) { Get.dialog(const DialogCheckText( text: '已打点', color: Color(0xffff870d), )); } else if (next != null) { if(isEnablePunchErrorPrompt){ dialogCPOrderErr(next); } } else { if(isEnablePunchErrorPrompt) { Get.dialog( const DialogCheckText(text: '打点错误', color: Colors.red)); } } } } _onProjectPoint(MControlPointInProject 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 onCheckControlPoint() async { final next = instance.model.nextWantPoint; if (next != null) { switch (next.type) { case MControlPointType.nfc: await _onIosNfcStart(); break; case MControlPointType.gps: await instance.checkPointGps(_onChecked, _onConfirmFinish); break; } } } void updateCompassRotateMap(double radians) { if (mapRotationMode.value == MapMode.compass) { setRotate(radians); } } void _registerNFC() { if (!isNfcScanUseDialog) { NfcManager.instance.startSession( onDiscovered: _onNfcDiscovered, alertMessage: '保持NFC靠近'); info('Nfc开始扫描'); } } 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'); instance.checkPointNFC( identifier, _onChecked, _onConfirmFinish, _onProjectPoint, _onNoPoint); } Future _onConfirmFinish() async { return true; // return await dialogAskConfirmFinish(); } void _onPositionUpdate(List offset) { if (offset.isNotEmpty) { final p = offset.last; if (isLockScreenCenterToMyPositionSystem) { showMyLocation(); } } } Future _workPlayDistanceSound()async{ while(!isClosed){ final model = instance.model; final distance = model.nextPlanCPDistance; if(distance != null && model.endAt==null){ var d = 5200.milliseconds; String? src = 'assets/sound/beep.wav'; if(distance < 30.meter){ d = 1200.milliseconds; } if(distance < 10.meter){ d = 700.milliseconds; } if((model.isInPlanControlPointArea && model.nextPlanPoint?.type == MControlPointType.gps) || (model.isInWantControlPointArea&& model.nextWantPoint?.type == MControlPointType.gps)){ d = 3400.milliseconds; src = 'assets/sound/punch_alarm.mp3'; } await _app.soundPlayAsset(src); await Future.delayed(d); }else{ await Future.delayed(500.milliseconds); } } } @override void onReady() { super.onReady(); _updateSettings(); _listenSettings(); setIsBrightnessMax(isBrightnessMax); _setMapMode(MapMode.compass); _workPlayDistanceSound(); state.bindStream(instance.stateStream); settlementTip.bindStream(instance.errorMsg.stream); _subscriptions.add(instance.stateStream.listen((state) { debug('游戏状态:$state'); if (state == GameInstanceState.closed) { dialogFinishResult(instance.finishData); } })); _subscriptions.add(instance.compassRadiansFused.listen((r) { updateCompassRotateMap(r); })); _subscriptions .add(instance.model.myPositionOnMapHistory.listen(_onPositionUpdate)); _registerNFC(); gameCompassController.duration.bindStream(instance.model.duration.stream); gameCompassController.compassRadians.bindStream(instance.compassRadiansSrc.stream); _subscriptions.add(instance.compassRadiansSrc.listen((p) { final p0 = instance.model.myPosition; final p1 = instance.model.nextPlanPoint?.position; if(p0 == null || p1 == null){ return; } gameCompassController.nextPointRadians.value=p + p0.directionTo(p1); })); gameCompassController.heartRatePercent.bindStream(instance.model.heartRatePercent.stream); gameCompassController.heartRate.bindStream(instance.model.heartRate.stream); gameCompassController.stepCount.bindStream(instance.model.stepCount.stream); gameCompassController.kCal.bindStream(instance.model.kCal.stream); gameCompassController.ck.bindStream(instance.model.ck.stream); gameCompassController.ei.bindStream(instance.model.ei.stream); } @override void onClose() { super.onClose(); for (final one in _subscriptions) { one.cancel(); } if (!isNfcScanUseDialog) { NfcManager.instance.stopSession(); } cancelListen?.call(); state.close(); settlementTip.close(); } final gameCompassController = GameCompassController(); final GameInstanceStd instance; final _app = App.to; UserProfile get _profile => _app.userProfile; final _subscriptions = []; @override GameMap? get gameMap => instance.gameMapData; final isShowTrace = true.obs; final isMapRotateAtCompassCenter = false.obs; static const bottomBarHeight = 120.0; final isSimpleDashboard = true.obs; final isEnableRoutePreview = true.obs; final isEnableUserLocationState = true.obs; final gameUIMode = GameUIMode.electronicMap.obs; void Function()? cancelListen; var isLockScreenCenterToMyPosition = false; /// 用户设置是否锁定旋转中心 bool get isLockScreenCenterToMyPositionSystem => isLockScreenCenterToMyPosition && isEnableUserLocation; bool get isEnableUserLocation => _profile.gameSettingsShowMyLocation.value; set isEnableUserLocation(v) { _profile.gameSettingsShowMyLocation.value = v; } final compassDiameter = 160.0.obs; Offset get compassCenter => Offset(App.to.screenSize.width / 2, App.to.screenSize.height - bottomBarHeight - compassDiameter / 2); final mapRotationMode = MapMode.compass.obs; final state = GameInstanceState.uninitialized.obs; MNetImage get legend => instance.model.gameSrcState.value.pbGameData.legendImage.toModel(); 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(); } } final compassLevel = Compass2.levelMin.obs; var _compassSizeIndex = 0; final _compassSizeList = [160.0, 200, 240, 280]; final isShowCompass = true.obs; final isNoMapRulerScaleMode = false.obs; /// 是否显示下一个点的方向 final isShowNextCPRadians = true.obs; final isShowRuler = false.obs; bool get isCheckCPButtonWarn => instance.model.isInWantControlPointArea && isCheckCPButtonEnable; final settlementTip = '正在计算,请稍后'.obs; double get compassPlantRadian { if (mapRotationMode.value == MapMode.compass) { return instance.compassRadiansFused.value; } final ogRm = mapTransformMatrix.value.clone(); double radian = MatrixGestureDetector.decomposeToValues(ogRm).rotation; return radian; } int get compassShowDegrees { var d1 = -(compassPlantRadian - instance.compassRadiansFused.value); if (mapRotationMode.value == MapMode.compass) { d1 = -compassPlantRadian; } var d = d1 * 180 ~/ pi; while (d < 0) { d += 360; } while (d > 360) { d -= 360; } return d; } /// 地图比例尺 1:[userSetMapScale] final Rx userSetMapScale = Rx(null); sensor.Orientation get orientation => instance.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) && (!instance.isPersonMoving); bool get isOutBoundary { final p = instance.model.myPositionOnMap; if (p != null) { final mapWidth = instance.gameMapData.width; final mapHeight = instance.gameMapData.height; if (p.dx <= 0 || p.dx >= mapWidth || p.dy <= 0 || p.dy >= mapHeight) { return true; } } return false; } bool get isShowOutBoundaryWarn => App.to.userProfile.gameSettingsBoundaryWarn.value && isOutBoundary; }