ソースコード source code

下記アプリの主要なソースコードを公開しています。アプリ開発の参考になれば幸いです。

画像等が別途必要ですので下記情報のみでアプリが完成するものではありません。 アプリは少しずつ機能拡張していますのでストア公開されているアプリと内容が異なる場合があります。 コードはコピーして自由にお使いいただけます。ただし著作権は放棄しておりませんので全部の再掲載はご遠慮ください。部分的に再掲載したり、改変して再掲載するのは構いません。 自身のアプリ作成の参考として個人使用・商用問わず自由にお使いいただけます。 コード記述のお手本を示すものではありません。ミニアプリですので変数名などさほど気遣いしていない部分も有りますし間違いも有るかと思いますので参考程度にお考え下さい。 他の賢者の皆様が公開されているコードを参考にした箇所も含まれます。Flutter開発の熟練者が書いたコードではありません。 エンジニア向け技術情報共有サービスではありませんので説明は省いています。 GitHubなどへの公開は予定しておりません。

下記コードの最終ビルド日: 2023-10-26

pubspec.yaml

name: gearcombination
description: "Gear Combination"
# 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.0.0+8

environment:
  sdk: '>=3.3.0-48.0.dev <4.0.0'

# 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.5
  package_info_plus: ^4.1.0
  shared_preferences: ^2.0.17
  http: ^1.1.0
  flutter_localizations:    #多言語ライブラリの本体    # .arbファイルを更新したら flutter gen-l10n
    sdk: flutter
  intl: ^0.18.1     #多言語やフォーマッタなどの関連ライブラリ
  google_mobile_ads: ^3.1.0
  just_audio: ^0.9.35
  flutter_svg: ^2.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_launcher_icons: ^0.13.1    #flutter pub run flutter_launcher_icons
  flutter_native_splash: ^2.3.2     #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: ^3.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/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # 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/custom-fonts/#from-packages

lib/ad_mob.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-02-06
///
library;

import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:io';

class AdMob {
  late BannerAd _adMobBanner;
  bool _isAdMob = false;
  AdMob() { //constructor
    String adBannerUnitId = '';
    if (!kIsWeb && Platform.isAndroid) {
      //adBannerUnitId = 'ca-app-pub-3940256099942544/6300978111';  //test
      adBannerUnitId = 'ca-app-pub-xxxxxxxxxxxxxxxx/xxxxxxxxxx';
      _isAdMob = true;
    } else if (!kIsWeb && Platform.isIOS) {
      //adBannerUnitId = 'ca-app-pub-3940256099942544/6300978111';  //test
      adBannerUnitId = 'ca-app-pub-xxxxxxxxxxxxxxxx/xxxxxxxxxx';
      _isAdMob = true;
    }
    if (_isAdMob) {
      _adMobBanner = BannerAd(
        adUnitId: adBannerUnitId,
        size: AdSize.banner,
        request: const AdRequest(),
        listener: const BannerAdListener(),
      );
    }
  }
  void load() {
    if (_isAdMob) {
      _adMobBanner.load();
    }
  }
  void dispose() {
    if (_isAdMob) {
      _adMobBanner.dispose();
    }
  }
  Widget getAdBannerWidget() {
    if (_isAdMob) {
      return Container(
        alignment: Alignment.center,
        width: _adMobBanner.size.width.toDouble(),
        height: _adMobBanner.size.height.toDouble(),
        child: AdWidget(ad: _adMobBanner),
      );
    } else {
      return Container();
    }
  }
  double getAdBannerHeight() {
    if (_isAdMob) {
      return _adMobBanner.size.height.toDouble();
    } else {
      return 150;  //for web
    }
  }
}

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 prefLanguageCode = 'languageCode';
  static const String prefMagnificationRate = 'magnificationRate';
  static const String prefSoundVolume = 'soundVolume';
  static const String prefConnectedFlash = 'connectedFlash';
  static const String prefFineTuningGearEngagement = 'fineTuningGearEngagement';
  static const String prefQuestProgress = 'questProgress';
  //image
  static const imageBack1 = 'assets/image/back1.svg';
  //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(0,0,0, 0.8);
  static const Color colorBackgroundFlash = Color.fromRGBO(0,0,0, 0.6);
  static const Color colorBackgroundSuccess = Color.fromRGBO(0,0,150, 1);
  //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';
  //string
  static const Map<String,String> languageCode = {
    'en': 'English',
    'bg': 'български език',
    'cs': 'Čeština',
    'da': 'dansk',
    'de': 'Deutsch',
    'el': 'Ελληνικά',
    'es': 'Español',
    'et': 'eesti keel',
    'fi': 'Suomen kieli',
    'fr': 'Français',
    'hu': 'magyar nyelv',
    'it': 'Italiano',
    'ja': '日本語',
    'lt': 'lietuvių kalba',
    'lv': 'Latviešu',
    'nl': 'Nederlands',
    'pl': 'Polski',
    'pt': 'Português',
    'ro': 'limba română',
    'ru': 'русский',
    'sk': 'Slovenčina',
    'sv': 'svenska',
    'th': 'ภาษาไทย',
    'zh': '中文',
  };

}

lib/empty.dart

lib/game.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;

class Game {

  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;
  int teeth1;  //歯の数外側
  int teeth2;  //歯の数内側
  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';

class Gears {

  ////////
  //非公開
  ////////

}

lib/language_state.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-01-27
///
library;

import 'package:gearcombination/preferences.dart';

class LanguageState {

  static String _languageCode = 'en';

