game_std_controller.dart 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'dart:math';
  4. import 'package:get_storage/get_storage.dart';
  5. import 'package:trackoffical_app/model/game_map.dart';
  6. import 'package:trackoffical_app/model/game_person_data.dart';
  7. import 'package:trackoffical_app/service/app.dart';
  8. import 'package:trackoffical_app/service/game/game_instance.dart';
  9. import 'package:trackoffical_app/service/game/game_instance_std/game_instance_std.dart';
  10. import 'package:trackoffical_app/service/user_profile.dart';
  11. import 'package:trackoffical_app/utils.dart';
  12. import 'package:trackoffical_app/view/ingame/game_compass/game_compass_base.dart';
  13. import 'package:nfc_manager/nfc_manager.dart';
  14. import 'package:nfc_manager/platform_tags.dart';
  15. import '../../../generated/assets.dart';
  16. import '../../../logger.dart';
  17. import '../../../widget/matrix_gesture_detector.dart';
  18. import '../dialog/dialog_base.dart';
  19. import '../dialog/dialog_check_rich2.dart';
  20. import '../dialog/dialog_check_text.dart';
  21. import '../dialog/dialog_cp_order_err.dart';
  22. import 'route_planning.dart';
  23. import 'dialog_finish.dart';
  24. import 'package:screen_brightness/screen_brightness.dart';
  25. import '../../../model/m_control_point.dart';
  26. import '../../../widget/compass2.dart';
  27. import '../../game_settings.dart';
  28. import '../layer/layer_controller.dart';
  29. export '../layer/layer.dart';
  30. import 'package:sensor/sensor.dart' as sensor;
  31. import 'package:vector_math/vector_math_64.dart' as vec;
  32. import 'utils.dart';
  33. class GameStdController extends LayerController {
  34. GameStdController(this.instance);
  35. void _listenSettings() {
  36. final box = GetStorage();
  37. cancelListen = box.listen(_updateSettings);
  38. }
  39. void _updateSettings() {
  40. final profile = _app.userProfile;
  41. isSimpleDashboard.value = profile.gameSettingsSimpleDashboard.value;
  42. isEnableRoutePreview.value =profile.gameSettingsRoutePreview.value;
  43. isEnableUserLocationState.value = isEnableUserLocation;
  44. gameUIMode.value = profile.gameSettingsUIMode.value;
  45. }
  46. bool get isNfcScanUseDialog {
  47. final platform = _app.platformInfo;
  48. if (platform is PlatformInfoIOS) {
  49. if (platform.deviceVersion >= 8) {
  50. return true;
  51. }
  52. }
  53. return false;
  54. }
  55. bool get isShowCheckCPButton {
  56. final next = instance.model.nextWantPoint;
  57. final nextPlan = instance.model.nextPlanPoint;
  58. return isNfcScanUseDialog ||
  59. next?.type == MControlPointType.gps ||
  60. nextPlan?.type == MControlPointType.gps;
  61. }
  62. bool get isCheckCPButtonEnable {
  63. final next = instance.model.nextWantPoint;
  64. final nextPlan = instance.model.nextPlanPoint;
  65. final iosEnable = isNfcScanUseDialog &&
  66. (next?.type == MControlPointType.nfc ||
  67. nextPlan?.type == MControlPointType.nfc);
  68. return (instance.model.isInPlanControlPointArea ||
  69. instance.model.isInWantControlPointArea ||
  70. iosEnable) &&
  71. isShowCheckCPButton;
  72. }
  73. /// 下个点方向弧度
  74. double? get nextCPRadians {
  75. final p0 = instance.model.myPosition;
  76. final p1 = instance.model.nextPlanPoint?.position;
  77. if(p0 == null || p1 == null){
  78. return null;
  79. }
  80. return instance.compassRadiansFused.value + p0.directionTo(p1);
  81. }
  82. void showMyLocation() {
  83. if (isEnableUserLocation) {
  84. final p = instance.model.myPositionOnMap;
  85. if (p != null) {
  86. final dst = mapRotateCenter.value;
  87. moveOnMapPointToScreen(p, dst);
  88. }
  89. }
  90. }
  91. void setRotateCenterToScreenCenter() {
  92. final size = mapWidgetSize.value;
  93. if (size != null) {
  94. final screenCenter = Offset(size.width / 2, size.height / 2);
  95. mapRotateCenter.value = screenCenter;
  96. }
  97. }
  98. void setRotateCenterToCompassCenter() {
  99. mapRotateCenter.value = compassCenter;
  100. }
  101. @override
  102. void onMapSizeChange(Size size) {
  103. super.onMapSizeChange(size);
  104. flushRotateCenter();
  105. if (!isLockScreenCenterToMyPositionSystem) {
  106. showNextPoint();
  107. }
  108. mapDoScale(2.3);
  109. }
  110. void flushRotateCenter() {
  111. if (isMapRotateAtCompassCenter.value) {
  112. setRotateCenterToCompassCenter();
  113. } else {
  114. setRotateCenterToScreenCenter();
  115. }
  116. if (isLockScreenCenterToMyPositionSystem) {
  117. showMyLocation();
  118. }
  119. }
  120. void mapModeSwitch() {
  121. switch (mapRotationMode.value) {
  122. case MapMode.original:
  123. _setMapMode(MapMode.compass);
  124. break;
  125. case MapMode.compass:
  126. _setMapMode(MapMode.original);
  127. break;
  128. }
  129. }
  130. void _setMapMode(MapMode mode) {
  131. resetMatrix();
  132. mapRotationMode.value = mode;
  133. }
  134. @override
  135. bool get isEnableUserTouchRotation {
  136. if (mapRotationMode.value == MapMode.compass) {
  137. return false;
  138. }
  139. return true;
  140. }
  141. @override
  142. bool get isEnableUserTouchTranslation {
  143. if (isLockScreenCenterToMyPositionSystem) {
  144. return false;
  145. }
  146. return true;
  147. }
  148. void showNextPoint() {
  149. final next = instance.model.nextPlanPoint;
  150. if (next != null) {
  151. moveOnMapPointToScreen(next.onMap, mapRotateCenter.value);
  152. }
  153. }
  154. Future<void> toSettings() async {
  155. await Get.to(() => const GameSettingsView(isInGame: true));
  156. }
  157. Future<void> showCheckedCP() async {
  158. showCheckedPoints(instance.checkedPointsHistory);
  159. }
  160. Future<void> onSwitchCompassSize() async {
  161. _compassSizeIndex++;
  162. if (_compassSizeIndex >= _compassSizeList.length) {
  163. _compassSizeIndex = 0;
  164. }
  165. compassDiameter.value = _compassSizeList[_compassSizeIndex];
  166. flushRotateCenter();
  167. }
  168. void showCompassSwitch() {
  169. isShowCompass.value = !isShowCompass.value;
  170. }
  171. void forceExit() {
  172. instance.gameGiveUp();
  173. }
  174. _playCheckSound(MControlPoint cp) async {
  175. if (cp.isFinish) {
  176. return;
  177. }
  178. await Future.delayed(200.milliseconds);
  179. var src = cp.isSuccess ? 'assets/sound/ok.wav' : 'assets/sound/fail.wav';
  180. if (!cp.isSuccess && cp.isPlan) {
  181. src = Assets.soundPlanOk;
  182. }
  183. if (isClosed) {
  184. return;
  185. }
  186. await _app.soundPlayAsset(src);
  187. }
  188. _closeExistDialog() {
  189. if (Get.isOverlaysOpen) {
  190. Get.back();
  191. }
  192. }
  193. void dialogRoutePlanning(){
  194. Get.dialog(Center(
  195. child: Obx((){
  196. final model = instance.model;
  197. return RoutePlanning(
  198. want: model.controlPointWantSequence,
  199. nextPlanPoint: model.nextPlanPoint,
  200. nextWantPoint: model.nextWantPoint,
  201. onClick: (point){
  202. model.nextPlanPoint=point;
  203. });
  204. }) ),
  205. );
  206. }
  207. _onChecked(MControlPoint cp) {
  208. _closeExistDialog();
  209. final isEnablePunchErrorPrompt = _app.userProfile.gameSettingsPunchErrorPrompt.value;
  210. final next = instance.model.nextWantPoint;
  211. _playCheckSound(cp);
  212. if (cp.isSuccess) {
  213. if (cp.isStart) {
  214. Get.dialog(DialogCheckText(
  215. text: '开始',
  216. color: Colors.white,
  217. autoPlayAfter: 200.milliseconds,
  218. ));
  219. } else {
  220. showDialogCheckRich(cp, instance.model.gameQuestionShowDuration)
  221. .then((value) {
  222. instance.save();
  223. });
  224. }
  225. } else {
  226. if (cp.isPlan) {
  227. Get.dialog(const DialogCheckText(
  228. text: '已打点',
  229. color: Color(0xffff870d),
  230. ));
  231. } else if (next != null) {
  232. if(isEnablePunchErrorPrompt){
  233. dialogCPOrderErr(next);
  234. }
  235. } else {
  236. if(isEnablePunchErrorPrompt) {
  237. Get.dialog(
  238. const DialogCheckText(text: '打点错误', color: Colors.red));
  239. }
  240. }
  241. }
  242. }
  243. _onProjectPoint(MControlPointInProject point) {
  244. _closeExistDialog();
  245. _playCheckSound(MControlPoint()..isSuccess = false);
  246. Get.dialog(dialogTitle('非线路检查点', Colors.red, const Text('请检查地图线路'),
  247. offAfter: 3.seconds));
  248. }
  249. _onNoPoint() {
  250. _closeExistDialog();
  251. _playCheckSound(MControlPoint()..isSuccess = false);
  252. Get.dialog(dialogTitle('打点错误', Colors.red, const Text('不是检查点'),
  253. offAfter: 3.seconds));
  254. }
  255. Future<void> _onIosNfcStart() async {
  256. if (!(await App.to.isNfcAvailable)) {
  257. Get.showSnackbar(const GetSnackBar(
  258. message: 'NFC不可用',
  259. ));
  260. return;
  261. }
  262. NfcManager.instance.startSession(
  263. alertMessage: '请靠近打卡点',
  264. onDiscovered: (tag) async {
  265. try {
  266. await _onNfcDiscovered(tag);
  267. await NfcManager.instance.stopSession(alertMessage: '打卡成功');
  268. } catch (e) {
  269. await NfcManager.instance.stopSession(errorMessage: '$e');
  270. }
  271. },
  272. );
  273. }
  274. Future<void> onCheckControlPoint() async {
  275. final next = instance.model.nextWantPoint;
  276. if (next != null) {
  277. switch (next.type) {
  278. case MControlPointType.nfc:
  279. await _onIosNfcStart();
  280. break;
  281. case MControlPointType.gps:
  282. await instance.checkPointGps(_onChecked, _onConfirmFinish);
  283. break;
  284. }
  285. }
  286. }
  287. void updateCompassRotateMap(double radians) {
  288. if (mapRotationMode.value == MapMode.compass) {
  289. setRotate(radians);
  290. }
  291. }
  292. void _registerNFC() {
  293. if (!isNfcScanUseDialog) {
  294. NfcManager.instance.startSession(
  295. onDiscovered: _onNfcDiscovered, alertMessage: '保持NFC靠近');
  296. info('Nfc开始扫描');
  297. }
  298. }
  299. Future<void> _onNfcDiscovered(NfcTag tag) async {
  300. info("NFC: \n ${tag.data}");
  301. String identifier = "";
  302. if (Platform.isAndroid) {
  303. identifier = (NfcA.from(tag)?.identifier ??
  304. NfcB.from(tag)?.identifier ??
  305. NfcF.from(tag)?.identifier ??
  306. NfcV.from(tag)?.identifier ??
  307. Uint8List(0))
  308. .toHexString();
  309. }
  310. if (Platform.isIOS) {
  311. identifier = (Iso15693.from(tag)?.identifier ??
  312. Iso7816.from(tag)?.identifier ??
  313. MiFare.from(tag)?.identifier ??
  314. Uint8List(0))
  315. .toHexString();
  316. }
  317. info('Id: $identifier');
  318. instance.checkPointNFC(
  319. identifier, _onChecked, _onConfirmFinish, _onProjectPoint, _onNoPoint);
  320. }
  321. Future<bool> _onConfirmFinish() async {
  322. return true;
  323. // return await dialogAskConfirmFinish();
  324. }
  325. void _onPositionUpdate(List<Offset> offset) {
  326. if (offset.isNotEmpty) {
  327. final p = offset.last;
  328. if (isLockScreenCenterToMyPositionSystem) {
  329. showMyLocation();
  330. }
  331. }
  332. }
  333. Future<void> _workPlayDistanceSound()async{
  334. while(!isClosed){
  335. final model = instance.model;
  336. final distance = model.nextPlanCPDistance;
  337. if(distance != null && model.endAt==null){
  338. var d = 5200.milliseconds;
  339. String? src = 'assets/sound/beep.wav';
  340. if(distance < 30.meter){
  341. d = 1200.milliseconds;
  342. }
  343. if(distance < 10.meter){
  344. d = 700.milliseconds;
  345. }
  346. if((model.isInPlanControlPointArea && model.nextPlanPoint?.type == MControlPointType.gps)
  347. || (model.isInWantControlPointArea&& model.nextWantPoint?.type == MControlPointType.gps)){
  348. d = 3400.milliseconds;
  349. src = 'assets/sound/punch_alarm.mp3';
  350. }
  351. await _app.soundPlayAsset(src);
  352. await Future.delayed(d);
  353. }else{
  354. await Future.delayed(500.milliseconds);
  355. }
  356. }
  357. }
  358. @override
  359. void onReady() {
  360. super.onReady();
  361. _updateSettings();
  362. _listenSettings();
  363. setIsBrightnessMax(isBrightnessMax);
  364. _setMapMode(MapMode.compass);
  365. _workPlayDistanceSound();
  366. state.bindStream(instance.stateStream);
  367. settlementTip.bindStream(instance.errorMsg.stream);
  368. _subscriptions.add(instance.stateStream.listen((state) {
  369. debug('游戏状态:$state');
  370. if (state == GameInstanceState.closed) {
  371. dialogFinishResult(instance.finishData);
  372. }
  373. }));
  374. _subscriptions.add(instance.compassRadiansFused.listen((r) {
  375. updateCompassRotateMap(r);
  376. }));
  377. _subscriptions
  378. .add(instance.model.myPositionOnMapHistory.listen(_onPositionUpdate));
  379. _registerNFC();
  380. gameCompassController.duration.bindStream(instance.model.duration.stream);
  381. gameCompassController.compassRadians.bindStream(instance.compassRadiansSrc.stream);
  382. _subscriptions.add(instance.compassRadiansSrc.listen((p) {
  383. final p0 = instance.model.myPosition;
  384. final p1 = instance.model.nextPlanPoint?.position;
  385. if(p0 == null || p1 == null){
  386. return;
  387. }
  388. gameCompassController.nextPointRadians.value=p + p0.directionTo(p1);
  389. }));
  390. gameCompassController.heartRatePercent.bindStream(instance.model.heartRatePercent.stream);
  391. gameCompassController.heartRate.bindStream(instance.model.heartRate.stream);
  392. gameCompassController.stepCount.bindStream(instance.model.stepCount.stream);
  393. gameCompassController.kCal.bindStream(instance.model.kCal.stream);
  394. gameCompassController.ck.bindStream(instance.model.ck.stream);
  395. gameCompassController.ei.bindStream(instance.model.ei.stream);
  396. }
  397. @override
  398. void onClose() {
  399. super.onClose();
  400. for (final one in _subscriptions) {
  401. one.cancel();
  402. }
  403. if (!isNfcScanUseDialog) {
  404. NfcManager.instance.stopSession();
  405. }
  406. cancelListen?.call();
  407. state.close();
  408. settlementTip.close();
  409. }
  410. final gameCompassController = GameCompassController();
  411. final GameInstanceStd instance;
  412. final _app = App.to;
  413. UserProfile get _profile => _app.userProfile;
  414. final _subscriptions = <StreamSubscription>[];
  415. @override
  416. GameMap? get gameMap => instance.gameMapData;
  417. final isShowTrace = true.obs;
  418. final isMapRotateAtCompassCenter = false.obs;
  419. static const bottomBarHeight = 120.0;
  420. final isSimpleDashboard = true.obs;
  421. final isEnableRoutePreview = true.obs;
  422. final isEnableUserLocationState = true.obs;
  423. final gameUIMode = GameUIMode.electronicMap.obs;
  424. void Function()? cancelListen;
  425. var isLockScreenCenterToMyPosition = false;
  426. /// 用户设置是否锁定旋转中心
  427. bool get isLockScreenCenterToMyPositionSystem =>
  428. isLockScreenCenterToMyPosition && isEnableUserLocation;
  429. bool get isEnableUserLocation => _profile.gameSettingsShowMyLocation.value;
  430. set isEnableUserLocation(v) {
  431. _profile.gameSettingsShowMyLocation.value = v;
  432. }
  433. final compassDiameter = 160.0.obs;
  434. Offset get compassCenter => Offset(App.to.screenSize.width / 2,
  435. App.to.screenSize.height - bottomBarHeight - compassDiameter / 2);
  436. final mapRotationMode = MapMode.compass.obs;
  437. final state = GameInstanceState.uninitialized.obs;
  438. MNetImage get legend =>
  439. instance.model.gameSrcState.value.pbGameData.legendImage.toModel();
  440. bool get isBrightnessMax =>
  441. App.to.userProfile.isEnableInGameBrightnessMax.val;
  442. Future<void> setIsBrightnessMax(bool v) async {
  443. App.to.userProfile.isEnableInGameBrightnessMax.val = v;
  444. if (v) {
  445. await ScreenBrightness().setScreenBrightness(1);
  446. } else {
  447. await ScreenBrightness().resetScreenBrightness();
  448. }
  449. }
  450. final compassLevel = Compass2.levelMin.obs;
  451. var _compassSizeIndex = 0;
  452. final _compassSizeList = <double>[160.0, 200, 240, 280];
  453. final isShowCompass = true.obs;
  454. final isNoMapRulerScaleMode = false.obs;
  455. /// 是否显示下一个点的方向
  456. final isShowNextCPRadians = true.obs;
  457. final isShowRuler = false.obs;
  458. bool get isCheckCPButtonWarn =>
  459. instance.model.isInWantControlPointArea && isCheckCPButtonEnable;
  460. final settlementTip = '正在计算,请稍后'.obs;
  461. double get compassPlantRadian {
  462. if (mapRotationMode.value == MapMode.compass) {
  463. return instance.compassRadiansFused.value;
  464. }
  465. final ogRm = mapTransformMatrix.value.clone();
  466. double radian = MatrixGestureDetector.decomposeToValues(ogRm).rotation;
  467. return radian;
  468. }
  469. int get compassShowDegrees {
  470. var d1 = -(compassPlantRadian - instance.compassRadiansFused.value);
  471. if (mapRotationMode.value == MapMode.compass) {
  472. d1 = -compassPlantRadian;
  473. }
  474. var d = d1 * 180 ~/ pi;
  475. while (d < 0) {
  476. d += 360;
  477. }
  478. while (d > 360) {
  479. d -= 360;
  480. }
  481. return d;
  482. }
  483. /// 地图比例尺 1:[userSetMapScale]
  484. final Rx<double?> userSetMapScale = Rx(null);
  485. sensor.Orientation get orientation => instance.orientation.value;
  486. bool get isPhoneHorizontal {
  487. final x = vec.degrees(orientation.x);
  488. final y = vec.degrees(orientation.y);
  489. if (x.abs() > 30 || y.abs() > 30) {
  490. return false;
  491. } else {
  492. return true;
  493. }
  494. }
  495. bool get isShowPhoneHorizontalWarn =>
  496. (!isPhoneHorizontal) && (!instance.isPersonMoving);
  497. bool get isOutBoundary {
  498. final p = instance.model.myPositionOnMap;
  499. if (p != null) {
  500. final mapWidth = instance.gameMapData.width;
  501. final mapHeight = instance.gameMapData.height;
  502. if (p.dx <= 0 || p.dx >= mapWidth || p.dy <= 0 || p.dy >= mapHeight) {
  503. return true;
  504. }
  505. }
  506. return false;
  507. }
  508. bool get isShowOutBoundaryWarn =>
  509. App.to.userProfile.gameSettingsBoundaryWarn.value && isOutBoundary;
  510. }