pubspec.yaml
name: gearcombination
description: "GearCombination"
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 2.1.0+13
environment:
sdk: ^3.8.0-148.0.dev
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
package_info_plus: ^9.0.0
shared_preferences: ^2.0.17
flutter_localizations: #多言語ライブラリの本体 # .arbファイルを更新したら flutter gen-l10n
sdk: flutter
intl: ^0.20.2 #多言語やフォーマッタなどの関連ライブラリ
google_mobile_ads: ^6.0.0
just_audio: ^0.10.4
flutter_svg: ^2.0.9
dev_dependencies:
flutter_test:
sdk: flutter
flutter_launcher_icons: ^0.14.4 #flutter pub run flutter_launcher_icons
flutter_native_splash: ^2.3.5 #flutter pub run flutter_native_splash:create
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
flutter_icons:
android: "launcher_icon"
ios: true
image_path: "assets/icon/icon.png"
adaptive_icon_background: "assets/icon/icon_back.png"
adaptive_icon_foreground: "assets/icon/icon_fore.png"
flutter_native_splash:
color: '#2A01AD'
image: 'assets/image/splash.png'
color_dark: '#2A01AD'
image_dark: 'assets/image/splash.png'
fullscreen: true
android_12:
icon_background_color: '#2A01AD'
image: 'assets/image/splash.png'
icon_background_color_dark: '#2A01AD'
image_dark: 'assets/image/splash.png'
# The following section is specific to Flutter packages.
flutter:
generate: true #pub get時に多言語対応のファイルが自動生成される
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
assets:
- assets/image/
- assets/sound/
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
lib/ad_banner_widget.dart
import 'package:flutter/cupertino.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:gearcombination/ad_manager.dart';
class AdBannerWidget extends StatefulWidget {
final AdManager adManager;
const AdBannerWidget({super.key, required this.adManager});
@override
State<AdBannerWidget> createState() => _AdBannerWidgetState();
}
class _AdBannerWidgetState extends State<AdBannerWidget> {
int _lastBannerWidthDp = 0;
bool _isAdLoaded = false;
bool _isLoading = false;
@override
Widget build(BuildContext context) {
return SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
final int width = constraints.maxWidth.isFinite
? constraints.maxWidth.truncate()
: MediaQuery.of(context).size.width.truncate();
final bannerAd = widget.adManager.bannerAd;
if (width > 0) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
final bannerAd = widget.adManager.bannerAd;
final bool widthChanged = _lastBannerWidthDp != width;
final bool sizeMismatch =
bannerAd == null || bannerAd.size.width != width;
if ((widthChanged || !_isAdLoaded || sizeMismatch) &&
!_isLoading) {
_lastBannerWidthDp = width;
setState(() {
_isAdLoaded = false;
_isLoading = true;
});
widget.adManager.loadAdaptiveBannerAd(width, () {
if (mounted) {
setState(() {
_isAdLoaded = true;
_isLoading = false;
});
}
});
}
}
});
}
if (_isAdLoaded && bannerAd != null) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: bannerAd.size.width.toDouble(),
height: bannerAd.size.height.toDouble(),
child: AdWidget(ad: bannerAd),
),
],
)
]
);
} else {
return const SizedBox.shrink();
}
},
),
);
}
}
lib/ad_manager.dart
import 'dart:async';
import 'dart:io' show Platform;
import 'dart:ui';
import 'package:google_mobile_ads/google_mobile_ads.dart';
class AdManager {
// Test IDs
// static const String _androidAdUnitId = "ca-app-pub-3940256099942544/6300978111";
// static const String _iosAdUnitId = "ca-app-pub-3940256099942544/2934735716";
// Production IDs
static const String _androidAdUnitId = "ca-app-pub-0/0";
static const String _iosAdUnitId = "ca-app-pub-0/0";
static String get _adUnitId =>
Platform.isIOS ? _iosAdUnitId : _androidAdUnitId;
BannerAd? _bannerAd;
int _lastWidthPx = 0;
VoidCallback? _onLoadedCb;
Timer? _retryTimer;
int _retryAttempt = 0;
BannerAd? get bannerAd => _bannerAd;
Future<void> loadAdaptiveBannerAd(
int widthPx,
VoidCallback onAdLoaded,
) async {
_onLoadedCb = onAdLoaded;
_lastWidthPx = widthPx;
_retryAttempt = 0;
_retryTimer?.cancel();
_startLoad(widthPx);
}
Future<void> _startLoad(int widthPx) async {
_bannerAd?.dispose();
AnchoredAdaptiveBannerAdSize? adaptiveSize;
try {
adaptiveSize =
await AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(
widthPx,
);
} catch (_) {
adaptiveSize = null;
}
final AdSize size = adaptiveSize ?? AdSize.fullBanner;
_bannerAd = BannerAd(
adUnitId: _adUnitId,
request: const AdRequest(),
size: size,
listener: BannerAdListener(
onAdLoaded: (ad) {
_retryTimer?.cancel();
_retryAttempt = 0;
final cb = _onLoadedCb;
if (cb != null) {
cb();
}
},
onAdFailedToLoad: (ad, err) {
ad.dispose();
_scheduleRetry();
},
),
)..load();
}
void _scheduleRetry() {
_retryTimer?.cancel();
// Exponential backoff: 3s, 6s, 12s, max 30s
_retryAttempt = (_retryAttempt + 1).clamp(1, 5);
final seconds = _retryAttempt >= 4 ? 30 : (3 << (_retryAttempt - 1));
_retryTimer = Timer(Duration(seconds: seconds), () {
_startLoad(_lastWidthPx > 0 ? _lastWidthPx : 320);
});
}
void dispose() {
_bannerAd?.dispose();
_retryTimer?.cancel();
}
}
lib/audio_play.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-01-27
///
library;
import 'package:just_audio/just_audio.dart';
import 'package:gearcombination/const_value.dart';
class AudioPlay {
//音を重ねて連続再生できるようにインスタンスを用意しておき、順繰りに使う。
static final List<AudioPlayer> _playerJoin = [
AudioPlayer(),
AudioPlayer(),
AudioPlayer(),
];
static final List<AudioPlayer> _playerSlide = [
AudioPlayer(),
AudioPlayer(),
AudioPlayer(),
];
int _playerJoinPtr = 0;
int _playerSlidePtr = 0;
double _soundVolume = 0.0;
//constructor
AudioPlay() {
constructor();
}
void constructor() async {
for (int i = 0; i < _playerJoin.length; i++) {
await _playerJoin[i].setVolume(0);
await _playerJoin[i].setAsset(ConstValue.audioJoin);
}
for (int i = 0; i < _playerSlide.length; i++) {
await _playerSlide[i].setVolume(0);
await _playerSlide[i].setAsset(ConstValue.audioSlide);
}
playZero();
}
void dispose() {
for (int i = 0; i < _playerJoin.length; i++) {
_playerJoin[i].dispose();
}
for (int i = 0; i < _playerSlide.length; i++) {
_playerSlide[i].dispose();
}
}
//getter
double get soundVolume {
return _soundVolume;
}
//setter
set soundVolume(double vol) {
_soundVolume = vol;
}
//最初に音が鳴らないのを回避する方法
void playZero() async {
AudioPlayer ap = AudioPlayer();
await ap.setAsset(ConstValue.audioZero);
await ap.load();
await ap.play();
}
//
void playJoin() async {
if (_soundVolume == 0) {
return;
}
_playerJoinPtr += 1;
if (_playerJoinPtr >= _playerJoin.length) {
_playerJoinPtr = 0;
}
await _playerJoin[_playerJoinPtr].setVolume(_soundVolume * 0.7);
await _playerJoin[_playerJoinPtr].pause();
await _playerJoin[_playerJoinPtr].seek(Duration.zero);
await _playerJoin[_playerJoinPtr].play();
}
void playSlide() async {
if (_soundVolume == 0) {
return;
}
_playerSlidePtr += 1;
if (_playerSlidePtr >= _playerSlide.length) {
_playerSlidePtr = 0;
}
await _playerSlide[_playerSlidePtr].setVolume(_soundVolume);
await _playerSlide[_playerSlidePtr].pause();
await _playerSlide[_playerSlidePtr].seek(Duration.zero);
await _playerSlide[_playerSlidePtr].play();
}
}
lib/const_value.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;
import 'package:flutter/material.dart';
class ConstValue {
//pref
static const String prefMagnificationRate = 'magnificationRate';
static const String prefSoundVolume = 'soundVolume';
static const String prefConnectedFlash = 'connectedFlash';
static const String prefFineTuningGearEngagement = 'fineTuningGearEngagement';
static const String prefBackgroundImageNumber = 'backgroundImageNumber';
static const String prefThemeNumber = 'themeNumber';
static const String prefQuestProgress = 'questProgress';
//image
static const String imageBack1 = 'assets/image/back1.svg';
static const String imageSpace1 = 'assets/image/space1.webp';
static const List<String> imageBackGrounds = [
'assets/image/bg1.webp', //0 dummy
'assets/image/bg1.webp', //1
'assets/image/bg2.webp', //2
'assets/image/bg3.webp',
'assets/image/bg4.webp',
'assets/image/bg5.webp',
'assets/image/bg6.webp',
'assets/image/bg7.webp',
'assets/image/bg8.webp',
'assets/image/bg9.webp',
'assets/image/bg10.webp',
];
//color
static const Color colorButtonBack = Color.fromARGB(60, 0,0,0);
static const Color colorButtonFore = Color.fromARGB(255, 255, 255, 255);
static const Color colorBack = Color.fromARGB(255, 42,1,173);
static const Color colorSettingAccent = Color.fromARGB(255, 76,43,164);
static const Color colorBackground = Color.fromRGBO(50,50,50, 1);
static const Color colorHeaderNormal = Color.fromRGBO(0,0,0, 0.8);
static const Color colorHeaderFlash = Color.fromRGBO(0,0,0, 0.1);
static const Color colorHeaderClear = Color.fromRGBO(0,0,150, 0.8);
//sound
static const String audioZero = 'assets/sound/zero.wav'; //効果音1個
static const String audioSlide = 'assets/sound/slide.mp3';
static const String audioJoin = 'assets/sound/set.wav';
}
lib/current_state.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-22
///
library;
enum CurrentState {
normal,
flash,
clear,
}
lib/empty.dart
lib/game.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;
class Game {
//main.dartとstage.dartとのデータ受け渡しで使用
//現在のクエスト番号
int currentQuestNumber = 0;
//constructor
Game();
}
lib/gear_one.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-22
///
library;
import 'package:flutter/cupertino.dart';
class GearOne {
String name; //ギア名
String src; //画像名
double width; //幅
Widget image; //生成したWidget imageを保持。使いまわすため
int teeth1; //歯の数外側
int teeth2; //歯の数内側
Widget widget; //生成したWidgetを保持。
double degrees; //回転 0..180..360
double ratio; //回転率。停止時は0
double left; //配置位置
double top; //配置位置
int stack; //重ね順(z-indexの役目)
//constructor
GearOne(this.name, this.src, this.width, this.image, this.teeth1, this.teeth2, this.widget, this.degrees, this.ratio, this.left, this.top, this.stack);
GearOne clone() {
return GearOne(name, src, width, image, teeth1, teeth2, widget, degrees, ratio, left, top, stack);
}
int compareStack(GearOne other) {
return stack.compareTo(other.stack);
}
}
lib/gears.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-22
///
library;
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:gearcombination/const_value.dart';
import 'package:gearcombination/preferences.dart';
import 'package:gearcombination/gear_one.dart';
import 'package:gearcombination/audio_play.dart';
import 'package:gearcombination/current_state.dart';
class Gears {
//各ギア
final List<GearOne> _gears = [
GearOne('16', 'assets/image/g16.svg', 145, Container(), 16, 0, Container(), 0, 0, 0,0, 24),
GearOne('18', 'assets/image/g18.svg', 161, Container(), 18, 0, Container(), 0, 0, 0,0, 23),
GearOne('20', 'assets/image/g20.svg', 177, Container(), 20, 0, Container(), 0, 0, 0,0, 22),
GearOne('22', 'assets/image/g22.svg', 193, Container(), 22, 0, Container(), 0, 0, 0,0, 21),
GearOne('24', 'assets/image/g24.svg', 209, Container(), 24, 0, Container(), 0, 0, 0,0, 20),
GearOne('26', 'assets/image/g26.svg', 225, Container(), 26, 0, Container(), 0, 0, 0,0, 19),
GearOne('28', 'assets/image/g28.svg', 241, Container(), 28, 0, Container(), 0, 0, 0,0, 18),
GearOne('30', 'assets/image/g30.svg', 257, Container(), 30, 0, Container(), 0, 0, 0,0, 17),
GearOne('32', 'assets/image/g32.svg', 273, Container(), 32, 0, Container(), 0, 0, 0,0, 16),
GearOne('32b','assets/image/g32_16.svg', 273, Container(), 32, 16, Container(), 0, 0, 0,0, 15),
GearOne('34', 'assets/image/g34.svg', 289, Container(), 34, 0, Container(), 0, 0, 0,0, 14),
GearOne('36', 'assets/image/g36.svg', 305, Container(), 36, 0, Container(), 0, 0, 0,0, 13),
GearOne('36b','assets/image/g36_18.svg', 305, Container(), 36, 18, Container(), 0, 0, 0,0, 12),
GearOne('38', 'assets/image/g38.svg', 321, Container(), 38, 0, Container(), 0, 0, 0,0, 11),
GearOne('40', 'assets/image/g40.svg', 337, Container(), 40, 0, Container(), 0, 0, 0,0, 10),
GearOne('40b','assets/image/g40_20.svg', 337, Container(), 40, 20, Container(), 0, 0, 0,0, 9),
GearOne('42', 'assets/image/g42.svg', 352, Container(), 42, 0, Container(), 0, 0, 0,0, 8),
GearOne('42b','assets/image/g42_21.svg', 352, Container(), 42, 21, Container(), 0, 0, 0,0, 7),
GearOne('44', 'assets/image/g44.svg', 368, Container(), 44, 0, Container(), 0, 0, 0,0, 6),
GearOne('44b','assets/image/g44_22.svg', 368, Container(), 44, 22, Container(), 0, 0, 0,0, 5),
GearOne('46', 'assets/image/g46.svg', 385, Container(), 46, 0, Container(), 0, 0, 0,0, 4),
GearOne('46b','assets/image/g46_23.svg', 385, Container(), 46, 23, Container(), 0, 0, 0,0, 3),
GearOne('48', 'assets/image/g48.svg', 401, Container(), 48, 0, Container(), 0, 0, 0,0, 2),
GearOne('48b','assets/image/g48_16.svg', 401, Container(), 48, 16, Container(), 0, 0, 0,0, 1),
GearOne('50', 'assets/image/g50.svg', 417, Container(), 50, 0, Container(), 0, 0, 0,0, 0),
];
//クエストで左上に配置されるギア
final Map<int,int> _questionGearSelect = {
0:0,
1:0,
2:0,
3:0,
4:6,
5:6,
6:6,
7:6,
8:10,
9:10,
10:10,
11:10,
12:10,
13:10,
14:16,
15:22,
16:22,
17:22,
18:22,
19:22,
20:22,
21:22,
22:13,
23:13,
24:13,
25:13,
26:13,
27:13,
28:13,
29:13,
30:13,
31:13,
32:13,
33:13,
34:13,
35:13,
36:13,
37:13,
38:13,
39:13,
40:13,
41:0,
42:0,
43:0,
44:0,
45:0,
46:0,
47:0,
48:0,
49:0,
50:0,
51:0,
52:0,
};
//回答
final Map<int,double> _answers = {
5:-1,
6:1,
7:2,
8:0.5,
9:-0.25,
10:0.25,
11:-5.1,
12:17,
13:-17,
14:1.5,
15:0.4,
16:-0.4,
17:36,
18:-36,
19:-3,
20:7.2,
21:-6,
22:-0.151,
23:-0.14,
24:-28.5,
25:19,
26:57,
27:15.2,
28:114,
29:-19,
30:11.4,
31:22.8,
32:14.25,
33:9.5,
34:-15.2,
35:6,
36:-3,
37:-5.7,
38:28.5,
39:3,
40:456,
41:-0.4,
42:-0.32,
43:-0.5,
44:0.4,
45:0.32,
46:0.5,
47:-0.8,
48:0.8,
49:1.6,
50:-1.6,
51:-3.2,
52:3.2,
};
final double _adjustTeethRatioBase1 = 2.87; //歯の数から噛み合う位置を求める
final double _adjustTeethRatioBase2 = 3.44; //歯の数から噛み合う位置を求める
double _adjustTeethRatio = 4.45; //歯の数から噛み合う位置を求める
double _devicePixelRatio = 1.0; //MediaQuery.of(context).devicePixelRatio
double _stageRatio = 1.0; //横幅を900とした場合の比率
double _magnificationRate = 1.0; //stage全体の拡大率
late AudioPlay _audioPlay;
late GearOne _rootGear; //左上に配置されるギア
GearOne? _lastGear = null; //ギア接続の最後
int _lastRotationGearCount = 0; //直前のギア接続数
bool _connectedFlash = false; //ギア接続でフラッシュする場合はtrue
double _fineTuningGearEngagement = 0.0; //ギア噛み合わせ微調整
CurrentState _headerColorMode = CurrentState.normal;
int _currentQuestNumber = 0; //クエスト番号
bool ready = false; //全ての状態が整ったらtrue
Future<void> initial(int currentQuestNumber) async {
_currentQuestNumber = currentQuestNumber;
await Preferences.initial();
_audioPlay = AudioPlay(); //音再生用
_audioPlay.soundVolume = 0.0;
_audioPlay.playZero(); //音が鳴らないから鳴らしておく
_connectedFlash = Preferences.connectedFlash;
_fineTuningGearEngagement = Preferences.fineTuningGearEngagement;
//ルートギア用意
_rootGear = _gears[_questionGearSelect[_currentQuestNumber] ?? 0].clone();
_rootGear.name = 'root';
_rootGear.ratio = 1;
_rootGear.stack = 0;
_rootGear.image = _rootGearImage();
_rootGear.widget = _rootGearWidget();
//各ギア用意
for (final GearOne gear in _gears) {
gear.image = _gearImage(gear);
gear.widget = _gearWidget(gear);
}
//準備完了
ready = true;
}
void setSoundVolumeZero() {
_audioPlay.soundVolume = 0;
}
void readSoundVolume() {
_audioPlay.soundVolume = Preferences.soundVolume;
}
Widget getRootWidget() {
return _rootGear.widget;
}
double getRootWidth() {
return _rootGear.width;
}
Widget getWidget(int index) {
return _gears[index].widget;
}
double getWidth(int index) {
return _gears[index].width;
}
int getGearLength() {
return _gears.length;
}
List<GearOne> getGears() {
return _gears;
}
//MediaQuery.of(context).devicePixelRatio
set devicePixelRatio(double num) {
_devicePixelRatio = num;
//スマートフォンとタブレットでギアのかみ合わせ位置がずれるため、その調整
_adjustTeethRatio = _devicePixelRatio * _adjustTeethRatioBase1 - _adjustTeethRatioBase2 + _fineTuningGearEngagement;
}
//横幅を900とした場合の比率
set stageRatio(double num) {
_stageRatio = num;
_updateGears();
}
//stage全体の拡大率
set magnificationRate(double num) {
_magnificationRate = num;
_updateGears();
}
//ギア再描画
void _updateGears() {
_rootGear.image = _rootGearImage();
_rootGear.widget = _rootGearWidget();
for (final GearOne gear in _gears) {
gear.image = _gearImage(gear);
gear.widget = _gearWidget(gear);
}
}
void rootGearPosition(double left, double top) {
_rootGear.left = left;
_rootGear.top = top;
_rootGear.widget = _rootGearWidget();
}
void gearPosition(int index, double left, double top) {
_gears[index].left = left;
_gears[index].top = top;
_gears[index].widget = _gearWidget(_gears[index]);
}
//stage.dartから1秒間に30回呼ばれる
void tick() {
if (ready == false) {
return;
}
//root
_rootGear.degrees += _rootGear.ratio;
_rootGear.degrees %= 360;
_rootGear.widget = _rootGearWidget();
//gears
for (final GearOne gear in _gears) {
if (gear.ratio != 0) {
gear.degrees += gear.ratio;
gear.degrees %= 360;
gear.widget = _gearWidget(gear);
}
}
}
//ルートギア画像
Widget _rootGearImage() {
return SvgPicture.asset(_rootGear.src,
width: _rootGear.width * _stageRatio * _magnificationRate,
height: _rootGear.width * _stageRatio * _magnificationRate,
);
}
//各ギア画像
Widget _gearImage(GearOne gearOne) {
return SvgPicture.asset(gearOne.src,
width: gearOne.width * _stageRatio * _magnificationRate,
height: gearOne.width * _stageRatio * _magnificationRate,
);
}
//ルートギアWidget
Widget _rootGearWidget() {
return Positioned(
left: _rootGear.left * _stageRatio,
top: _rootGear.top * _stageRatio,
child: Transform.rotate(
angle: _rootGear.degrees * (3.141592653589793 / 180),
child: _rootGear.image,
)
);
}
//各ギアWidget
Widget _gearWidget(GearOne gearOne) {
return Positioned(
left: gearOne.left * _stageRatio,
top: gearOne.top * _stageRatio,
child: GestureDetector(
onPanUpdate: (panUpdateDetails) {
Offset position = panUpdateDetails.delta;
gearOne.left += position.dx / _stageRatio;
gearOne.top += position.dy / _stageRatio;
_updatePosition(gearOne.name);
},
onPanEnd: (panUpdateDetails) {
_checkGearTouch(gearOne.name);
},
child: Transform.rotate(
angle: gearOne.degrees * (3.141592653589793 / 180),
child: gearOne.image,
)
)
);
}
//ギアのドラッグ更新
void _updatePosition(String name) {
final int index = _gears.indexWhere((gear) => gear.name == name);
_gears[index].widget = _gearWidget(_gears[index]);
_checkGearTouch('');
_findTip();
}
//ギアの接触を調べて回転速度をセット
void _checkGearTouch(String gearName) {
//全て回転を止める
for (final GearOne gearOne in _gears) {
gearOne.ratio = 0;
}
//ルートギアと接触しているか調査
for (final GearOne gear1 in _gears) {
final int touch = _checkGearTouch2(_rootGear,gear1);
if (touch == 1) {
gear1.ratio = _rootGear.ratio * _rootGear.teeth1 / gear1.teeth1 * -1;
if (gear1.name == gearName) {
_adsorption(_rootGear, gear1);
}
} else if (touch == 3) {
gear1.ratio = _rootGear.ratio * _rootGear.teeth1 / gear1.teeth2 * -1;
if (gear1.name == gearName) {
_adsorption(_rootGear, gear1);
}
}
}
//その他ギアと接触しているか調査
for (int i = 0; i < _gears.length; i++) {
bool joinFlag = false;
for (final GearOne gear1 in _gears) {
for (final GearOne gear2 in _gears) {
if (gear1.name != gear2.name) {
final int touch = _checkGearTouch2(gear1,gear2);
if (gear1.ratio != 0 && gear2.ratio == 0) {
if (touch == 1) {
gear2.ratio = gear1.ratio * gear1.teeth1 / gear2.teeth1 * -1;
if (gear2.name == gearName) {
_adsorption(gear1, gear2);
}
joinFlag = true;
} else if (touch == 2) {
gear2.ratio = gear1.ratio * gear1.teeth2 / gear2.teeth1 * -1;
if (gear2.name == gearName && gear2.stack < gear1.stack) {
final int tmp = gear2.stack;
gear2.stack = gear1.stack;
gear1.stack = tmp;
_adsorption(gear1, gear2);
}
joinFlag = true;
} else if (touch == 3) {
gear2.ratio = gear1.ratio * gear1.teeth1 / gear2.teeth2 * -1;
if (gear2.name == gearName && gear1.stack < gear2.stack) {
final int tmp = gear2.stack;
gear2.stack = gear1.stack;
gear1.stack = tmp;
_adsorption(gear1, gear2);
}
joinFlag = true;
}
}
}
}
}
if (joinFlag == false) {
break;
}
}
}
//ギアとギアの接触を調べる
int _checkGearTouch2(GearOne gear1, GearOne gear2) {
double rad1 = gear1.teeth1 * _adjustTeethRatio * _stageRatio * _magnificationRate;
double center1x = gear1.left / 2 + rad1;
double center1y = gear1.top / 2 + rad1;
double rad2 = gear2.teeth1 * _adjustTeethRatio * _stageRatio * _magnificationRate;
double center2x = gear2.left / 2 + rad2;
double center2y = gear2.top / 2 + rad2;
double distance = sqrt(pow(center1x - center2x,2) + pow(center1y - center2y,2)); //ギア同士の距離
double judge = rad1 + rad2; //ギア2個の半径計
if (distance < judge + 3 && distance > judge - 3) {
return 1; //接触
}
//
if (gear1.teeth2 > 0) {
double rad1b = gear1.teeth2 * _adjustTeethRatio * _stageRatio * _magnificationRate;
double judge = rad1b + rad2; //ギア2個の半径計
if (distance < judge + 3 && distance > judge - 3) {
return 2; //ギア1の内側に接触
}
}
if (gear2.teeth2 > 0) {
double rad2b = gear2.teeth2 * _adjustTeethRatio * _stageRatio * _magnificationRate;
double judge = rad1 + rad2b; //ギア2個の半径計
if (distance < judge + 3 && distance > judge - 3) {
return 3; //ギア2の内側に接触
}
}
return 0; //非接触
}
//ギアの吸着。位置をずらしていき、一番距離が短いものを採用
void _adsorption(GearOne gear1, GearOne gear2) {
double rad1 = gear1.teeth1 * _adjustTeethRatio * _stageRatio * _magnificationRate;
double rad2 = gear2.teeth1 * _adjustTeethRatio * _stageRatio * _magnificationRate;
double judge = rad1 + rad2; //ギア2個の半径計
double center1x = gear1.left / 2 + rad1;
double center1y = gear1.top / 2 + rad1;
int answerX = 0; //最小の移動距離が記録される
int answerY = 0; //最小の移動距離が記録される
double minimum = 999;
for (int y in [-5,-4,-3,-2,-1,0,1,2,3,4,5]) {
for (int x in [-5,-4,-3,-2,-1,0,1,2,3,4,5]) {
double center2x = (gear2.left + x) / 2 + rad2;
double center2y = (gear2.top + y) / 2 + rad2;
double distance = sqrt(pow(center1x - center2x, 2) + pow(center1y - center2y, 2));
if (distance - judge < minimum) {
if ((distance - judge).abs() <= 3) {
minimum = distance - judge;
answerX = x;
answerY = y;
}
}
}
}
gear2.left += answerX;
gear2.top += answerY;
}
//末端を調べる
void _findTip() {
List<GearOne> connection = []; //末端までの接続
//ルートギアと接触しているギアを求める
GearOne? gear1;
for (GearOne gear in _gears) {
final int touch = _checkGearTouch2(_rootGear,gear);
if (touch != 0) {
gear1 = gear;
break;
}
}
_lastGear = gear1;
if (_lastGear != null) {
connection.add(gear1!);
List<GearOne> used = [];
used.add(gear1);
for (int j = 0; j < _gears.length; j++) {
bool findFlag = false;
for (GearOne gear1 in _gears) {
if (used.contains(gear1) == false) {
final int touch = _checkGearTouch2(_lastGear!,gear1);
if (touch != 0) {
_lastGear = gear1;
used.add(gear1);
connection.add(gear1);
findFlag = true;
break;
}
}
}
if (findFlag == false) {
break;
}
}
}
_rotateGearCount();
if (_headerColorMode != CurrentState.clear) {
final bool ret = _answerCheck(connection);
if (ret) {
_headerColorMode = CurrentState.clear; //success
Preferences.addQuestProgressSuccess(_currentQuestNumber);
}
}
}
//回転しているギアの数で音を出す
void _rotateGearCount() {
int count = 0;
for (final GearOne gear1 in _gears) {
if (gear1.ratio != 0) {
count += 1;
}
}
if (_lastRotationGearCount < count) {
_lastRotationGearCount = count;
_audioPlay.playJoin();
if (_connectedFlash) {
if (_headerColorMode != CurrentState.clear) {
(() async {
_headerColorMode = CurrentState.flash;
await Future.delayed(const Duration(milliseconds: 200));
if (_headerColorMode != CurrentState.clear) {
_headerColorMode = CurrentState.normal;
}
})();
}
}
} else if (_lastRotationGearCount > count) {
_lastRotationGearCount = count;
_audioPlay.playSlide();
}
}
bool _answerCheck(List<GearOne> connection) {
const double allowable = 0.002; //計算誤差に対する許容範囲プラス方向とマイナス方向
//各クエスト番号で正解していたらtrueを返す。
if (_currentQuestNumber == 0) {
return true;
} else if (_currentQuestNumber == 1) {
if (connection.length == 1) {
return true;
}
} else if (_currentQuestNumber == 2) {
if (connection.length == 2) {
return true;
}
} else if (_currentQuestNumber == 3) {
if (connection.length == 3) {
return true;
}
} else if (_currentQuestNumber == 4) {
if (connection.isNotEmpty) {
if (connection.last.teeth1 == 28) {
return true;
}
}
} else if (_currentQuestNumber >= 5) {
if (connection.isNotEmpty) {
final double r = connection.last.ratio;
final double reference = _answers[_currentQuestNumber] ?? 0.0;
if (r >= (reference - allowable) && r <= (reference + allowable)) {
return true;
}
}
}
return false;
}
//ルートギア下に配置される文字
Widget rootGearText() {
return Positioned(
left: 10 * _stageRatio,
top: _rootGear.width / 2 * _stageRatio * _magnificationRate,
child: const Text('10 rpm',
style: TextStyle(
color: Colors.white,
fontSize: 15.0,
)
)
);
}
//最終接続ギアの下に配置される文字
Widget lastGearText() {
if (_lastGear == null) {
return Container();
}
return Positioned(
left: (_lastGear!.left + (_lastGear!.width / 4 * _magnificationRate)) * _stageRatio,
top: (_lastGear!.top + _lastGear!.width * _magnificationRate) * _stageRatio,
child: Text('${(_lastGear!.ratio * 1000).roundToDouble() / 100} rpm',
style: const TextStyle(
color: Colors.red,
fontSize: 15.0,
)
)
);
}
//ヘッダカラー
Color headerColor() {
if (_headerColorMode == CurrentState.flash) { //フラッシュ時
return ConstValue.colorHeaderFlash;
} else if (_headerColorMode == CurrentState.clear) { //クエストクリア時
return ConstValue.colorHeaderClear;
} else { //通常時
return ConstValue.colorHeaderNormal;
}
}
CurrentState get headerColorMode => _headerColorMode;
}
lib/language_support.dart
///
/// Consolidated language utilities.
///
library;
/// Usage:
/// await LanguageState.ensureInitialized();
/// final currentCode = await LanguageState.getLanguageCode();
/// await LanguageState.setLanguageCode('en');
/// final locale = parseLocaleTag(currentCode);
///
/// Use LanguageCatalog when presenting selectable language names.
/// Configure LanguageState.configure if you need custom storage.
///
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LanguageCatalog {
const LanguageCatalog._();
static const String preferenceKey = 'languageCode';
static const Map<String, String> names = {
'en': 'English',
'bg': 'Български',
'cs': 'Čeština',
'da': 'Dansk',
'de': 'Deutsch',
'el': 'Ελληνικά',
'es': 'Español',
'et': 'Eesti',
'fi': 'Suomi',
'fr': 'Français',
'hu': 'Magyar',
'it': 'Italiano',
'ja': '日本語',
'lt': 'Lietuvių',
'lv': 'Latviešu',
'nl': 'Nederlands',
'pl': 'Polski',
'pt': 'Português',
'ro': 'Română',
'ru': 'Русский',
'sk': 'Slovenčina',
'sv': 'Svenska',
'th': 'ไทย',
'zh': '中文',
};
static List<String> get supportedCodes => names.keys.toList(growable: false);
static List<Locale> buildSupportedLocales() {
return supportedCodes.map((code) => Locale(code)).toList(growable: false);
}
static String labelFor(String? code) {
if (code == null || code.isEmpty) {
return 'Default';
}
return names[code] ?? code;
}
}
abstract class LanguageStorage {
const LanguageStorage();
Future<void> saveLanguageCode(String code);
Future<String> loadLanguageCode();
}
class SharedPreferencesLanguageStorage extends LanguageStorage {
const SharedPreferencesLanguageStorage({
this.preferenceKey = LanguageCatalog.preferenceKey,
});
final String preferenceKey;
Future<SharedPreferences> _ensurePrefs() async {
return SharedPreferences.getInstance();
}
@override
Future<void> saveLanguageCode(String code) async {
final prefs = await _ensurePrefs();
await prefs.setString(preferenceKey, code);
}
@override
Future<String> loadLanguageCode() async {
final prefs = await _ensurePrefs();
return prefs.getString(preferenceKey) ?? '';
}
}
class LanguageState {
LanguageState._();
static LanguageStorage _storage = const SharedPreferencesLanguageStorage();
static String _languageCode = '';
static bool _initialized = false;
static Completer<void>? _initializing;
static String get currentCode => _languageCode;
static void configure({LanguageStorage? storage, String? initialCode}) {
if (storage != null) {
_storage = storage;
}
if (initialCode != null) {
_languageCode = initialCode;
_initialized = true;
}
}
static Future<void> ensureInitialized() async {
if (_initialized) {
return;
}
final completer = _initializing;
if (completer != null) {
await completer.future;
return;
}
final newCompleter = Completer<void>();
_initializing = newCompleter;
try {
_languageCode = await _storage.loadLanguageCode();
_initialized = true;
newCompleter.complete();
} catch (error, stackTrace) {
if (!newCompleter.isCompleted) {
newCompleter.completeError(error, stackTrace);
}
rethrow;
} finally {
_initializing = null;
}
}
static Future<void> setLanguageCode(String? code) async {
final value = code?.trim() ?? '';
_languageCode = value;
_initialized = true;
await _storage.saveLanguageCode(value);
}
static Future<String> getLanguageCode() async {
await ensureInitialized();
return _languageCode;
}
}
Locale? parseLocaleTag(String tag) {
if (tag.isEmpty) {
return null;
}
final parts = tag.split('-');
final language = parts.isNotEmpty ? parts[0] : tag;
String? script;
String? country;
if (parts.length >= 2) {
final p1 = parts[1];
if (p1.length == 4) {
script = p1;
} else {
country = p1;
}
}
if (parts.length >= 3) {
final p2 = parts[2];
if (p2.length == 4) {
script = p2;
} else {
country = p2;
}
}
return Locale.fromSubtags(
languageCode: language,
scriptCode: script,
countryCode: country,
);
}
lib/loading_screen.dart
import 'package:flutter/material.dart';
class LoadingScreen extends StatelessWidget {
const LoadingScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.indigo,
body: const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.indigoAccent),
backgroundColor: Colors.white,
),
),
);
}
}
lib/main.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-02
///
library;
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'l10n/app_localizations.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart' if (dart.library.html) 'empty.dart'; //webの時
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:gearcombination/const_value.dart';
import 'package:gearcombination/language_support.dart';
import 'package:gearcombination/version_state.dart';
import 'package:gearcombination/quest_progress.dart';
import 'package:gearcombination/game.dart';
import 'package:gearcombination/setting.dart';
import 'package:gearcombination/stage.dart';
import 'package:gearcombination/ad_manager.dart';
import 'package:gearcombination/ad_banner_widget.dart';
import 'package:gearcombination/preferences.dart';
import 'package:gearcombination/loading_screen.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
if (!kIsWeb) {
MobileAds.instance.initialize();
}
await Preferences.initial();
await LanguageState.ensureInitialized();
runApp(const MainApp());
}
ThemeMode _themeModeFromNumber(int value) {
switch (value) {
case 1:
return ThemeMode.light;
case 2:
return ThemeMode.dark;
default:
return ThemeMode.system;
}
}
class MainApp extends StatefulWidget { //statefulに変更して言語変更に対応
const MainApp({super.key});
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
Locale? localeLanguage;
ThemeMode themeMode = ThemeMode.system;
@override
void initState() {
super.initState();
localeLanguage = parseLocaleTag(LanguageState.currentCode);
themeMode = _themeModeFromNumber(Preferences.themeNumber);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: localeLanguage,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: ConstValue.colorBack),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: ConstValue.colorBack,
brightness: Brightness.dark,
),
useMaterial3: true,
),
themeMode: themeMode,
home: const MainHomePage(),
);
}
}
class MainHomePage extends StatefulWidget {
const MainHomePage({super.key});
@override
State<MainHomePage> createState() => _MainHomePageState();
}
class _MainHomePageState extends State<MainHomePage> with SingleTickerProviderStateMixin {
late AdManager _adManager;
final Game _game = Game(); //ゲームのデータを一時的に保持するなどの役目
bool _isReady = false;
//アプリのバージョン取得
void _getVersion() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
setState(() {
VersionState.versionSave(packageInfo.version);
});
}
//言語準備
Future<void> _getCurrentLocale() async {
final String code = await LanguageState.getLanguageCode();
if (!mounted) {
return;
}
final mainState = context.findAncestorStateOfType<_MainAppState>();
if (mainState == null) {
return;
}
mainState
..localeLanguage = parseLocaleTag(code)
..setState(() {});
}
void _applyThemeMode() {
final mainState = context.findAncestorStateOfType<_MainAppState>();
if (mainState == null) {
return;
}
mainState
..themeMode = _themeModeFromNumber(Preferences.themeNumber)
..setState(() {});
}
@override
void initState() {
super.initState();
_initState();
}
void _initState() async {
_getVersion();
_adManager = AdManager();
await Preferences.initial();
if (!mounted) {
return;
}
await _getCurrentLocale();
_applyThemeMode();
setState(() {
_isReady = true;
});
}
//ページ終了時に一度だけ呼ばれる
@override
void dispose() {
_adManager.dispose();
super.dispose();
}
Widget _stageArea() {
return Padding(
padding: const EdgeInsets.only(top: 10, left: 10, right: 10, bottom: 45),
child: Column(children: [
Row(children: [
_questButton(0),
]),
const SizedBox(height: 8),
Row(children: [
_questButton(1),
const SizedBox(width: 8),
_questButton(2),
const SizedBox(width: 8),
_questButton(3),
const SizedBox(width: 8),
_questButton(4),
]),
const SizedBox(height: 8),
Row(children: [
_questButton(5),
const SizedBox(width: 8),
_questButton(6),
const SizedBox(width: 8),
_questButton(7),
const SizedBox(width: 8),
_questButton(8),
]),
const SizedBox(height: 8),
Row(children: [
_questButton(9),
const SizedBox(width: 8),
_questButton(10),
const SizedBox(width: 8),
_questButton(11),
const SizedBox(width: 8),
_questButton(12),
]),
const SizedBox(height: 8),
Row(children: [
_questButton(13),
const SizedBox(width: 8),
_questButton(14),
const SizedBox(width: 8),
_questButton(15),
const SizedBox(width: 8),
_questButton(16),
]),
const SizedBox(height: 8),
Row(children: [
_questButton(17),
const SizedBox(width: 8),
_questButton(18),
const SizedBox(width: 8),
_questButton(19),
const SizedBox(width: 8),
_questButton(20),
]),
const SizedBox(height: 8),
Row(children: [
_questButton(21),
const SizedBox(width: 8),
_questButton(22),
const SizedBox(width: 8),
_questButton(23),
const SizedBox(width: 8),
_questButton(24),
]),
const SizedBox(height: 8),
Row(children: [
_questButton(25),
const SizedBox(width: 8),
_questButton(26),
const SizedBox(width: 8),
_questButton(27),
const SizedBox(width: 8),
_questButton(28),
]),
const SizedBox(height: 8),
Row(children: [
_questButton(29),
const SizedBox(width: 8),
_questButton(30),
const SizedBox(width: 8),
_questButton(31),
const SizedBox(width: 8),
_questButton(32),
]),
const SizedBox(height: 8),
Row(children: [
_questButton(33),
const SizedBox(width: 8),
_questButton(34),
const SizedBox(width: 8),
_questButton(35),
const SizedBox(width: 8),
_questButton(36),
]),
const SizedBox(height: 8),
Row(children: [
_questButton(37),
const SizedBox(width: 8),
_questButton(38),
const SizedBox(width: 8),
_questButton(39),
const SizedBox(width: 8),
_questButton(40),
]),
const SizedBox(height: 8),
Row(children: [
_questButton(41),
const SizedBox(width: 8),
_questButton(42),
const SizedBox(width: 8),
_questButton(43),
const SizedBox(width: 8),
_questButton(44),
]),
const SizedBox(height: 8),
Row(children: [
_questButton(45),
const SizedBox(width: 8),
_questButton(46),
const SizedBox(width: 8),
_questButton(47),
const SizedBox(width: 8),
_questButton(48),
]),
const SizedBox(height: 8),
Row(children: [
_questButton(49),
const SizedBox(width: 8),
_questButton(50),
const SizedBox(width: 8),
_questButton(51),
const SizedBox(width: 8),
_questButton(52),
]),
])
);
}
//クエストの各ボタン
Widget _questButton(int number) {
//保存されているクエスト進行具合を取得
Set<QuestProgress> questProgress = Preferences.questProgress;
//進行具合にnumberが含まれていればそれを取り出す。無ければQuestProgress(0, false)を用意
QuestProgress? questP = questProgress.firstWhere((quest) => quest.questNumber == number, orElse: () => QuestProgress(0, false));
//クリア済みかセット
final bool clearFlag = questP.clearFlag;
//
return Expanded(
child: Center(
child: GestureDetector(
onTap: () async {
_game.currentQuestNumber = number;
await Navigator.of(context).push(
MaterialPageRoute<bool>(
builder: (context) => StagePage(game: _game),
),
);
setState(() {});
},
child: Container(
decoration: const BoxDecoration(
color: Color.fromRGBO(255,255,255, 0.5),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.check,
color: clearFlag ? Colors.redAccent : Colors.grey,
),
const SizedBox(width: 8),
Text('Q$number',
style: const TextStyle(
color: Colors.black,
fontSize: 22.0,
)
)
]
)
)
)
)
);
}
//画面全体
@override
Widget build(BuildContext context) {
if (_isReady == false) {
return LoadingScreen();
}
return Scaffold(
appBar: AppBar(
backgroundColor: ConstValue.colorBack,
//タイトル表示
title: const Text('Gear combination',
style: TextStyle(
color: ConstValue.colorButtonFore,
fontSize: 15.0,
)
),
//設定ボタン
actions: <Widget>[
TextButton(
onPressed: () async {
bool? ret = await Navigator.of(context).push(
MaterialPageRoute<bool>(
builder: (context) => SettingPage(game: _game),
),
);
//awaitで呼び出しているので、settingから戻ったら以下が実行される。
if (ret == true) {
await _getCurrentLocale();
_applyThemeMode();
if (!mounted) {
return;
}
setState(() {});
}
},
child: Text(
AppLocalizations.of(context)!.setting,
style: const TextStyle(
color: ConstValue.colorButtonFore,
)
)
)
]
),
body: SafeArea(
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
),
child: Column(children:[
Expanded(
child: SingleChildScrollView(
child: Stack(children:[
SvgPicture.asset(
ConstValue.imageBack1,
fit: BoxFit.cover,
),
Column(children: <Widget>[
_stageArea(),
])
])
)
),
])
)
),
bottomNavigationBar: AdBannerWidget(adManager: _adManager),
);
}
}
lib/preferences.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;
import 'dart:async';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:gearcombination/const_value.dart';
import 'package:gearcombination/quest_progress.dart';
//デバイスに設定を保存
class Preferences {
static bool ready = false;
static SharedPreferences? _prefs;
static Completer<void>? _initializing;
//値は常に最新に保つ
static double _magnificationRate = 1.0;
static double _soundVolume = 1.0;
static bool _connectedFlash = false;
static double _fineTuningGearEngagement = 0.0;
static int _backgroundImageNumber = 0;
static int _themeNumber = 0;
static Set<QuestProgress> _questProgress = {};
static double get magnificationRate => _magnificationRate;
static double get soundVolume => _soundVolume;
static bool get connectedFlash => _connectedFlash;
static double get fineTuningGearEngagement => _fineTuningGearEngagement;
static int get backgroundImageNumber => _backgroundImageNumber;
static int get themeNumber => _themeNumber;
static Set<QuestProgress> get questProgress => _questProgress;
static Future<SharedPreferences> _ensurePrefs() async {
final cached = _prefs;
if (cached != null) {
return cached;
}
final prefs = await SharedPreferences.getInstance();
_prefs = prefs;
return prefs;
}
static Future<void> initial() async {
if (ready) {
return;
}
final existing = _initializing;
if (existing != null) {
await existing.future;
return;
}
final completer = Completer<void>();
_initializing = completer;
try {
final prefs = await _ensurePrefs();
_magnificationRate = prefs.getDouble(ConstValue.prefMagnificationRate) ?? 1.0;
_soundVolume = prefs.getDouble(ConstValue.prefSoundVolume) ?? 1.0;
_connectedFlash = prefs.getBool(ConstValue.prefConnectedFlash) ?? false;
_fineTuningGearEngagement = prefs.getDouble(ConstValue.prefFineTuningGearEngagement) ?? 0.0;
_backgroundImageNumber = prefs.getInt(ConstValue.prefBackgroundImageNumber) ?? 0;
_themeNumber = (prefs.getInt(ConstValue.prefThemeNumber) ?? 0).clamp(0, 2);
_questProgress = _decodeQuestProgress(
prefs.getString(ConstValue.prefQuestProgress),
);
ready = true;
completer.complete();
} catch (error, stackTrace) {
if (!completer.isCompleted) {
completer.completeError(error, stackTrace);
}
rethrow;
} finally {
_initializing = null;
}
}
//----------------------------
//拡大率
static Future<void> setMagnificationRate(double num) async {
_magnificationRate = num;
final prefs = await _ensurePrefs();
await prefs.setDouble(ConstValue.prefMagnificationRate, num);
}
static Future<double> getMagnificationRate() async {
if (ready) {
return _magnificationRate;
}
final prefs = await _ensurePrefs();
_magnificationRate = prefs.getDouble(ConstValue.prefMagnificationRate) ?? 1.0;
return _magnificationRate;
}
//----------------------------
//効果音音量
static Future<void> setSoundVolume(double num) async {
_soundVolume = num;
final prefs = await _ensurePrefs();
await prefs.setDouble(ConstValue.prefSoundVolume, num);
}
static Future<double> getSoundVolume() async {
if (ready) {
return _soundVolume;
}
final prefs = await _ensurePrefs();
_soundVolume = prefs.getDouble(ConstValue.prefSoundVolume) ?? 1.0;
return _soundVolume;
}
//----------------------------
//ギア接続でフラッシュ
static Future<void> setConnectedFlash(bool flag) async {
_connectedFlash = flag;
final prefs = await _ensurePrefs();
await prefs.setBool(ConstValue.prefConnectedFlash, flag);
}
static Future<bool> getConnectedFlash() async {
if (ready) {
return _connectedFlash;
}
final prefs = await _ensurePrefs();
_connectedFlash = prefs.getBool(ConstValue.prefConnectedFlash) ?? false;
return _connectedFlash;
}
//----------------------------
//ギア微調整噛み合い
static Future<void> setFineTuningGearEngagement(double num) async {
_fineTuningGearEngagement = num;
final prefs = await _ensurePrefs();
await prefs.setDouble(ConstValue.prefFineTuningGearEngagement, num);
}
static Future<double> getFineTuningGearEngagement() async {
if (ready) {
return _fineTuningGearEngagement;
}
final prefs = await _ensurePrefs();
_fineTuningGearEngagement =
prefs.getDouble(ConstValue.prefFineTuningGearEngagement) ?? 0.0;
return _fineTuningGearEngagement;
}
//----------------------------
//背景画像番号
static Future<void> setBackgroundImageNumber(int num) async {
_backgroundImageNumber = num;
final prefs = await _ensurePrefs();
await prefs.setInt(ConstValue.prefBackgroundImageNumber, num);
}
static Future<int> getBackgroundImageNumber() async {
if (ready) {
return _backgroundImageNumber;
}
final prefs = await _ensurePrefs();
_backgroundImageNumber = prefs.getInt(ConstValue.prefBackgroundImageNumber) ?? 0;
return _backgroundImageNumber;
}
//----------------------------
//テーマ
static Future<void> setThemeNumber(int num) async {
final int value = num < 0 ? 0 : (num > 2 ? 2 : num);
_themeNumber = value;
final prefs = await _ensurePrefs();
await prefs.setInt(ConstValue.prefThemeNumber, value);
}
static Future<int> getThemeNumber() async {
if (ready) {
return _themeNumber;
}
final prefs = await _ensurePrefs();
final int num = prefs.getInt(ConstValue.prefThemeNumber) ?? 0;
_themeNumber = num < 0
? 0
: (num > 2
? 2
: num);
return _themeNumber;
}
//----------------------------
//quest progress
static Future<void> setQuestProgress(Set<QuestProgress> questProgress) async {
_questProgress = questProgress;
final prefs = await _ensurePrefs();
final String json = jsonEncode(
questProgress.map((quest) => quest.toJson()).toList(),
);
await prefs.setString(ConstValue.prefQuestProgress, json);
}
static Future<Set<QuestProgress>> getQuestProgress() async {
if (ready) {
return _questProgress;
}
final prefs = await _ensurePrefs();
_questProgress = _decodeQuestProgress(
prefs.getString(ConstValue.prefQuestProgress),
);
return _questProgress;
}
static Future<void> addQuestProgressSuccess(int currentQuestNumber) async {
final Set<QuestProgress> questProgress = {
...await getQuestProgress(),
};
QuestProgress val = QuestProgress(currentQuestNumber, true);
if (!questProgress.contains(val)) {
questProgress.add(val);
await setQuestProgress(questProgress);
}
}
static Set<QuestProgress> _decodeQuestProgress(String? json) {
if (json == null || json.isEmpty) {
return {};
}
final List<dynamic> decodedList = jsonDecode(json) as List<dynamic>;
return decodedList
.map((item) => QuestProgress(
item['questNumber'] as int,
item['clearFlag'] as bool,
))
.toSet();
}
//----------------------------
}
lib/quest_progress.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-25
///
library;
class QuestProgress {
int questNumber;
bool clearFlag;
//constructor
QuestProgress(this.questNumber, this.clearFlag);
Map<String, dynamic> toJson() {
return {
'questNumber': questNumber,
'clearFlag': clearFlag,
};
}
}
lib/quest_question.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;
class QuestQuestion {
int number;
String en;
String ja;
//constructor
QuestQuestion(this.number,this.en,this.ja);
}
lib/setting.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;
import 'package:flutter/material.dart';
import 'l10n/app_localizations.dart';
import 'package:gearcombination/language_support.dart';
import 'package:gearcombination/preferences.dart';
import 'package:gearcombination/version_state.dart';
import 'package:gearcombination/game.dart';
import 'package:gearcombination/ad_manager.dart';
import 'package:gearcombination/ad_banner_widget.dart';
import 'package:gearcombination/loading_screen.dart';
import 'package:gearcombination/theme_color.dart';
class SettingPage extends StatefulWidget {
//メインページでは SettingPage(game: _game) と渡している。
//受け取った game は widget.game でアクセスできる。
final Game game;
const SettingPage({super.key, required this.game});
@override
State<SettingPage> createState() => _SettingPageState();
}
class _SettingPageState extends State<SettingPage> {
String? _languageKey; //言語コード(null はデフォルト)
double _magnificationRate = 1.0;
double _soundVolume = 0.0;
bool _connectedFlash = false;
double _fineTuningGearEngagement = 0.0;
int _backgroundImageNumber = 0;
int _selectedThemeNumber = 0;
late AdManager _adManager;
late ThemeColor _themeColor;
bool _isReady = false;
bool _isFirst = true;
@override
void initState() {
super.initState();
_initState();
}
void _initState() async {
_adManager = AdManager();
final String languageCode = await LanguageState.getLanguageCode();
if (!mounted) {
return;
}
_languageKey = languageCode.isEmpty ? null : languageCode;
await Preferences.initial();
if (!mounted) {
return;
}
_magnificationRate = Preferences.magnificationRate;
_soundVolume = Preferences.soundVolume;
_connectedFlash = Preferences.connectedFlash;
_fineTuningGearEngagement = Preferences.fineTuningGearEngagement;
_backgroundImageNumber = Preferences.backgroundImageNumber;
_selectedThemeNumber = Preferences.themeNumber;
setState(() {
_isReady = true;
});
}
@override
void dispose() {
_adManager.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_isReady == false) {
return LoadingScreen();
}
if (_isFirst) {
_isFirst = false;
_themeColor = ThemeColor(themeNumber: _selectedThemeNumber, context: context);
}
return Scaffold(
backgroundColor: _themeColor.backColor,
appBar: AppBar(
centerTitle: true,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
Navigator.of(context).pop(false);
},
),
title: Text(AppLocalizations.of(context)!.setting),
foregroundColor: _themeColor.appBarForegroundColor,
backgroundColor: Colors.transparent,
actions: [
Padding(
padding: const EdgeInsets.only(right: 10),
child: IconButton(
icon: const Icon(Icons.check),
onPressed: () async {
await LanguageState.setLanguageCode(_languageKey);
await Preferences.setMagnificationRate(_magnificationRate);
await Preferences.setSoundVolume(_soundVolume);
await Preferences.setConnectedFlash(_connectedFlash);
await Preferences.setFineTuningGearEngagement(_fineTuningGearEngagement);
await Preferences.setBackgroundImageNumber(_backgroundImageNumber);
await Preferences.setThemeNumber(_selectedThemeNumber);
if (!mounted) {
return;
}
Navigator.of(context).pop(true);
},
),
)
],
),
body: Column(children:[
Expanded(
child: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(), //背景タップでキーボードを仕舞う
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(top: 8, left: 8, right: 8, bottom: 100),
child: Column(children: [
_buildMagnificationRate(),
_buildSoundVolume(),
_buildFlash(),
_buildFileTune(),
_buildBackgroundImage(),
_buildLanguage(),
_buildTheme(),
_buildUsage(),
_buildVersion(),
]),
),
),
),
),
]),
bottomNavigationBar: AdBannerWidget(adManager: _adManager),
);
}
Widget _buildMagnificationRate() {
final l = AppLocalizations.of(context)!;
return Card(
margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
color: _themeColor.cardColor,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 16, left: 4, right: 4, bottom: 0),
child: Row(children: [
Text(l.magnificationRate),
const Spacer(),
])
),
Padding(
padding: const EdgeInsets.only(top: 0, left: 4, right: 4, bottom: 0),
child: Row(children: <Widget>[
Text('${(_magnificationRate * 10).roundToDouble() / 10}'),
Expanded(
child: Slider(
value: _magnificationRate,
min: 0.3,
max: 3.0,
divisions: 27,
onChanged: (double value) {
setState(() {
_magnificationRate = value;
});
}
)
)
])
),
],
),
),
);
}
Widget _buildSoundVolume() {
final l = AppLocalizations.of(context)!;
return Card(
margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
color: _themeColor.cardColor,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 16, left: 4, right: 4, bottom: 0),
child: Row(children: [
Text(l.soundVolume),
const Spacer(),
])
),
Padding(
padding: const EdgeInsets.only(top: 0, left: 4, right: 4, bottom: 0),
child: Row(children: <Widget>[
Text(_soundVolume.toString()),
Expanded(
child: Slider(
value: _soundVolume,
min: 0.0,
max: 1.0,
divisions: 10,
onChanged: (double value) {
setState(() {
_soundVolume = value;
});
}
)
)
])
),
],
),
),
);
}
Widget _buildFlash() {
final l = AppLocalizations.of(context)!;
return Card(
margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
color: _themeColor.cardColor,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 8, left: 4, right: 4, bottom: 8),
child: Row(children:<Widget>[
Expanded(
child: Text(l.connectedFlash),
),
Switch(
value: _connectedFlash,
onChanged: (bool value) {
setState(() {
_connectedFlash = value;
});
},
),
]),
),
],
),
),
);
}
Widget _buildFileTune() {
final l = AppLocalizations.of(context)!;
return Card(
margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
color: _themeColor.cardColor,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 16, left: 4, right: 4, bottom: 0),
child: Row(children: [
Text(l.fineTuningGearEngagement),
const Spacer(),
])
),
Padding(
padding: const EdgeInsets.only(top: 0, left: 4, right: 4, bottom: 0),
child: Row(children: <Widget>[
Text(_fineTuningGearEngagement.toStringAsFixed(1)),
Expanded(
child: Slider(
value: _fineTuningGearEngagement,
min: -5.0,
max: 5.0,
divisions: 100,
onChanged: (double value) {
setState(() {
_fineTuningGearEngagement = value;
});
}
)
)
])
),
],
),
),
);
}
Widget _buildBackgroundImage() {
final l = AppLocalizations.of(context)!;
return Card(
margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
color: _themeColor.cardColor,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 16, left: 4, right: 4, bottom: 0),
child: Row(children: [
Text(l.backgroundImageNumber),
const Spacer(),
])
),
Padding(
padding: const EdgeInsets.only(top: 0, left: 4, right: 4, bottom: 0),
child: Row(children: <Widget>[
Text(_backgroundImageNumber.toString().padLeft(2, '0')),
Expanded(
child: Slider(
value: _backgroundImageNumber.toDouble(),
min: 0,
max: 10,
divisions: 10,
onChanged: (double value) {
setState(() {
_backgroundImageNumber = value.toInt();
});
}
)
)
])
),
],
),
),
);
}
Widget _buildLanguage() {
final l = AppLocalizations.of(context)!;
return Card(
margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
color: _themeColor.cardColor,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text(l.language,style: Theme.of(context).textTheme.bodyMedium),
contentPadding: const EdgeInsets.symmetric(horizontal: 0),
trailing: DropdownButton<String?>(
value: _languageKey,
dropdownColor: _themeColor.dropdownColor,
items: [
const DropdownMenuItem<String?>(
value: null,
child: Text('Default'),
),
...LanguageCatalog.names.entries.map(
(entry) => DropdownMenuItem<String?>(
value: entry.key,
child: Text(entry.value),
),
),
],
onChanged: (String? value) {
setState(() {
_languageKey = value;
});
},
),
),
],
),
),
);
}
Widget _buildTheme() {
final l = AppLocalizations.of(context)!;
return Card(
margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
color: _themeColor.cardColor,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text(l.theme,style: Theme.of(context).textTheme.bodyMedium),
contentPadding: const EdgeInsets.symmetric(horizontal: 0),
trailing: DropdownButton<int>(
value: _selectedThemeNumber,
dropdownColor: _themeColor.dropdownColor,
items: [
DropdownMenuItem(value: 0, child: Text(l.systemDefault)),
DropdownMenuItem(value: 1, child: Text(l.lightTheme)),
DropdownMenuItem(value: 2, child: Text(l.darkTheme)),
],
onChanged: (int? value) {
if (value == null) {
return;
}
setState(() {
_selectedThemeNumber = value;
});
},
),
),
],
),
),
);
}
Widget _buildUsage() {
final l = AppLocalizations.of(context)!;
return SizedBox(
width: double.infinity,
child: Card(
margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
color: _themeColor.cardColor,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children:[
Text(l.usage1),
const SizedBox(height:15),
Text(l.usage2),
const SizedBox(height:15),
Text(l.usage3),
if (l.usage4 != '') Column(children: [
const SizedBox(height:15),
Text(l.usage4),
])
]
),
),
)
);
}
Widget _buildVersion() {
return SizedBox(
width: double.infinity,
child: Card(
margin: const EdgeInsets.only(left: 4, top: 12, right: 4, bottom: 0),
color: _themeColor.cardColor,
elevation: 0,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Text(
'version ${VersionState.versionLoad()}',
style: const TextStyle(fontSize: 10),
),
),
),
),
);
}
}
lib/stage.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'l10n/app_localizations.dart';
import 'package:gearcombination/const_value.dart';
import 'package:gearcombination/preferences.dart';
import 'package:gearcombination/game.dart';
import 'package:gearcombination/ad_manager.dart';
import 'package:gearcombination/ad_banner_widget.dart';
import 'package:gearcombination/gears.dart';
import 'package:gearcombination/gear_one.dart';
import 'package:gearcombination/current_state.dart';
import 'package:gearcombination/loading_screen.dart';
class StagePage extends StatefulWidget {
//メインページでは SettingPage(game: _game) と渡している。
//受け取った game は widget.game でアクセスできる。
final Game game;
const StagePage({super.key, required this.game});
@override
State<StagePage> createState() => _StagePageState();
}
class _StagePageState extends State<StagePage> with SingleTickerProviderStateMixin {
late AdManager _adManager;
final Gears _gears = Gears();
late Timer _timer;
double _devicePixelRatio = 0;
double _screenWidth = 0;
double _screenHeight = 0;
double _stageWidth = 0;
double _stageHeight = 0;
double _stageRatio = 1;
double _bgImageSize = 0; //背景画像サイズ
double _bgImageAngle = 0; //背景画像回転角度
int _backgroundImageNumber = 0;
final List<String> _questQuestion = [];
String _title = '';
bool _isReady = false;
bool _isFirst = true;
late AnimationController _excellentController;
late Animation<double> _excellentScale;
bool _showExcellent = false;
bool _excellentDismissible = false;
bool _excellentShown = false;
Timer? _excellentDismissTimer;
Timer? _excellentFadeOutTimer;
double _excellentOpacity = 1.0;
@override
void initState() {
super.initState();
_excellentController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
);
_excellentScale = Tween<double>(begin: 0, end: 0.8).animate(
CurvedAnimation(parent: _excellentController, curve: Curves.easeOut),
);
_initState();
}
void _initState() async {
_adManager = AdManager();
await Preferences.initial();
await _gears.initial(widget.game.currentQuestNumber);
_gears.magnificationRate = Preferences.magnificationRate;
_gears.readSoundVolume();
_backgroundImageNumber = Preferences.backgroundImageNumber;
_timer = Timer.periodic(const Duration(milliseconds: (1000 ~/ 30)), (timer) {
final bool triggerClear = !_excellentShown &&
_gears.headerColorMode == CurrentState.clear;
if (triggerClear) {
_excellentShown = true;
_excellentController.forward(from: 0);
_excellentDismissTimer?.cancel();
_excellentDismissTimer = Timer(const Duration(seconds: 2), () {
if (!mounted || !_showExcellent) {
return;
}
setState(() {
_excellentDismissible = true;
_excellentOpacity = 1.0;
});
});
}
setState(() {
_gears.tick();
_bgImageAngle -= 0.002;
if (_bgImageAngle < -314159265) {
_bgImageAngle = 0;
}
if (triggerClear) {
_showExcellent = true;
_excellentDismissible = false;
_excellentOpacity = 1.0;
_excellentFadeOutTimer?.cancel();
}
});
});
setState(() {
_isReady = true;
});
}
@override
void dispose() {
_gears.setSoundVolumeZero();
_adManager.dispose();
_timer.cancel();
_excellentDismissTimer?.cancel();
_excellentFadeOutTimer?.cancel();
_excellentController.dispose();
super.dispose();
}
//別のページから戻ったときに実行される
@override
void didUpdateWidget(StagePage oldWidget) {
super.didUpdateWidget(oldWidget);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
//クエスト出題をすべて取得
_questQuestion.add(AppLocalizations.of(context)!.quest0);
_questQuestion.add(AppLocalizations.of(context)!.quest1);
_questQuestion.add(AppLocalizations.of(context)!.quest2);
_questQuestion.add(AppLocalizations.of(context)!.quest3);
_questQuestion.add(AppLocalizations.of(context)!.quest4);
_questQuestion.add(AppLocalizations.of(context)!.quest5);
_questQuestion.add(AppLocalizations.of(context)!.quest6);
_questQuestion.add(AppLocalizations.of(context)!.quest7);
_questQuestion.add(AppLocalizations.of(context)!.quest8);
_questQuestion.add(AppLocalizations.of(context)!.quest9);
_questQuestion.add(AppLocalizations.of(context)!.quest10);
_questQuestion.add(AppLocalizations.of(context)!.quest11);
_questQuestion.add(AppLocalizations.of(context)!.quest12);
_questQuestion.add(AppLocalizations.of(context)!.quest13);
_questQuestion.add(AppLocalizations.of(context)!.quest14);
_questQuestion.add(AppLocalizations.of(context)!.quest15);
_questQuestion.add(AppLocalizations.of(context)!.quest16);
_questQuestion.add(AppLocalizations.of(context)!.quest17);
_questQuestion.add(AppLocalizations.of(context)!.quest18);
_questQuestion.add(AppLocalizations.of(context)!.quest19);
_questQuestion.add(AppLocalizations.of(context)!.quest20);
_questQuestion.add(AppLocalizations.of(context)!.quest21);
_questQuestion.add(AppLocalizations.of(context)!.quest22);
_questQuestion.add(AppLocalizations.of(context)!.quest23);
_questQuestion.add(AppLocalizations.of(context)!.quest24);
_questQuestion.add(AppLocalizations.of(context)!.quest25);
_questQuestion.add(AppLocalizations.of(context)!.quest26);
_questQuestion.add(AppLocalizations.of(context)!.quest27);
_questQuestion.add(AppLocalizations.of(context)!.quest28);
_questQuestion.add(AppLocalizations.of(context)!.quest29);
_questQuestion.add(AppLocalizations.of(context)!.quest30);
_questQuestion.add(AppLocalizations.of(context)!.quest31);
_questQuestion.add(AppLocalizations.of(context)!.quest32);
_questQuestion.add(AppLocalizations.of(context)!.quest33);
_questQuestion.add(AppLocalizations.of(context)!.quest34);
_questQuestion.add(AppLocalizations.of(context)!.quest35);
_questQuestion.add(AppLocalizations.of(context)!.quest36);
_questQuestion.add(AppLocalizations.of(context)!.quest37);
_questQuestion.add(AppLocalizations.of(context)!.quest38);
_questQuestion.add(AppLocalizations.of(context)!.quest39);
_questQuestion.add(AppLocalizations.of(context)!.quest40);
_questQuestion.add(AppLocalizations.of(context)!.quest41);
_questQuestion.add(AppLocalizations.of(context)!.quest42);
_questQuestion.add(AppLocalizations.of(context)!.quest43);
_questQuestion.add(AppLocalizations.of(context)!.quest44);
_questQuestion.add(AppLocalizations.of(context)!.quest45);
_questQuestion.add(AppLocalizations.of(context)!.quest46);
_questQuestion.add(AppLocalizations.of(context)!.quest47);
_questQuestion.add(AppLocalizations.of(context)!.quest48);
_questQuestion.add(AppLocalizations.of(context)!.quest49);
_questQuestion.add(AppLocalizations.of(context)!.quest50);
_questQuestion.add(AppLocalizations.of(context)!.quest51);
_questQuestion.add(AppLocalizations.of(context)!.quest52);
}
@override
Widget build(BuildContext context) {
if (_isReady == false || Preferences.ready == false || _gears.ready == false) {
return LoadingScreen();
}
if (_isFirst) {
_isFirst = false;
_devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
_gears.devicePixelRatio = _devicePixelRatio;
_screenWidth = MediaQuery.of(context).size.width;
_screenHeight = MediaQuery.of(context).size.height;
_bgImageSize = max(_screenWidth,_screenHeight);
_stageWidth = _screenWidth;
_stageHeight = _screenWidth * (1100 / 900);
_stageRatio = _stageWidth / 900;
_gears.stageRatio = _stageRatio;
_gears.rootGearPosition(0,- _gears.getRootWidth() / 2 * Preferences.magnificationRate);
double h = 0;
for (int i = _gears.getGearLength() - 1; i >= 0; i--) {
_gears.gearPosition(i,-(_gears.getWidth(i) / 2) + (_stageWidth / _stageRatio) - (_gears.getWidth(0) / 7),h);
h += _gears.getWidth(0) / 4; //最初のギアを基準値にしている。深い意味はない
}
try {
_title = _questQuestion[widget.game.currentQuestNumber];
} catch (_) {}
_gears.readSoundVolume();
}
return Container(
decoration: _decoration(),
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
centerTitle: true,
elevation: 0,
//戻るボタン
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop(false); //falseを返す
},
),
title: Text(_title,
style: const TextStyle(
color: Colors.white,
fontSize: 14.0,
)
),
foregroundColor: const Color.fromRGBO(255,255,255,1),
backgroundColor: _gears.headerColor(),//ConstValue.colorSettingAccent,
),
body: SafeArea(
child: Stack(children:[
_background(),
Column(children:[
Expanded(
child: Stack(children:_stageGears())
),
]),
if (_showExcellent) _excellentOverlay(),
])
),
bottomNavigationBar: AdBannerWidget(adManager: _adManager),
)
);
}
Decoration _decoration() {
if (_backgroundImageNumber == 0) {
return const BoxDecoration(
color: Colors.white,
);
} else {
return const BoxDecoration(
image: DecorationImage(
image: AssetImage(ConstValue.imageSpace1),
fit: BoxFit.cover,
),
);
}
}
Widget _background() {
if (_backgroundImageNumber == 0) {
return Container(
color: ConstValue.colorBackground,
);
} else {
return Transform.rotate(
angle: _bgImageAngle,
child: Transform.scale(
scale: 2.6,
child: Image.asset(
ConstValue.imageBackGrounds[_backgroundImageNumber],
width: _bgImageSize,
height: _bgImageSize,
),
),
);
}
}
//ステージ上に配置される要素
List<Widget> _stageGears() {
//Widgetの配列を返す
List<Widget> widgets = [];
//各ギアを取得
List<GearOne> gearAll = _gears.getGears();
//ギアの重なり順に並び変える
gearAll.sort((a, b) => a.compareStack(b));
//各ギア
for (final GearOne gear1 in gearAll) {
widgets.add(gear1.widget);
}
//ルートギア
widgets.add(_gears.getRootWidget());
//ルートギア下のテキスト
widgets.add(_gears.rootGearText());
//最終接続ギア下のテキスト
widgets.add(_gears.lastGearText());
//
return widgets;
}
Widget _excellentOverlay() {
return Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (_excellentDismissible == false) {
return;
}
_excellentDismissible = false;
_excellentFadeOutTimer?.cancel();
_excellentFadeOutTimer = null;
_excellentDismissTimer?.cancel();
_excellentDismissTimer = null;
_runExcellentFadeOut();
},
child: Center(
child: AnimatedBuilder(
animation: _excellentScale,
builder: (context, child) {
final double width = MediaQuery.of(context).size.width * _excellentScale.value;
if (width <= 0) {
return const SizedBox.shrink();
}
return SizedBox(
width: width,
height: width,
child: Opacity(
opacity: _excellentOpacity,
child: child,
),
);
},
child: SvgPicture.asset(
'assets/image/excellent.svg',
fit: BoxFit.contain,
),
),
),
),
);
}
void _runExcellentFadeOut() {
const fadeDuration = Duration(milliseconds: 500);
const tick = Duration(milliseconds: 16);
var elapsed = Duration.zero;
_excellentFadeOutTimer = Timer.periodic(tick, (timer) {
if (!mounted) {
timer.cancel();
_excellentFadeOutTimer = null;
return;
}
elapsed += tick;
final t = (elapsed.inMilliseconds / fadeDuration.inMilliseconds).clamp(0.0, 1.0);
setState(() {
_excellentOpacity = 1.0 - t;
});
if (elapsed >= fadeDuration) {
timer.cancel();
_excellentFadeOutTimer = null;
setState(() {
_showExcellent = false;
_excellentOpacity = 1.0;
});
}
});
}
}
lib/theme_color.dart
import 'package:flutter/material.dart';
class ThemeColor {
final int? themeNumber;
final BuildContext context;
ThemeColor({this.themeNumber, required this.context});
Brightness get _effectiveBrightness {
switch (themeNumber) {
case 1:
return Brightness.light;
case 2:
return Brightness.dark;
default:
return Theme.of(context).brightness;
}
}
Color get backColor =>
_effectiveBrightness == Brightness.light
? Colors.grey[200]!
: Colors.grey[900]!;
Color get cardColor =>
_effectiveBrightness == Brightness.light
? Colors.white
: Colors.grey[800]!;
Color get appBarForegroundColor =>
_effectiveBrightness == Brightness.light
? Colors.grey[700]!
: Colors.white70;
Color get dropdownColor => cardColor;
}
lib/version_state.dart
///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-01-27
///
library;
class VersionState {
static String _version = '';
//バージョンを記録
static void versionSave(String str) {
_version = str;
}
//バージョンを返す
static String versionLoad() {
return _version;
}
}