  //言語コードを記録
  static Future<void> setLanguageCode(String str) async {
    _languageCode = str;
    await Preferences.setLanguageCode(_languageCode);
  }
  //言語コードを返す
  static Future<String> getLanguageCode() async {
    _languageCode = await Preferences.getLanguageCode() ?? 'en';
    return _languageCode;
  }

}

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 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart' if (dart.library.html) 'empty.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter/foundation.dart' show kIsWeb;

//自身で作成したclassを読み込む
import 'package:gearcombination/const_value.dart';
import 'package:gearcombination/language_state.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_mob.dart';
import 'package:gearcombination/page_state.dart';
import 'package:gearcombination/preferences.dart';

void main() {
  //MobileAds.instance.initialize(); ここに入れると進行しなかった
  runApp(const MainApp());
}

class MainApp extends StatefulWidget {    //statefulに変更して言語変更に対応
  const MainApp({super.key});
  @override
  State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
  Locale localeLanguage = const Locale('en');
  @override
  Widget build(BuildContext context) {
    if (kIsWeb == false) {
      MobileAds.instance.initialize();
    }
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      localizationsDelegates: AppLocalizations.localizationsDelegates,   //多言語化
      supportedLocales: AppLocalizations.supportedLocales,  //自動で言語リストを生成
      locale: localeLanguage,
      home: const MainHomePage(),
    );
  }
}

class MainHomePage extends StatefulWidget {
  const MainHomePage({super.key});
  @override
  State<MainHomePage> createState() => _MainHomePageState();
}

class _MainHomePageState extends State<MainHomePage> with SingleTickerProviderStateMixin {
  final AdMob _adMob = AdMob(); //広告表示
  final Game _game = Game();  //ゲームのデータを一時的に保持するなどの役目

  //アプリのバージョン取得
  void _getVersion() async {
    PackageInfo packageInfo = await PackageInfo.fromPlatform();
    setState(() {
      VersionState.versionSave(packageInfo.version);
    });
  }
  //言語準備
  void _getCurrentLocale() async {
    Locale locale = Locale(await LanguageState.getLanguageCode());
    if (mounted) {  //Widgetが存在する。Widgetが存在しない時の実行によるエラーを回避する為。
      context.findAncestorStateOfType<_MainAppState>()!
        ..localeLanguage = locale
        ..setState(() {});
    }
  }
  //ページ起動開始時に一度だけ呼ばれる
  @override
  void initState() {
    super.initState();
    _getVersion();
    _getCurrentLocale();
    LanguageState.getLanguageCode();
    _adMob.load();
    (() async {
      await Preferences.initial();
      setState(() {});
    })();
  }
  //ページ終了時に一度だけ呼ばれる
  @override
  void dispose() {
    PageState.setCurrentPage('');
    _adMob.dispose();
    super.dispose();
  }
  //stage area
  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),
        ]),
      ])
    );
  }
  Widget _questButton(int number) {
    Set<QuestProgress> questProgress = Preferences.questProgress;
    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;
            bool? ret = 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) {
    //このページが開いたときに一度だけ実行される処理を記述。initStateではタイミングが合わない為。
    if (PageState.getCurrentPage() != 'main') {
      PageState.setCurrentPage('main');
    }
    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!) { //設定で適用だった場合
                _getCurrentLocale();
                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(),
                  ])
                ])
              )
            ),
            //広告
            Padding(
              padding: const EdgeInsets.only(top: 10, left: 0, right: 0, bottom: 0),
              child: SizedBox(
                width: double.infinity,
                child: _adMob.getAdBannerWidget(),
              )
            )
          ])
        )
      )
    );
  }

}

lib/page_state.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-05
///
library;

//現在のページを記録。initStateでタイミングが合わない時にbuild内で一度だけ実行させるために使用。
class PageState {

  static String _currentPage = '';

  static void setCurrentPage(String str) {
    _currentPage = str;
  }

  static String getCurrentPage() {
    return _currentPage;
  }

}

lib/preferences.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;

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 String _languageCode = '';
  static double _magnificationRate = 1.0;
  static double _soundVolume = 1.0;
  static bool _connectedFlash = false;
  static double _fineTuningGearEngagement = 0.0;
  static Set<QuestProgress> _questProgress = {};

  static String get languageCode {
    return _languageCode;
  }
  static double get magnificationRate {
    return _magnificationRate;
  }
  static double get soundVolume {
    return _soundVolume;
  }
  static bool get connectedFlash {
    return _connectedFlash;
  }
  static double get fineTuningGearEngagement {
    return _fineTuningGearEngagement;
  }
  static Set<QuestProgress> get questProgress {
    return _questProgress;
  }

  static Future<void> initial() async {
    _languageCode = await getLanguageCode();
    _magnificationRate = await getMagnificationRate();
    _soundVolume = await getSoundVolume();
    _connectedFlash = await getConnectedFlash();
    _fineTuningGearEngagement = await getFineTuningGearEngagement();
    _questProgress = await getQuestProgress();
    ready = true;
  }

  //----------------------------

  //言語コード
  static Future<void> setLanguageCode(String str) async {
    _languageCode = str;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setString(ConstValue.prefLanguageCode, str);
  }
  static Future<String> getLanguageCode() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final String str = prefs.getString(ConstValue.prefLanguageCode) ?? 'en';
    return str;
  }

  //----------------------------

  //拡大率
  static Future<void> setMagnificationRate(double num) async {
    _magnificationRate = num;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setDouble(ConstValue.prefMagnificationRate, num);
  }
  static Future<double> getMagnificationRate() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final double num = prefs.getDouble(ConstValue.prefMagnificationRate) ?? 1.0;
    return num;
  }

  //----------------------------

  //効果音音量
  static Future<void> setSoundVolume(double num) async {
    _soundVolume = num;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setDouble(ConstValue.prefSoundVolume, num);
  }
  static Future<double> getSoundVolume() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final double num = prefs.getDouble(ConstValue.prefSoundVolume) ?? 1.0;
    return num;
  }

  //----------------------------

  //ギア接続でフラッシュ
  static Future<void> setConnectedFlash(bool flag) async {
    _connectedFlash = flag;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setBool(ConstValue.prefConnectedFlash, flag);
  }
  static Future<bool> getConnectedFlash() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final bool flag = prefs.getBool(ConstValue.prefConnectedFlash) ?? false;
    return flag;
  }

  //----------------------------

  //ギア噛み合わせ微調整
  static Future<void> setFineTuningGearEngagement(double num) async {
    _fineTuningGearEngagement = num;
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setDouble(ConstValue.prefFineTuningGearEngagement, num);
  }
  static Future<double> getFineTuningGearEngagement() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final double num = prefs.getDouble(ConstValue.prefFineTuningGearEngagement) ?? 0.0;
    return num;
  }

  //----------------------------

  //quest progress
  static Future<void> setQuestProgress(Set<QuestProgress> questProgress) async {
    _questProgress = questProgress;
    List<Map<String, dynamic>> questProgressList = questProgress.map((quest) => quest.toJson()).toList();
    final String json = jsonEncode(questProgressList);
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setString(ConstValue.prefQuestProgress, json);
  }
  static Future<Set<QuestProgress>> getQuestProgress() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final String json = prefs.getString(ConstValue.prefQuestProgress) ?? '[]';
    List<dynamic> decodedList = jsonDecode(json);
    Set<QuestProgress> questProgress = decodedList.map((item) => QuestProgress(item['questNumber'] as int, item['clearFlag'] as bool)).toSet();
    return questProgress;
  }
  static Future<void> addQuestProgressSuccess(int currentQuestNumber) async {
    Set<QuestProgress> questProgress = await getQuestProgress();
    QuestProgress val = QuestProgress(currentQuestNumber, true);
    if (!questProgress.contains(val)) {
      questProgress.add(val);
    }
    await setQuestProgress(questProgress);
  }

//----------------------------

}

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 'package:flutter_gen/gen_l10n/app_localizations.dart';

import 'package:gearcombination/const_value.dart';
import 'package:gearcombination/preferences.dart';
import 'package:gearcombination/language_state.dart';
import 'package:gearcombination/version_state.dart';
import 'package:gearcombination/game.dart';
import 'package:gearcombination/ad_mob.dart';
import 'package:gearcombination/page_state.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> {
  //これら変数はUIへの表示や入力の為に一時的に使用される。
  String _languageKey = ''; //言語コード 'en'
  String _languageValue = '';
  double _magnificationRate = 1.0;
  double _soundVolume = 0.0;
  bool _connectedFlash = false;
  double _fineTuningGearEngagement = 0.0;

  final AdMob _adMob = AdMob(); //広告

  //ページ起動時に一度だけ実行される
  @override
  void initState() {
    super.initState();
    _adMob.load();
  }
  //ページ終了時に一度だけ実行される
  @override
  void dispose() {
    PageState.setCurrentPage('');
    _adMob.dispose();
    super.dispose();
  }
  //ページ描画
  @override
  Widget build(BuildContext context) {
    //このページが開いたときに一度だけ実行される処理を記述。initStateではタイミングが合わない為。
    if (PageState.getCurrentPage() != 'setting') {
      PageState.setCurrentPage('setting');
      (() async {
        _languageKey = await LanguageState.getLanguageCode();
        _languageValue = ConstValue.languageCode[_languageKey] ?? '';
        await Preferences.initial();
        _magnificationRate = Preferences.magnificationRate;
        _soundVolume = Preferences.soundVolume;
        _connectedFlash = Preferences.connectedFlash;
        _fineTuningGearEngagement = Preferences.fineTuningGearEngagement;
        setState((){});
      })();
    }
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        elevation: 0,
        //設定キャンセルボタン
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: () {
            Navigator.of(context).pop(false); //falseを返す
          },
        ),
        title: Text(AppLocalizations.of(context)!.setting),
        foregroundColor: const Color.fromRGBO(255,255,255,1),
        backgroundColor: ConstValue.colorSettingAccent,
        actions: [
          //設定OKボタン
          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);
              if (!mounted) {
                return;
              }
              Navigator.of(context).pop(true);  //trueを返す
            },
          ),
        ],
      ),
      body: Column(children:[
        Expanded(
          child: GestureDetector(
            onTap: () => FocusScope.of(context).unfocus(),  //背景タップでキーボードを仕舞う
            child: SingleChildScrollView(
              child: Padding(
                padding: const EdgeInsets.all(20),
                child: Column(children: [
                  Padding(
                    padding: const EdgeInsets.only(top: 18, left: 16, right: 16, bottom: 0),
                    child: Row(children: [
                      Text(AppLocalizations.of(context)!.magnificationRate,style: const TextStyle(fontSize: 16)),
                      const Spacer(),
                    ])
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 6),
                    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;
                            });
                          }
                        )
                      )
                    ])
                  ),
                  _border(),
                  Padding(
                    padding: const EdgeInsets.only(top: 18, left: 16, right: 16, bottom: 0),
                    child: Row(children: [
                      Text(AppLocalizations.of(context)!.soundVolume,style: const TextStyle(fontSize: 16)),
                      const Spacer(),
                    ])
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 6),
                    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;
                            });
                          }
                        )
                      )
                    ])
                  ),
                  _border(),
                  Padding(
                    padding: const EdgeInsets.only(top: 16, left: 16, right: 16, bottom: 16),
                    child: Row(children:<Widget>[
                      Expanded(
                        child: Text(AppLocalizations.of(context)!.connectedFlash,style: const TextStyle(fontSize: 16)),
                      ),
                      Switch(
                        value: _connectedFlash,
                        onChanged: (bool value) {
                          setState(() {
                            _connectedFlash = value;
                          });
                        },
                      ),
                    ]),
                  ),
                  _border(),
                  Padding(
                      padding: const EdgeInsets.only(top: 18, left: 16, right: 16, bottom: 0),
                      child: Row(children: [
                        Text(AppLocalizations.of(context)!.fineTuningGearEngagement,style: const TextStyle(fontSize: 16)),
                        const Spacer(),
                      ])
                  ),
                  Padding(
                      padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 6),
                      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;
                                  });
                                }
                            )
                        )
                      ])
                  ),
                  _border(),
                  Padding(
                    padding: const EdgeInsets.only(top: 18, left: 0, right: 0, bottom: 0),
                    child: Row(children:[
                      const SizedBox(width:16),
                      Text(AppLocalizations.of(context)!.language,
                        style: const TextStyle(
                          fontSize: 16,
                        )
                      ),
                      const Spacer(),
                    ])
                  ),
                  Padding(
                    padding: const EdgeInsets.only(top: 12, left: 0, right: 0, bottom: 18),
                    child: Table(
                      children: <TableRow>[
                        TableRow(children: <Widget>[
                          _languageTableCell(0),
                          _languageTableCell(1),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(2),
                          _languageTableCell(3),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(4),
                          _languageTableCell(5),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(6),
                          _languageTableCell(7),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(8),
                          _languageTableCell(9),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(10),
                          _languageTableCell(11),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(12),
                          _languageTableCell(13),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(14),
                          _languageTableCell(15),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(16),
                          _languageTableCell(17),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(18),
                          _languageTableCell(19),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(20),
                          _languageTableCell(21),
                        ]),
                        TableRow(children: <Widget>[
                          _languageTableCell(22),
                          _languageTableCell(23),
                        ]),
                      ]
                    ),
                  ),
                  _border(),
                  Padding(
                    padding: const EdgeInsets.only(top: 24, left: 0, right: 0, bottom: 24),
                    child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children:[
                          Text(AppLocalizations.of(context)!.usage1),
                          const SizedBox(height:15),
                          Text(AppLocalizations.of(context)!.usage2),
                          const SizedBox(height:15),
                          Text(AppLocalizations.of(context)!.usage3),
                          const SizedBox(height:15),
                          Text(AppLocalizations.of(context)!.usage4),
                        ]
                    ),
                  ),
                  _border(),
                  Padding(
                    padding: const EdgeInsets.only(top: 24, left: 0, right: 0, bottom: 36),
                    child: SizedBox(
                      child: Text('version  ${VersionState.versionLoad()}',
                        style: const TextStyle(
                          fontSize: 10,
                        ),
                      ),
                    ),
                  ),
                ]),
              ),
            ),
          ),
        ),
        Padding(
          padding: const EdgeInsets.only(top: 10, left: 0, right: 0, bottom: 0),
          child: SizedBox(
            width: double.infinity,
            child: _adMob.getAdBannerWidget(),
          ),
        ),
      ]),
    );
  }
  //UIの仕切り用ボーダーライン
  Widget _border() {
    return Container(
      decoration: BoxDecoration(
        border: Border(
          top: BorderSide(
            color: Colors.grey.shade300,
            width: 1,
          ),
        ),
      ),
    );
  }
  //言語一覧表示
  TableCell _languageTableCell(int index) {
    return TableCell(
      child: RadioListTile(
        visualDensity: const VisualDensity(horizontal: VisualDensity.minimumDensity,vertical: VisualDensity.minimumDensity),
        contentPadding: EdgeInsets.zero,
        title: Text(ConstValue.languageCode.values.elementAt(index)),
        value: ConstValue.languageCode.values.elementAt(index),
        groupValue: _languageValue,
        onChanged: (String? value) {
          setState(() {
            _languageValue = value ?? '';
            _languageKey = ConstValue.languageCode.keys.elementAt(index);
          });
        },
      ),
    );
  }

}

lib/stage.dart

///
/// @author akira ohmachi
/// @copyright ao-system, Inc.
/// @date 2023-10-15
///
library;

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

import 'package:gearcombination/const_value.dart';
import 'package:gearcombination/preferences.dart';
import 'package:gearcombination/game.dart';
import 'package:gearcombination/ad_mob.dart';
import 'package:gearcombination/page_state.dart';
import 'package:gearcombination/gears.dart';
import 'package:gearcombination/gear_one.dart';

class StagePage extends StatefulWidget {
  final Game game;
  const StagePage({super.key, required this.game});

  @override
  State<StagePage> createState() => _StagePageState();
}

class _StagePageState extends State<StagePage> {
  final AdMob _adMob = AdMob();
  final Gears _gears = Gears();
  late Timer _timer;
  double _devicePixelRatio = 0;
  double _screenWidth = 0;
  double _screenHeight = 0;
  double _screenRatio = 1;
  final List<String> _questQuestion = [];
  String _title = '';

  @override
  void initState() {
    super.initState();
    _adMob.load();
    (() async {
      await Preferences.initial();
      await _gears.initial(widget.game.currentQuestNumber);
      _gears.magnificationRate = Preferences.magnificationRate;
      _gears.readSoundVolume();
      _timer = Timer.periodic(const Duration(milliseconds: (1000 ~/ 30)), (timer) {
        setState(() {
          _gears.tick();
        });
      });
    })();
  }
  @override
  void dispose() {
    PageState.setCurrentPage('');
    _gears.setSoundVolumeZero();
    _adMob.dispose();
    _timer.cancel();
    super.dispose();
  }

  ////////
  //非公開
  ////////

}

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;
  }

}

lib/l10n/app_bg.arb

{
	"@@locale":"bg",
	"@locale": {
		"description": "ブルガリア"
	},
  "setting": "Настройка",
  "magnificationRate": "Степен на увеличение",
  "soundVolume": "Сила на звука на звуковия ефект",
  "connectedFlash": "Мига, когато предавката е свързана",
	"fineTuningGearEngagement": "Фина настройка на зацепването на предавките",
  "language": "език",
  "usage1": "Свържете екипировката и изчистете мисията.",
  "usage2": "Въртенето по посока на часовниковата стрелка се показва като плюс, въртенето обратно на часовниковата стрелка се показва като минус.",
  "usage3": "Всички зъби на зъбните колела имат еднакво разстояние и размер.",
  "usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_cs.arb

{
	"@@locale":"cs",
	"@locale": {
		"description": "チェコ"
	},
	"setting": "Nastavení",
	"magnificationRate": "Míra zvětšení",
	"soundVolume": "Hlasitost zvukového efektu",
	"connectedFlash": "Bliká, když je připojeno zařízení",
	"fineTuningGearEngagement": "Jemné doladění záběru ozubených kol",
	"language": "Jazyk",
	"usage1": "Připojte zařízení a zrušte úkol.",
	"usage2": "Otáčení ve směru hodinových ručiček je zobrazeno jako plus, otáčení proti směru hodinových ručiček je zobrazeno jako mínus.",
	"usage3": "Všechny zuby ozubeného kola mají stejnou rozteč a velikost.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_da.arb

{
	"@@locale":"da",
	"@locale": {
		"description": "デンマーク"
	},
	"setting": "Indstilling",
	"magnificationRate": "Forstørrelseshastighed",
	"soundVolume": "Lydeffekt lydstyrke",
	"connectedFlash": "Blink når gear er tilsluttet",
	"fineTuningGearEngagement": "Finjustering af gearindgreb",
	"language": "Sprog",
	"usage1": "Tilslut gearet og ryd missionen.",
	"usage2": "Rotation med uret vises som et plus, rotation mod uret vises som et minus.",
	"usage3": "Alle tandhjulstænderne har samme afstand og størrelse.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_de.arb

{
	"@@locale":"de",
	"@locale": {
		"description": "ドイツ"
	},
	"setting": "Einstellung",
	"magnificationRate": "Vergrößerungsrate",
	"soundVolume": "Lautstärke des Soundeffekts",
	"connectedFlash": "Blinkt, wenn ein Gerät angeschlossen ist",
	"fineTuningGearEngagement": "Feinabstimmung des Gangeingriffs",
	"language": "Sprache",
	"usage1": "Schließe die Ausrüstung an und schließe die Quest ab.",
	"usage2": "Die Drehung im Uhrzeigersinn wird als Plus angezeigt, die Drehung gegen den Uhrzeigersinn wird als Minus angezeigt.",
	"usage3": "Alle Zahnradzähne haben den gleichen Abstand und die gleiche Größe.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_el.arb

{
	"@@locale":"el",
	"@locale": {
		"description": "ギリシャ"
	},
	"setting": "Σύνθεση",
	"magnificationRate": "Ρυθμός μεγέθυνσης",
	"soundVolume": "Ένταση ήχου εφέ",
	"connectedFlash": "Αναβοσβήνει όταν είναι συνδεδεμένο το γρανάζι",
	"fineTuningGearEngagement": "Τελειοποίηση εμπλοκής ταχυτήτων",
	"language": "Γλώσσα",
	"usage1": "Συνδέστε το γρανάζι και εκκαθαρίστε την αποστολή.",
	"usage2": "Η δεξιόστροφη περιστροφή εμφανίζεται ως συν, η αριστερόστροφη περιστροφή εμφανίζεται ως μείον.",
	"usage3": "Όλα τα δόντια του γραναζιού έχουν την ίδια απόσταση και το ίδιο μέγεθος.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_en.arb

{
	"@@locale":"en",
	"@locale": {
		"description": "英語"
	},
	"setting": "Setting",
	"magnificationRate": "Magnification Rate",
	"soundVolume": "Sound Effect Volume",
	"connectedFlash": "Flash when gear is connected",
	"fineTuningGearEngagement": "Fine-tuning gear engagement",
	"language": "Language",
	"usage1": "Connect the gear and clear the quest.",
	"usage2": "Clockwise rotation is displayed as a plus, counterclockwise rotation is displayed as a minus.",
	"usage3": "All the gear teeth have the same spacing and size.",
	"usage4": "",

	"quest0": "Q0: Connect the gears freely.",
	"quest1": "Q1: Connect one gear.",
	"quest2": "Q2: Connect two gears.",
	"quest3": "Q3: Connect three gears.",
	"quest4": "Q4: Connect gears #28",
	"quest5": "Q5: Connect the gears to reach -10rpm",
	"quest6": "Q6: Connect the gears to reach 10rpm",
	"quest7": "Q7: Connect the gears to reach 20rpm",
	"quest8": "Q8: Connect the gears to reach 5rpm",
	"quest9": "Q9: Connect the gears to reach -2.5rpm",
	"quest10": "Q10: Connect the gears to reach 2.5rpm",
	"quest11": "Q11: Connect the gears to reach -51rpm",
	"quest12": "Q12: Connect the gears to reach 170rpm",
	"quest13": "Q13: Connect the gears to reach -170rpm",
	"quest14": "Q14: Connect the gears to reach 15rpm",
	"quest15": "Q15: Connect the gears to reach 4rpm",
	"quest16": "Q16: Connect the gears to reach -4rpm",
	"quest17": "Q17: Connect the gears to reach 360rpm",
	"quest18": "Q18: Connect the gears to reach -360rpm",
	"quest19": "Q19: Connect the gears to reach -30rpm",
	"quest20": "Q20: Connect the gears to reach 72rpm",
	"quest21": "Q21: Connect the gears to reach -60rpm",
	"quest22": "Q22: Connect the gears to reach -1.51rpm",
	"quest23": "Q23: Connect the gears to reach -1.4rpm",
	"quest24": "Q24: Connect the gears to reach -285rpm",
	"quest25": "Q25: Connect the gears to reach 190rpm",
	"quest26": "Q26: Connect the gears to reach 570rpm",
	"quest27": "Q27: Connect the gears to reach 152rpm",
	"quest28": "Q28: Connect the gears to reach 1140rpm",
	"quest29": "Q29: Connect the gears to reach -190rpm",
	"quest30": "Q30: Connect the gears to reach 114rpm",
	"quest31": "Q31: Connect the gears to reach 228rpm",
	"quest32": "Q32: Connect the gears to reach 142.5rpm",
	"quest33": "Q33: Connect the gears to reach 95rpm",
	"quest34": "Q34: Connect the gears to reach -152rpm",
	"quest35": "Q35: Connect the gears to reach 60rpm",
	"quest36": "Q36: Connect the gears to reach -30rpm",
	"quest37": "Q37: Connect the gears to reach -57rpm",
	"quest38": "Q38: Connect the gears to reach 285rpm",
	"quest39": "Q39: Connect the gears to reach 30rpm",
	"quest40": "Q40: Connect the gears to reach 4560rpm",

	"dummy": "dummy"
}

lib/l10n/app_es.arb

{
	"@@locale":"es",
	"@locale": {
		"description": "スペイン"
	},
	"setting": "Configuración",
	"magnificationRate": "Tasa de ampliación",
	"soundVolume": "Volumen del efecto de sonido",
	"connectedFlash": "Destella cuando el equipo está conectado",
	"fineTuningGearEngagement": "Ajuste fino del engranaje",
	"language": "Idioma",
	"usage1": "Conecta el equipo y completa la misión.",
	"usage2": "La rotación en el sentido de las agujas del reloj se muestra como un signo más, la rotación en el sentido contrario a las agujas del reloj se muestra como un signo menos.",
	"usage3": "Todos los dientes del engranaje tienen el mismo espacio y tamaño.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_et.arb

{
	"@@locale":"et",
	"@locale": {
		"description": "エストニア"
	},
	"setting": "Seadistamine",
	"magnificationRate": "Suurenduskiirus",
	"soundVolume": "Heliefekti helitugevus",
	"connectedFlash": "Vilgub, kui käik on ühendatud",
	"fineTuningGearEngagement": "Käigu sisselülitamise peenhäälestus",
	"language": "Keel",
	"usage1": "Ühendage käik ja tühjendage ülesanne.",
	"usage2": "Päripäeva pööramine kuvatakse plussina, vastupäeva pööramine kuvatakse miinusena.",
	"usage3": "Kõik hammasratta hambad on ühesuguse vahekauguse ja suurusega.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_fi.arb

{
	"@@locale":"fi",
	"@locale": {
		"description": "フィンランド"
	},
	"setting": "Asetus",
	"magnificationRate": "Suurennusnopeus",
	"soundVolume": "Äänitehosteen äänenvoimakkuus",
	"connectedFlash": "Vilkkuu, kun vaihde on kytketty",
	"fineTuningGearEngagement": "Vaihteiston kytkennän hienosäätö",
	"language": "Kieli",
	"usage1": "Liitä varusteet ja tyhjennä tehtävä.",
	"usage2": "Myötäpäivään pyöriminen näkyy plussaa, vastapäivään pyöriminen näkyy miinuksena.",
	"usage3": "Kaikilla hammaspyörän hampailla on sama väli ja koko.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_fr.arb

{
	"@@locale":"fr",
	"@locale": {
		"description": "フランス"
	},
	"setting": "Paramètre",
	"magnificationRate": "Taux de grossissement",
	"soundVolume": "Volume des effets sonores",
	"connectedFlash": "Flash lorsque l'équipement est connecté",
	"fineTuningGearEngagement": "Réglage fin de l'engagement des vitesses",
	"language": "Langue",
	"usage1": "Connectez l'équipement et terminez la quête.",
	"usage2": "La rotation dans le sens des aiguilles d'une montre est affichée comme un plus, la rotation dans le sens inverse des aiguilles d'une montre est affichée comme un moins.",
	"usage3": "Toutes les dents d’engrenage ont le même espacement et la même taille.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_hu.arb

{
	"@@locale":"hu",
	"@locale": {
		"description": "ハンガリー"
	},
	"setting": "Beállítás",
	"magnificationRate": "Nagyítási arány",
	"soundVolume": "Hangeffektus hangereje",
	"connectedFlash": "Villog, ha a felszerelés csatlakoztatva van",
	"fineTuningGearEngagement": "Fokozatbekapcsolás finomhangolása",
	"language": "Nyelv",
	"usage1": "Csatlakoztassa a felszerelést, és törölje a küldetést.",
	"usage2": "Az óramutató járásával megegyező irányú forgatás pluszként, az óramutató járásával ellentétes forgatás mínuszként jelenik meg.",
	"usage3": "A fogaskerék minden foga azonos távolságú és méretű.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_it.arb

{
	"@@locale":"it",
	"@locale": {
		"description": "イタリア"
	},
	"setting": "Collocamento",
	"magnificationRate": "Tasso di ingrandimento",
	"soundVolume": "Volume dell'effetto sonoro",
	"connectedFlash": "Lampeggia quando l'attrezzatura è collegata",
	"fineTuningGearEngagement": "Regolazione fine dell'innesto delle marce",
	"language": "Lingua",
	"usage1": "Collega l'attrezzatura e completa la missione.",
	"usage2": "La rotazione in senso orario viene visualizzata come un segno più, la rotazione in senso antiorario viene visualizzata come un segno meno.",
	"usage3": "Tutti i denti degli ingranaggi hanno la stessa spaziatura e dimensione.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_ja.arb

{
	"@@locale":"ja",
	"@locale": {
		"description": "日本"
	},
	"setting": "設定",
	"magnificationRate": "拡大率",
	"soundVolume": "効果音の音量",
	"connectedFlash": "ギア接続で発光",
	"fineTuningGearEngagement": "ギア噛み合わせ微調整",
	"language": "言語",
	"usage1": "ギアを繋げてクエストをクリアしていきます。",
	"usage2": "時計回りはプラス、反時計回りはマイナスで表示されます。",
	"usage3": "すべてのギアの歯の間隔とサイズは同じです。",
	"usage4": "",

    "quest0": "Q0: ギアを自由につなぐ",
    "quest1": "Q1: ギアを1個つなげる",
    "quest2": "Q2: ギアを2個つなげる",
    "quest3": "Q3: ギアを3個つなげる",
    "quest4": "Q4: #28のギアをつなげる",
    "quest5": "Q5: ギアをつなげて-10rpmにする",
    "quest6": "Q6: ギアをつなげて10rpmにする",
    "quest7": "Q7: ギアをつなげて20rpmにする",
    "quest8": "Q8: ギアをつなげて5rpmにする",
    "quest9": "Q9: ギアをつなげて-2.5rpmにする",
    "quest10": "Q10: ギアをつなげて2.5rpmにする",
    "quest11": "Q11: ギアをつなげて-51rpmにする",
    "quest12": "Q12: ギアをつなげて170rpmにする",
    "quest13": "Q13: ギアをつなげて-170rpmにする",
    "quest14": "Q14: ギアをつなげて15rpmにする",
    "quest15": "Q15: ギアをつなげて4rpmにする",
    "quest16": "Q16: ギアをつなげて-4rpmにする",
    "quest17": "Q17: ギアをつなげて360rpmにする",
    "quest18": "Q18: ギアをつなげて-360rpmにする",
    "quest19": "Q19: ギアをつなげて-30rpmにする",
    "quest20": "Q20: ギアをつなげて72rpmにする",
    "quest21": "Q21: ギアをつなげて-60rpmにする",
    "quest22": "Q22: ギアをつなげて-1.51rpmにする",
    "quest23": "Q23: ギアをつなげて-1.4rpmにする",
    "quest24": "Q24: ギアをつなげて-285rpmにする",
    "quest25": "Q25: ギアをつなげて190rpmにする",
    "quest26": "Q26: ギアをつなげて570rpmにする",
    "quest27": "Q27: ギアをつなげて152rpmにする",
    "quest28": "Q28: ギアをつなげて1140rpmにする",
    "quest29": "Q29: ギアをつなげて-190rpmにする",
    "quest30": "Q30: ギアをつなげて114rpmにする",
    "quest31": "Q31: ギアをつなげて228rpmにする",
    "quest32": "Q32: ギアをつなげて142.5rpmにする",
    "quest33": "Q33: ギアをつなげて95rpmにする",
    "quest34": "Q34: ギアをつなげて-152rpmにする",
    "quest35": "Q35: ギアをつなげて60rpmにする",
    "quest36": "Q36: ギアをつなげて-30rpmにする",
    "quest37": "Q37: ギアをつなげて-57rpmにする",
    "quest38": "Q38: ギアをつなげて285rpmにする",
    "quest39": "Q39: ギアをつなげて30rpmにする",
    "quest40": "Q40: ギアをつなげて4560rpmにする",

	"dummy": "dummy"
}

lib/l10n/app_lt.arb

{
	"@@locale":"lt",
	"@locale": {
		"description": "リトアニア"
	},
	"setting": "Nustatymas",
	"magnificationRate": "Didinimo greitis",
	"soundVolume": "Garso efekto garsumas",
	"connectedFlash": "Mirksi, kai prijungta pavara",
	"fineTuningGearEngagement": "Tikslus pavarų perjungimo reguliavimas",
	"language": "Kalba",
	"usage1": "Prijunkite pavarą ir išvalykite užduotį.",
	"usage2": "Sukimas pagal laikrodžio rodyklę rodomas kaip pliusas, o sukimas prieš laikrodžio rodyklę rodomas kaip minusas.",
	"usage3": "Visi krumpliaračio dantys yra vienodo atstumo ir dydžio.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_lv.arb

{
	"@@locale":"lv",
	"@locale": {
		"description": "ラトビア"
	},
	"setting": "Iestatījums",
	"magnificationRate": "Palielinājuma ātrums",
	"soundVolume": "Skaņas efekta skaļums",
	"connectedFlash": "Mirgo, kad ir pievienots pārnesums",
	"fineTuningGearEngagement": "Precīza pārnesumu ieslēgšanās",
	"language": "Valoda",
	"usage1": "Pievienojiet rīku un notīriet uzdevumu.",
	"usage2": "Rotācija pulksteņrādītāja virzienā tiek parādīta kā plus, griešanās pretēji pulksteņrādītāja virzienam tiek parādīta kā mīnuss.",
	"usage3": "Visiem zobratu zobiem ir vienāds attālums un vienāds izmērs.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_nl.arb

{
	"@@locale":"nl",
	"@locale": {
		"description": "オランダ"
	},
	"setting": "Instelling",
	"magnificationRate": "Vergrotingssnelheid",
	"soundVolume": "Geluidseffectvolume",
	"connectedFlash": "Knippert wanneer de uitrusting is aangesloten",
	"fineTuningGearEngagement": "Fijnafstelling van de versnellingsinschakeling",
	"language": "Taal",
	"usage1": "Sluit de uitrusting aan en voltooi de zoektocht.",
	"usage2": "Rotatie met de klok mee wordt weergegeven als een plus, rotatie tegen de klok in wordt weergegeven als een minpunt.",
	"usage3": "Alle tandwieltanden hebben dezelfde afstand en grootte.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_pl.arb

{
	"@@locale":"pl",
	"@locale": {
		"description": "ポーランド"
	},
	"setting": "Ustawienie",
	"magnificationRate": "Stopień powiększenia",
	"soundVolume": "Głośność efektu dźwiękowego",
	"connectedFlash": "Miga po podłączeniu sprzętu",
	"fineTuningGearEngagement": "Precyzyjna regulacja włączania biegów",
	"language": "Język",
	"usage1": "Podłącz sprzęt i zakończ zadanie.",
	"usage2": "Obrót w prawo jest wyświetlany jako plus, obrót w lewo jest wyświetlany jako minus.",
	"usage3": "Wszystkie zęby przekładni mają ten sam rozstaw i rozmiar.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_pt.arb

{
	"@@locale":"pt",
	"@locale": {
		"description": "ポルトガル"
	},
	"setting": "Contexto",
	"magnificationRate": "Taxa de ampliação",
	"soundVolume": "Volume do efeito sonoro",
	"connectedFlash": "Pisca quando o equipamento está conectado",
	"fineTuningGearEngagement": "Engajamento de engrenagem de ajuste fino",
	"language": "Linguagem",
	"usage1": "Conecte o equipamento e conclua a missão.",
	"usage2": "A rotação no sentido horário é exibida como um sinal de mais, a rotação no sentido anti-horário é exibida como um sinal de menos.",
	"usage3": "Todos os dentes da engrenagem têm o mesmo espaçamento e tamanho.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_ro.arb

{
	"@@locale":"ro",
	"@locale": {
		"description": "ルーマニア"
	},
	"setting": "Setare",
	"magnificationRate": "Rata de mărire",
	"soundVolume": "Volumul efectului sonor",
	"connectedFlash": "Clipește când echipamentul este conectat",
	"fineTuningGearEngagement": "Reglarea fină a treptei de viteză",
	"language": "Limba",
	"usage1": "Conectați echipamentul și finalizați misiunea.",
	"usage2": "Rotirea în sensul acelor de ceasornic este afișată ca un plus, iar rotația în sens invers acelor de ceasornic este afișată ca un minus.",
	"usage3": "Toți dinții angrenajului au aceeași distanță și dimensiune.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_ru.arb

{
	"@@locale":"ru",
	"@locale": {
		"description": "ロシア"
	},
	"setting": "Параметр",
	"magnificationRate": "Скорость увеличения",
	"soundVolume": "Громкость звукового эффекта",
	"connectedFlash": "Мигает при подключении шестерни",
	"fineTuningGearEngagement": "Точная настройка включения передач",
	"language": "Язык",
	"usage1": "Подключите шестерню и завершите квест.",
	"usage2": "Вращение по часовой стрелке отображается как плюс, вращение против часовой стрелки отображается как минус.",
	"usage3": "Все зубья шестерни имеют одинаковое расстояние и размер.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_sk.arb

{
	"@@locale":"sk",
	"@locale": {
		"description": "スロバキア"
	},
	"setting": "Nastavenie",
	"magnificationRate": "Miera zväčšenia",
	"soundVolume": "Hlasitosť zvukových efektov",
	"connectedFlash": "Bliká, keď je zariadenie pripojené",
	"fineTuningGearEngagement": "Jemné doladenie záberu prevodového stupňa",
	"language": "Jazyk",
	"usage1": "Pripojte výstroj a zrušte úlohu.",
	"usage2": "Otáčanie v smere hodinových ručičiek sa zobrazuje ako plus, otáčanie proti smeru hodinových ručičiek sa zobrazuje ako mínus.",
	"usage3": "Všetky zuby ozubeného kolesa majú rovnaký rozstup a veľkosť.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_sv.arb

{
	"@@locale":"sv",
	"@locale": {
		"description": "スウェーデン"
	},
	"setting": "Miljö",
	"magnificationRate": "Förstoringshastighet",
	"soundVolume": "Ljudeffektvolym",
	"connectedFlash": "Blinkar när växeln är ansluten",
	"fineTuningGearEngagement": "Finjustera redskapskopplingen",
	"language": "Språk",
	"usage1": "Anslut utrustningen och rensa uppdraget.",
	"usage2": "Medurs rotation visas som ett plus, moturs rotation visas som ett minus.",
	"usage3": "Alla kuggar har samma avstånd och storlek.",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_th.arb

{
	"@@locale":"th",
	"@locale": {
		"description": "タイ"
	},
	"setting": "การตั้งค่า",
	"magnificationRate": "อัตราการขยาย",
	"soundVolume": "ระดับเสียงเอฟเฟกต์",
	"connectedFlash": "กะพริบเมื่อเชื่อมต่อเกียร์",
	"fineTuningGearEngagement": "การปรับแต่งการเข้าเกียร์อย่างละเอียด",
	"language": "ภาษา",
	"usage1": "เชื่อมต่ออุปกรณ์และเคลียร์ภารกิจ",
	"usage2": "การหมุนตามเข็มนาฬิกาจะแสดงเป็นเครื่องหมายบวก การหมุนทวนเข็มนาฬิกาจะแสดงเป็นเครื่องหมายลบ",
	"usage3": "ฟันเฟืองทั้งหมดมีระยะห่างและขนาดเท่ากัน",
	"usage4": "",


	"dummy": "dummy"
}

lib/l10n/app_zh.arb

{
	"@@locale":"zh",
	"@locale": {
		"description": "中国"
	},
	"setting": "环境",
	"magnificationRate": "放大倍率",
	"soundVolume": "音效音量",
	"connectedFlash": "连接齿轮时闪烁",
	"fineTuningGearEngagement": "微调齿轮啮合",
	"language": "语言",
	"usage1": "连接装备并完成任务。",
	"usage2": "顺时针旋转显示为正,逆时针旋转显示为负。",
	"usage3": "所有齿轮的齿距和尺寸均相同。",
	"usage4": "",


	"dummy": "dummy"
}