Initial commit with gitignore
This commit is contained in:
9
lib/data/cardtype_data.dart
Normal file
9
lib/data/cardtype_data.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
const cardTypes = [
|
||||
'スタンダード',
|
||||
'POTW',
|
||||
'EPIC.BT',
|
||||
'ハイライト',
|
||||
'ショータイム',
|
||||
];
|
||||
|
||||
const cardTypeLabel = 'カードタイプ縛り';
|
||||
405
lib/data/formation_positions.dart
Normal file
405
lib/data/formation_positions.dart
Normal file
@@ -0,0 +1,405 @@
|
||||
// フォーメーションごとの座標配置(イーフトっぽい並び)
|
||||
final Map<String, List<List<Map<String, double>>>> formationLayouts = {
|
||||
'4-1-2-3': [
|
||||
[
|
||||
{'x': 0.20, 'y': 0.12},
|
||||
{'x': 0.50, 'y': 0.08},
|
||||
{'x': 0.80, 'y': 0.12},
|
||||
],
|
||||
[
|
||||
{'x': 0.40, 'y': 0.30},
|
||||
{'x': 0.60, 'y': 0.30},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.45},
|
||||
],
|
||||
[
|
||||
{'x': 0.15, 'y': 0.55},
|
||||
{'x': 0.35, 'y': 0.62},
|
||||
{'x': 0.65, 'y': 0.62},
|
||||
{'x': 0.85, 'y': 0.55},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
'4-3-3': [
|
||||
[
|
||||
{'x': 0.20, 'y': 0.12},
|
||||
{'x': 0.50, 'y': 0.08},
|
||||
{'x': 0.80, 'y': 0.12},
|
||||
],
|
||||
[
|
||||
{'x': 0.30, 'y': 0.35},
|
||||
{'x': 0.50, 'y': 0.45},
|
||||
{'x': 0.70, 'y': 0.35},
|
||||
],
|
||||
[
|
||||
{'x': 0.15, 'y': 0.55},
|
||||
{'x': 0.35, 'y': 0.62},
|
||||
{'x': 0.65, 'y': 0.62},
|
||||
{'x': 0.85, 'y': 0.55},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
'4-4-2': [
|
||||
[
|
||||
{'x': 0.35, 'y': 0.1},
|
||||
{'x': 0.65, 'y': 0.1},
|
||||
],
|
||||
[
|
||||
{'x': 0.20, 'y': 0.35},
|
||||
{'x': 0.40, 'y': 0.45},
|
||||
{'x': 0.60, 'y': 0.45},
|
||||
{'x': 0.80, 'y': 0.35},
|
||||
],
|
||||
[
|
||||
{'x': 0.15, 'y': 0.55},
|
||||
{'x': 0.35, 'y': 0.62},
|
||||
{'x': 0.65, 'y': 0.62},
|
||||
{'x': 0.85, 'y': 0.55},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
'4-3-2-1': [
|
||||
[
|
||||
{'x': 0.50, 'y': 0.1},
|
||||
],
|
||||
[
|
||||
{'x': 0.35, 'y': 0.25},
|
||||
{'x': 0.65, 'y': 0.25},
|
||||
],
|
||||
[
|
||||
{'x': 0.25, 'y': 0.40},
|
||||
{'x': 0.50, 'y': 0.45},
|
||||
{'x': 0.75, 'y': 0.40},
|
||||
],
|
||||
[
|
||||
{'x': 0.15, 'y': 0.55},
|
||||
{'x': 0.35, 'y': 0.62},
|
||||
{'x': 0.65, 'y': 0.62},
|
||||
{'x': 0.85, 'y': 0.55},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
'4-3-1-2': [
|
||||
[
|
||||
{'x': 0.35, 'y': 0.1},
|
||||
{'x': 0.65, 'y': 0.1},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.30},
|
||||
],
|
||||
[
|
||||
{'x': 0.25, 'y': 0.40},
|
||||
{'x': 0.50, 'y': 0.45},
|
||||
{'x': 0.75, 'y': 0.40},
|
||||
],
|
||||
[
|
||||
{'x': 0.15, 'y': 0.55},
|
||||
{'x': 0.35, 'y': 0.62},
|
||||
{'x': 0.65, 'y': 0.62},
|
||||
{'x': 0.85, 'y': 0.55},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
'4-2-3-1': [
|
||||
[
|
||||
{'x': 0.50, 'y': 0.1},
|
||||
],
|
||||
[
|
||||
{'x': 0.20, 'y': 0.30},
|
||||
{'x': 0.50, 'y': 0.30},
|
||||
{'x': 0.80, 'y': 0.30},
|
||||
],
|
||||
[
|
||||
{'x': 0.30, 'y': 0.45},
|
||||
{'x': 0.70, 'y': 0.45},
|
||||
],
|
||||
[
|
||||
{'x': 0.15, 'y': 0.55},
|
||||
{'x': 0.35, 'y': 0.62},
|
||||
{'x': 0.65, 'y': 0.62},
|
||||
{'x': 0.85, 'y': 0.55},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
'4-2-1-3': [
|
||||
[
|
||||
{'x': 0.20, 'y': 0.12},
|
||||
{'x': 0.50, 'y': 0.08},
|
||||
{'x': 0.80, 'y': 0.12},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.28},
|
||||
],
|
||||
[
|
||||
{'x': 0.40, 'y': 0.45},
|
||||
{'x': 0.60, 'y': 0.45},
|
||||
],
|
||||
[
|
||||
{'x': 0.15, 'y': 0.55},
|
||||
{'x': 0.35, 'y': 0.62},
|
||||
{'x': 0.65, 'y': 0.62},
|
||||
{'x': 0.85, 'y': 0.55},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
'4-1-4-1':[
|
||||
[
|
||||
{'x': 0.50, 'y': 0.1},
|
||||
],
|
||||
[
|
||||
{'x': 0.15, 'y': 0.25},
|
||||
{'x': 0.40, 'y': 0.25},
|
||||
{'x': 0.60, 'y': 0.25},
|
||||
{'x': 0.85, 'y': 0.25},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.45},
|
||||
],
|
||||
[
|
||||
{'x': 0.15, 'y': 0.55},
|
||||
{'x': 0.35, 'y': 0.62},
|
||||
{'x': 0.65, 'y': 0.62},
|
||||
{'x': 0.85, 'y': 0.55},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
'4-2-2-2': [
|
||||
[
|
||||
{'x': 0.35, 'y': 0.08},
|
||||
{'x': 0.65, 'y': 0.08},
|
||||
],
|
||||
[
|
||||
{'x': 0.20, 'y': 0.25},
|
||||
{'x': 0.80, 'y': 0.25},
|
||||
],
|
||||
[
|
||||
{'x': 0.40, 'y': 0.45},
|
||||
{'x': 0.60, 'y': 0.45},
|
||||
],
|
||||
[
|
||||
{'x': 0.15, 'y': 0.55},
|
||||
{'x': 0.35, 'y': 0.62},
|
||||
{'x': 0.65, 'y': 0.62},
|
||||
{'x': 0.85, 'y': 0.55},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
'3-4-3': [
|
||||
[
|
||||
{'x': 0.20, 'y': 0.12},
|
||||
{'x': 0.50, 'y': 0.08},
|
||||
{'x': 0.80, 'y': 0.12},
|
||||
],
|
||||
[
|
||||
{'x': 0.20, 'y': 0.30},
|
||||
{'x': 0.40, 'y': 0.45},
|
||||
{'x': 0.60, 'y': 0.45},
|
||||
{'x': 0.80, 'y': 0.30},
|
||||
],
|
||||
[
|
||||
{'x': 0.20, 'y': 0.65},
|
||||
{'x': 0.50, 'y': 0.65},
|
||||
{'x': 0.80, 'y': 0.65},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
'3-2-4-1': [
|
||||
[
|
||||
{'x': 0.50, 'y': 0.1},
|
||||
],
|
||||
[
|
||||
{'x': 0.15, 'y': 0.25},
|
||||
{'x': 0.40, 'y': 0.25},
|
||||
{'x': 0.60, 'y': 0.25},
|
||||
{'x': 0.85, 'y': 0.25},
|
||||
],
|
||||
[
|
||||
{'x': 0.40, 'y': 0.45},
|
||||
{'x': 0.60, 'y': 0.45},
|
||||
],
|
||||
[
|
||||
{'x': 0.20, 'y': 0.65},
|
||||
{'x': 0.50, 'y': 0.65},
|
||||
{'x': 0.80, 'y': 0.65},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
'3-2-3-2': [
|
||||
[
|
||||
{'x': 0.35, 'y': 0.1},
|
||||
{'x': 0.65, 'y': 0.1},
|
||||
],
|
||||
[
|
||||
{'x': 0.20, 'y': 0.30},
|
||||
{'x': 0.50, 'y': 0.30},
|
||||
{'x': 0.80, 'y': 0.30},
|
||||
],
|
||||
[
|
||||
{'x': 0.40, 'y': 0.45},
|
||||
{'x': 0.60, 'y': 0.45},
|
||||
],
|
||||
[
|
||||
{'x': 0.20, 'y': 0.65},
|
||||
{'x': 0.50, 'y': 0.65},
|
||||
{'x': 0.80, 'y': 0.65},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
'3-1-4-2': [
|
||||
[
|
||||
{'x': 0.35, 'y': 0.1},
|
||||
{'x': 0.65, 'y': 0.1},
|
||||
],
|
||||
[
|
||||
{'x': 0.15, 'y': 0.30},
|
||||
{'x': 0.40, 'y': 0.30},
|
||||
{'x': 0.60, 'y': 0.30},
|
||||
{'x': 0.85, 'y': 0.30},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.45},
|
||||
],
|
||||
[
|
||||
{'x': 0.20, 'y': 0.65},
|
||||
{'x': 0.50, 'y': 0.65},
|
||||
{'x': 0.80, 'y': 0.65},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
'5-3-2': [
|
||||
[
|
||||
{'x': 0.40, 'y': 0.1},
|
||||
{'x': 0.60, 'y': 0.1},
|
||||
],
|
||||
[
|
||||
{'x': 0.30, 'y': 0.40},
|
||||
{'x': 0.50, 'y': 0.45},
|
||||
{'x': 0.70, 'y': 0.40},
|
||||
],
|
||||
[
|
||||
{'x': 0.12, 'y': 0.52},
|
||||
{'x': 0.32, 'y': 0.62},
|
||||
{'x': 0.50, 'y': 0.64},
|
||||
{'x': 0.68, 'y': 0.62},
|
||||
{'x': 0.88, 'y': 0.52},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
'5-2-2-1': [
|
||||
[
|
||||
{'x': 0.50, 'y': 0.1},
|
||||
],
|
||||
[
|
||||
{'x': 0.35, 'y': 0.30},
|
||||
{'x': 0.65, 'y': 0.30},
|
||||
],
|
||||
[
|
||||
{'x': 0.40, 'y': 0.45},
|
||||
{'x': 0.60, 'y': 0.45},
|
||||
],
|
||||
[
|
||||
{'x': 0.12, 'y': 0.52},
|
||||
{'x': 0.32, 'y': 0.62},
|
||||
{'x': 0.50, 'y': 0.64},
|
||||
{'x': 0.68, 'y': 0.62},
|
||||
{'x': 0.88, 'y': 0.52},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
'5-2-1-2': [
|
||||
[
|
||||
{'x': 0.35, 'y': 0.08},
|
||||
{'x': 0.65, 'y': 0.08},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.25},
|
||||
],
|
||||
[
|
||||
{'x': 0.40, 'y': 0.45},
|
||||
{'x': 0.60, 'y': 0.45},
|
||||
],
|
||||
[
|
||||
{'x': 0.12, 'y': 0.52},
|
||||
{'x': 0.32, 'y': 0.62},
|
||||
{'x': 0.50, 'y': 0.64},
|
||||
{'x': 0.68, 'y': 0.62},
|
||||
{'x': 0.88, 'y': 0.52},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
'5-2-3': [
|
||||
[
|
||||
{'x': 0.20, 'y': 0.12},
|
||||
{'x': 0.50, 'y': 0.08},
|
||||
{'x': 0.80, 'y': 0.12},
|
||||
],
|
||||
[
|
||||
{'x': 0.40, 'y': 0.45},
|
||||
{'x': 0.60, 'y': 0.45},
|
||||
],
|
||||
[
|
||||
{'x': 0.12, 'y': 0.52},
|
||||
{'x': 0.32, 'y': 0.62},
|
||||
{'x': 0.50, 'y': 0.64},
|
||||
{'x': 0.68, 'y': 0.62},
|
||||
{'x': 0.88, 'y': 0.52},
|
||||
],
|
||||
[
|
||||
{'x': 0.50, 'y': 0.85},
|
||||
],
|
||||
],
|
||||
|
||||
};
|
||||
|
||||
List<List<Map<String, double>>> getFormationLayout(String formation) {
|
||||
return formationLayouts[formation] ?? formationLayouts['4-3-3']!;
|
||||
}
|
||||
13
lib/data/league_data.dart
Normal file
13
lib/data/league_data.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
// 遊び用のリーグ候補
|
||||
const leagues = [
|
||||
'プレミアリーグ',
|
||||
'ラ・リーガ',
|
||||
'セリエA',
|
||||
'ブンデスリーガ',
|
||||
'リーグ・アン',
|
||||
'Jリーグ',
|
||||
'代表(ナショナル)',
|
||||
];
|
||||
|
||||
// “縛りの名前”を表示するとき用(任意)
|
||||
const leagueLabel = 'リーグ縛り';
|
||||
160
lib/data/player_attributes.dart
Normal file
160
lib/data/player_attributes.dart
Normal file
@@ -0,0 +1,160 @@
|
||||
//各ポジションごとのプレースタイルを定義 = {
|
||||
final Map<String, List<String>> playerAttributes ={
|
||||
'GK':[
|
||||
'守備的GK',
|
||||
'攻撃的GK',
|
||||
],
|
||||
|
||||
'CB':[
|
||||
'ハードプレス',
|
||||
'ビルドアップ',
|
||||
'オーバーラップ',
|
||||
'無印',
|
||||
],
|
||||
|
||||
'LSB':[
|
||||
'守備的サイドバック',
|
||||
'攻撃的サイドバック',
|
||||
'インナーラップサイドバック',
|
||||
'無印',
|
||||
],
|
||||
|
||||
'RSB':[
|
||||
'守備的サイドバック',
|
||||
'攻撃的サイドバック',
|
||||
'インナーラップサイドバック',
|
||||
'無印',
|
||||
],
|
||||
|
||||
'DMF':[
|
||||
'ハードプレス',
|
||||
'アンカー',
|
||||
'ボックストゥボックス',
|
||||
'プレーメイカー',
|
||||
'無印',
|
||||
],
|
||||
|
||||
'CMF':[
|
||||
'ハードプレス',
|
||||
'プレーメイカー',
|
||||
'ボックストゥボックス',
|
||||
'2列目からの飛び出し'
|
||||
'無印',
|
||||
],
|
||||
|
||||
'OMF':[
|
||||
'2列目からの飛び出し',
|
||||
'チャンスメイカー',
|
||||
'ナンバー10',
|
||||
'デコイラン',
|
||||
'無印',
|
||||
],
|
||||
|
||||
'LMF':[
|
||||
'2列目からの飛び出し',
|
||||
'ボックストゥボックス',
|
||||
'クロサー',
|
||||
'インサイドレシーバー',
|
||||
'チャンスメイカー',
|
||||
'無印',
|
||||
],
|
||||
|
||||
'RMF':[
|
||||
'2列目からの飛び出し',
|
||||
'ボックストゥボックス',
|
||||
'クロサー',
|
||||
'インサイドレシーバー',
|
||||
'チャンスメイカー',
|
||||
'無印',
|
||||
],
|
||||
|
||||
'ST':[
|
||||
'2列目からの飛び出し',
|
||||
'ナンバー10',
|
||||
'チャンスメイカー',
|
||||
'リンクフォワード',
|
||||
'デコイラン',
|
||||
'無印',
|
||||
],
|
||||
|
||||
'LWG':[
|
||||
'チャンスメイカー',
|
||||
'クロサー',
|
||||
'インサイドレシーバー',
|
||||
'ウイングストライカー',
|
||||
'無印'
|
||||
],
|
||||
|
||||
'RWG':[
|
||||
'チャンスメイカー',
|
||||
'クロサー',
|
||||
'インサイドレシーバー',
|
||||
'ウイングストライカー',
|
||||
'無印'
|
||||
],
|
||||
|
||||
'CF':[
|
||||
'ラインブレイカー',
|
||||
'ボックスストライカー',
|
||||
'ターゲットマン',
|
||||
'リンクフォワード',
|
||||
'デコイラン',
|
||||
'無印',
|
||||
],
|
||||
};
|
||||
|
||||
//全てのプレースタイル(完全ランダム用)
|
||||
const allPlayStyle = [
|
||||
'守備的GK',
|
||||
'攻撃的GK',
|
||||
'インナーラップサイドバック',
|
||||
'守備的サイドバック',
|
||||
'攻撃的サイドバック',
|
||||
'オーバーラップ',
|
||||
'ビルドアップ',
|
||||
'プレーメイカー',
|
||||
'ハードプレス',
|
||||
'アンカー',
|
||||
'ボックストゥボックス',
|
||||
'2列目からの飛び出し',
|
||||
'ナンバー10',
|
||||
'クロサー',
|
||||
'インサイドレシーバー',
|
||||
'ウイングストライカー',
|
||||
'チャンスメイカー',
|
||||
'リンクフォワード',
|
||||
'ターゲットマン',
|
||||
'ボックスストライカー',
|
||||
'デコイラン',
|
||||
'ラインブレイカー',
|
||||
];
|
||||
|
||||
const lowProbabilityStyles = [
|
||||
'デコイラン',
|
||||
'ナンバー10',
|
||||
'ターゲットマン',
|
||||
'インナーラップサイドバック',
|
||||
'オーバーラップ',
|
||||
'インサイドレシーバー'
|
||||
];
|
||||
|
||||
|
||||
//抽選用フォーメーション候補
|
||||
const formations = [
|
||||
'4-4-2',
|
||||
'4-3-3',
|
||||
'4-3-2-1',
|
||||
'4-3-1-2',
|
||||
'4-2-3-1',
|
||||
'4-2-1-3',
|
||||
'4-2-2-2',
|
||||
'4-1-4-1',
|
||||
'3-4-3',
|
||||
'3-2-4-1',
|
||||
'3-2-3-2',
|
||||
'3-1-4-2',
|
||||
'5-3-2',
|
||||
'5-2-2-1',
|
||||
'5-2-1-2',
|
||||
'5-2-3',
|
||||
];
|
||||
6
lib/data/player_positions.dart
Normal file
6
lib/data/player_positions.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
final Map <String, List<String>> positionsGroups = {
|
||||
'GK': ['GK'],
|
||||
'DF': ['CB','LSB','RSB'],
|
||||
'MF': ['DMF','CMF','OMF','LMF','RMF'],
|
||||
'FW': ['CF','ST','LWG','RWG'],
|
||||
};
|
||||
25
lib/main.dart
Normal file
25
lib/main.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'screens/home_screen.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const SquadSimulatorApp());
|
||||
}
|
||||
|
||||
class SquadSimulatorApp extends StatelessWidget {
|
||||
const SquadSimulatorApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'eFootball Squad Simulator',
|
||||
theme: ThemeData.dark(useMaterial3: true),
|
||||
home: const HomeScreen(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
16
lib/models/player.dart
Normal file
16
lib/models/player.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
class Player {
|
||||
final String name;
|
||||
final Set<String> positions;
|
||||
|
||||
Player({required this.name, required this.positions});
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'name': name,
|
||||
'positions': positions.toList(),
|
||||
};
|
||||
|
||||
factory Player.fromMap(Map<String, dynamic> map) => Player(
|
||||
name: (map['name'] ?? '').toString(),
|
||||
positions: Set<String>.from(map['positions'] ?? const []),
|
||||
);
|
||||
}
|
||||
279
lib/screens/home_screen.dart
Normal file
279
lib/screens/home_screen.dart
Normal file
@@ -0,0 +1,279 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../services/formation_generator.dart';
|
||||
import '../data/league_data.dart';
|
||||
import '../data/cardtype_data.dart';
|
||||
import 'result_screen.dart';
|
||||
import '../models/player.dart';
|
||||
import 'pool_screen.dart';
|
||||
|
||||
const allRoles = [
|
||||
'GK',
|
||||
'CB', 'LSB', 'RSB',
|
||||
'DMF', 'CMF', 'OMF', 'LMF', 'RMF',
|
||||
'CF', 'ST', 'RWG', 'LWG',
|
||||
];
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
static const _poolKey = 'player_pool_v1';
|
||||
|
||||
ConstraintMode _mode = ConstraintMode.playstyle;
|
||||
bool _rareStyleOn = false;
|
||||
|
||||
String? _selectedLeague;
|
||||
String? _selectedCardType;
|
||||
|
||||
// 保存対象:選手プール
|
||||
List<Map<String, dynamic>> _playerPool = [];
|
||||
|
||||
// ★チムスタ重み(0〜100の重み。合計は気にしない)
|
||||
final Map<String, int> _teamStyleWeights = {
|
||||
'ポゼッション': 50,
|
||||
'ショートカウンター': 50,
|
||||
'ロングカウンター': 50,
|
||||
'サイドアタック': 50,
|
||||
'ロングボール': 50,
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadPool();
|
||||
}
|
||||
|
||||
Future<void> _loadPool() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_poolKey);
|
||||
if (raw == null || raw.isEmpty) return;
|
||||
|
||||
try {
|
||||
final decoded = jsonDecode(raw) as List;
|
||||
setState(() {
|
||||
_playerPool = decoded
|
||||
.map((e) => {
|
||||
'name': (e['name'] ?? '').toString(),
|
||||
'positions': List<String>.from(e['positions'] ?? const []),
|
||||
})
|
||||
.toList();
|
||||
});
|
||||
} catch (_) {
|
||||
// 破損時は無視
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _savePool() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_poolKey, jsonEncode(_playerPool));
|
||||
}
|
||||
|
||||
Future<void> _openPoolManager() async {
|
||||
// Map -> Player
|
||||
final currentPlayers = _playerPool.map((m) => Player.fromMap(m)).toList();
|
||||
|
||||
final updated = await Navigator.push<List<Player>>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => PoolScreen(initial: currentPlayers),
|
||||
),
|
||||
);
|
||||
|
||||
if (updated != null) {
|
||||
final mapped = updated.map((p) => p.toMap()).toList();
|
||||
setState(() => _playerPool = mapped);
|
||||
await _savePool();
|
||||
}
|
||||
}
|
||||
|
||||
// ★チムスタスライダー部品
|
||||
Widget _teamStyleSlider(String label) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'$label : ${_teamStyleWeights[label]}',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
Slider(
|
||||
value: (_teamStyleWeights[label] ?? 0).toDouble(),
|
||||
min: 0,
|
||||
max: 100,
|
||||
divisions: 100,
|
||||
onChanged: (v) => setState(() {
|
||||
_teamStyleWeights[label] = v.round();
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _goResult() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ResultScreen(
|
||||
mode: _mode,
|
||||
rareStyleOn: _rareStyleOn,
|
||||
selectedLeague: _selectedLeague,
|
||||
selectedCardType: _selectedCardType,
|
||||
playerPool: _playerPool,
|
||||
teamStyleWeights: _teamStyleWeights, // ★追加:Resultへ渡す
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.green[900],
|
||||
appBar: AppBar(
|
||||
title: const Text('efootball スカッドメーカー'),
|
||||
backgroundColor: Colors.green[900],
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _openPoolManager,
|
||||
icon: const Icon(Icons.people_alt),
|
||||
tooltip: '選手プール',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'登録選手: ${_playerPool.length}人',
|
||||
style: const TextStyle(color: Colors.white70),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _openPoolManager,
|
||||
child: const Text('選手プール'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
DropdownButtonFormField<ConstraintMode>(
|
||||
value: _mode,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '遊び方(縛り)',
|
||||
filled: true,
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: ConstraintMode.playstyle,
|
||||
child: Text('プレースタイル縛り'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ConstraintMode.league,
|
||||
child: Text('リーグ縛り'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ConstraintMode.cardType,
|
||||
child: Text('カードタイプ縛り'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: ConstraintMode.pool,
|
||||
child: Text('選手プール抽選'),
|
||||
),
|
||||
],
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() {
|
||||
_mode = v;
|
||||
if (_mode != ConstraintMode.league) _selectedLeague = null;
|
||||
if (_mode != ConstraintMode.cardType) _selectedCardType = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
if (_mode == ConstraintMode.playstyle)
|
||||
SwitchListTile(
|
||||
title: const Text(
|
||||
'希少なプレースタイルを出したい時にON',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
value: _rareStyleOn,
|
||||
onChanged: (v) => setState(() => _rareStyleOn = v),
|
||||
),
|
||||
|
||||
if (_mode == ConstraintMode.league)
|
||||
DropdownButtonFormField<String?>(
|
||||
value: _selectedLeague,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'リーグ(ランダム=ポジション別)',
|
||||
filled: true,
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('ランダム'),
|
||||
),
|
||||
...leagues.map(
|
||||
(l) => DropdownMenuItem<String?>(value: l, child: Text(l)),
|
||||
),
|
||||
],
|
||||
onChanged: (v) => setState(() => _selectedLeague = v),
|
||||
),
|
||||
|
||||
if (_mode == ConstraintMode.cardType)
|
||||
DropdownButtonFormField<String?>(
|
||||
value: _selectedCardType,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'カードタイプ(ランダム=ポジション別)',
|
||||
filled: true,
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('ランダム'),
|
||||
),
|
||||
...cardTypes.map(
|
||||
(c) => DropdownMenuItem<String?>(value: c, child: Text(c)),
|
||||
),
|
||||
],
|
||||
onChanged: (v) => setState(() => _selectedCardType = v),
|
||||
),
|
||||
|
||||
// ★チムスタ確率UI
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'チームスタイル確率(重み)',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
_teamStyleSlider('ポゼッション'),
|
||||
_teamStyleSlider('ショートカウンター'),
|
||||
_teamStyleSlider('ロングカウンター'),
|
||||
_teamStyleSlider('サイドアタック'),
|
||||
_teamStyleSlider('ロングボール'),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
ElevatedButton.icon(
|
||||
onPressed: _goResult,
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
label: const Text('結果を見る(抽選)'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.green[900],
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
133
lib/screens/pool_screen.dart
Normal file
133
lib/screens/pool_screen.dart
Normal file
@@ -0,0 +1,133 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/player.dart';
|
||||
|
||||
const allRoles = [
|
||||
'GK',
|
||||
'CB', 'LSB', 'RSB',
|
||||
'DMF', 'CMF', 'OMF', 'LMF', 'RMF',
|
||||
'CF', 'ST', 'LWG', 'RWG',
|
||||
];
|
||||
|
||||
class PoolScreen extends StatefulWidget {
|
||||
final List<Player> initial;
|
||||
const PoolScreen({super.key, required this.initial});
|
||||
|
||||
@override
|
||||
State<PoolScreen> createState() => _PoolScreenState();
|
||||
}
|
||||
|
||||
class _PoolScreenState extends State<PoolScreen> {
|
||||
late List<Player> _players;
|
||||
final _nameCtrl = TextEditingController();
|
||||
final Set<String> _selected = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_players = [...widget.initial];
|
||||
}
|
||||
|
||||
void _add() {
|
||||
final name = _nameCtrl.text.trim();
|
||||
|
||||
if (name.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('選手名を入力してください')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selected.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('適正ポジションを1つ以上選んでください')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_players.add(Player(name: name, positions: {..._selected}));
|
||||
_nameCtrl.clear();
|
||||
_selected.clear();
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('追加しました')),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('選手プール'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, _players),
|
||||
child: const Text('完了', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: _nameCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '選手名',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: allRoles.map((r) {
|
||||
final on = _selected.contains(r);
|
||||
return FilterChip(
|
||||
label: Text(r),
|
||||
selected: on,
|
||||
onSelected: (v) {
|
||||
setState(() {
|
||||
if (v) _selected.add(r);
|
||||
else _selected.remove(r);
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ElevatedButton(
|
||||
onPressed: _add,
|
||||
child: const Text('追加'),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Divider(),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _players.length,
|
||||
itemBuilder: (_, i) {
|
||||
final p = _players[i];
|
||||
return ListTile(
|
||||
title: Text(p.name),
|
||||
subtitle: Text(p.positions.join(', ')),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => setState(() => _players.removeAt(i)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
304
lib/screens/result_screen.dart
Normal file
304
lib/screens/result_screen.dart
Normal file
@@ -0,0 +1,304 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/formation_generator.dart';
|
||||
import '../data/formation_positions.dart';
|
||||
|
||||
class ResultScreen extends StatefulWidget {
|
||||
final ConstraintMode mode;
|
||||
final bool rareStyleOn;
|
||||
final String? selectedLeague;
|
||||
final String? selectedCardType;
|
||||
final List<Map<String, dynamic>> playerPool;
|
||||
|
||||
// チームスタイル重み
|
||||
final Map<String, int> teamStyleWeights;
|
||||
|
||||
const ResultScreen({
|
||||
super.key,
|
||||
required this.mode,
|
||||
required this.rareStyleOn,
|
||||
required this.selectedLeague,
|
||||
required this.selectedCardType,
|
||||
required this.playerPool,
|
||||
required this.teamStyleWeights,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ResultScreen> createState() => _ResultScreenState();
|
||||
}
|
||||
|
||||
class _ResultScreenState extends State<ResultScreen> {
|
||||
Map<String, dynamic>? _result;
|
||||
List<Map<String, String>> _visiblePlayers = [];
|
||||
bool _isAnimating = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_reroll();
|
||||
}
|
||||
|
||||
Future<void> _reroll() async {
|
||||
setState(() {
|
||||
_result = null;
|
||||
_visiblePlayers = [];
|
||||
_isAnimating = true;
|
||||
});
|
||||
|
||||
final res = FormationGenerator.generate(
|
||||
mode: widget.mode,
|
||||
rareStyleOn: widget.rareStyleOn,
|
||||
selectedLeague: widget.selectedLeague,
|
||||
selectedCardType: widget.selectedCardType,
|
||||
playerPool: widget.playerPool,
|
||||
teamStyleWeights: widget.teamStyleWeights,
|
||||
);
|
||||
|
||||
setState(() => _result = res);
|
||||
|
||||
final players = (res['positions'] as List).cast<dynamic>();
|
||||
for (int i = 0; i < players.length; i++) {
|
||||
await Future.delayed(const Duration(milliseconds: 140));
|
||||
setState(() {
|
||||
_visiblePlayers.add(Map<String, String>.from(players[i]));
|
||||
});
|
||||
}
|
||||
|
||||
setState(() => _isAnimating = false);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// タップでフルネーム表示
|
||||
// =========================
|
||||
void _showPlayerNameDialog(String fullName) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('選手名'),
|
||||
content: Text(
|
||||
fullName,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('閉じる'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ピッチ上は名字だけ(本家っぽい)
|
||||
String _displayName(String fullName) {
|
||||
final name = fullName.trim();
|
||||
if (name.isEmpty) return '';
|
||||
if (!name.contains(' ')) return name;
|
||||
return name.split(RegExp(r'\s+')).last;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// 色分け
|
||||
// =========================
|
||||
Color _getColor(String pos) {
|
||||
if (pos == 'GK') return Colors.orange;
|
||||
if (['CB', 'LSB', 'RSB'].contains(pos)) return Colors.blue;
|
||||
if (['DMF', 'CMF', 'OMF', 'LMF', 'RMF'].contains(pos)) return Colors.green;
|
||||
if (['CF', 'ST', 'RWG', 'LWG'].contains(pos)) return Colors.red;
|
||||
return Colors.grey;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// カード(タップ対応)
|
||||
// =========================
|
||||
Widget _buildPlayerCard(Map<String, String> data) {
|
||||
final pos = data['pos'] ?? '';
|
||||
final style = data['style'] ?? '';
|
||||
final fullName = data['name'] ?? '';
|
||||
|
||||
final color = _getColor(pos);
|
||||
|
||||
// poolモードは name が主役、それ以外は style が主役
|
||||
final middleText = (widget.mode == ConstraintMode.pool)
|
||||
? (fullName.isEmpty ? '未登録' : _displayName(fullName))
|
||||
: (_displayName(fullName));
|
||||
|
||||
final bottomText = (widget.mode == ConstraintMode.pool) ? '' : style;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: fullName.trim().isEmpty ? null : () => _showPlayerNameDialog(fullName),
|
||||
child: Container(
|
||||
width: 92,
|
||||
height: 74,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.white, width: 1),
|
||||
),
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
pos,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
middleText,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: (widget.mode == ConstraintMode.pool && fullName.isEmpty)
|
||||
? Colors.white70
|
||||
: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
bottomText,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// ピッチ配置
|
||||
// =========================
|
||||
Widget _buildPitch(String formation, List<Map<String, String>> roles) {
|
||||
final layout = getFormationLayout(formation);
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
final height = constraints.maxHeight;
|
||||
const cardW = 92.0;
|
||||
const cardH = 74.0;
|
||||
|
||||
final children = <Widget>[
|
||||
Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.green.shade800, Colors.green.shade700],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white54, width: 2),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
int index = 0;
|
||||
for (int row = 0; row < layout.length; row++) {
|
||||
for (int col = 0; col < layout[row].length; col++) {
|
||||
if (index >= roles.length) break;
|
||||
|
||||
final data = roles[index++];
|
||||
final xNorm = layout[row][col]['x'] ?? 0.5;
|
||||
final yNorm = layout[row][col]['y'] ?? 0.5;
|
||||
|
||||
children.add(
|
||||
Positioned(
|
||||
left: width * xNorm - cardW / 2,
|
||||
top: height * yNorm - cardH / 2,
|
||||
child: _buildPlayerCard(data),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return Stack(children: children);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final formation = _result?['formation'] as String?;
|
||||
final label = _result?['constraintLabel'] as String?;
|
||||
final teamStyle = _result?['teamStyle'] as String?;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.green[900],
|
||||
appBar: AppBar(
|
||||
title: const Text('結果'),
|
||||
backgroundColor: Colors.green[900],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
if (formation != null)
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
'フォーメーション: $formation',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (label != null)
|
||||
Text(label, style: const TextStyle(color: Colors.white70)),
|
||||
if (teamStyle != null)
|
||||
Text('チームスタイル: $teamStyle',
|
||||
style: const TextStyle(color: Colors.white70)),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'※カードをタップでフルネーム表示',
|
||||
style: TextStyle(color: Colors.white54, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: formation == null
|
||||
? const Center(
|
||||
child: Text(
|
||||
'抽選中…',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 16),
|
||||
),
|
||||
)
|
||||
: _buildPitch(formation, _visiblePlayers),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 16),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _isAnimating ? null : _reroll,
|
||||
icon: const Icon(Icons.shuffle),
|
||||
label: const Text('再抽選'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.green[900],
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
485
lib/services/formation_generator.dart
Normal file
485
lib/services/formation_generator.dart
Normal file
@@ -0,0 +1,485 @@
|
||||
import 'dart:math';
|
||||
import '../data/formation_positions.dart';
|
||||
import '../data/player_attributes.dart';
|
||||
import '../data/league_data.dart';
|
||||
import '../data/cardtype_data.dart';
|
||||
|
||||
enum ConstraintMode { playstyle, league, cardType, pool }
|
||||
|
||||
class FormationGenerator {
|
||||
static final Random _rand = Random();
|
||||
|
||||
/// playerPool:
|
||||
/// [{'name':'A', 'positions':['CF','ST']}, ...]
|
||||
static Map<String, dynamic> generate({
|
||||
required ConstraintMode mode,
|
||||
bool rareStyleOn = false,
|
||||
String? selectedLeague,
|
||||
String? selectedCardType,
|
||||
List<Map<String, dynamic>> playerPool = const [],
|
||||
Map<String, int>? teamStyleWeights,
|
||||
}) {
|
||||
final formation = formations[_rand.nextInt(formations.length)];
|
||||
final layout = getFormationLayout(formation);
|
||||
final roles = _assignRolesForFormation(formation, layout);
|
||||
final teamStyle = _pickTeamStyle(teamStyleWeights);
|
||||
|
||||
|
||||
// poolモードのときだけ名前を割り当てる(混ぜない)
|
||||
final names = (mode == ConstraintMode.pool)
|
||||
? _assignNamesByRole(roles: roles, pool: playerPool)
|
||||
: List.filled(roles.length, '');
|
||||
|
||||
final result = <Map<String, String>>[];
|
||||
|
||||
for (int i = 0; i < roles.length; i++) {
|
||||
final pos = roles[i];
|
||||
|
||||
String styleOrValue;
|
||||
switch (mode) {
|
||||
case ConstraintMode.playstyle:
|
||||
styleOrValue = _pickStyle(pos, rareStyleOn); // ←プレスタはそのまま
|
||||
break;
|
||||
|
||||
case ConstraintMode.league:
|
||||
// ランダムなら「ポジションごと」に抽選
|
||||
styleOrValue = (selectedLeague == null)
|
||||
? leagues[_rand.nextInt(leagues.length)]
|
||||
: selectedLeague;
|
||||
break;
|
||||
|
||||
case ConstraintMode.cardType:
|
||||
styleOrValue = (selectedCardType == null)
|
||||
? cardTypes[_rand.nextInt(cardTypes.length)]
|
||||
: selectedCardType;
|
||||
break;
|
||||
|
||||
case ConstraintMode.pool:
|
||||
// poolモードでは「style」は表示用に空(混ぜない)
|
||||
styleOrValue = '';
|
||||
break;
|
||||
}
|
||||
|
||||
result.add({
|
||||
'pos': pos,
|
||||
'style': styleOrValue,
|
||||
'name': names[i],
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
'formation': formation,
|
||||
'mode': mode.name,
|
||||
'constraintLabel': _constraintLabel(mode, selectedLeague, selectedCardType),
|
||||
'positions': result,
|
||||
'teamStyle':teamStyle
|
||||
};
|
||||
}
|
||||
|
||||
// =====================
|
||||
// チームスタイル抽選
|
||||
// =====================
|
||||
static const List<String> teamStyles = [
|
||||
'ポゼッション',
|
||||
'ショートカウンター',
|
||||
'ロングカウンター',
|
||||
'サイドアタック',
|
||||
'ロングボール',
|
||||
];
|
||||
|
||||
static String _pickTeamStyle(Map<String, int>? weights) {
|
||||
final w = weights ??
|
||||
{
|
||||
'ポゼッション': 50,
|
||||
'ショートカウンター': 50,
|
||||
'ロングカウンター': 50,
|
||||
'サイドアタック': 50,
|
||||
'ロングボール': 50,
|
||||
};
|
||||
|
||||
final entries = teamStyles
|
||||
.map((s) => MapEntry(s, w[s] ?? 0))
|
||||
.where((e) => e.value > 0)
|
||||
.toList();
|
||||
|
||||
if (entries.isEmpty) {
|
||||
return teamStyles[_rand.nextInt(teamStyles.length)];
|
||||
}
|
||||
|
||||
final total = entries.fold<int>(0, (sum, e) => sum + e.value);
|
||||
int r = _rand.nextInt(total);
|
||||
|
||||
for (final e in entries) {
|
||||
r -= e.value;
|
||||
if (r < 0) return e.key;
|
||||
}
|
||||
|
||||
return entries.last.key;
|
||||
}
|
||||
|
||||
|
||||
static String _constraintLabel(
|
||||
ConstraintMode mode,
|
||||
String? selectedLeague,
|
||||
String? selectedCardType,
|
||||
) {
|
||||
switch (mode) {
|
||||
case ConstraintMode.playstyle:
|
||||
return 'プレースタイル縛り';
|
||||
case ConstraintMode.league:
|
||||
return selectedLeague != null
|
||||
? 'リーグ縛り: $selectedLeague'
|
||||
: 'リーグ縛り: ランダム(ポジション別)';
|
||||
case ConstraintMode.cardType:
|
||||
return selectedCardType != null
|
||||
? 'カードタイプ縛り: $selectedCardType'
|
||||
: 'カードタイプ縛り: ランダム(ポジション別)';
|
||||
case ConstraintMode.pool:
|
||||
return '選手プール抽選';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// プール割り当て(完全一致)
|
||||
// ============================
|
||||
static List<String> _assignNamesByRole({
|
||||
required List<String> roles,
|
||||
required List<Map<String, dynamic>> pool,
|
||||
}) {
|
||||
final remaining = pool
|
||||
.map((p) => {
|
||||
'name': (p['name'] ?? '').toString().trim(),
|
||||
'positions': List<String>.from(p['positions'] ?? const <String>[]),
|
||||
})
|
||||
.where((p) => (p['name'] as String).isNotEmpty)
|
||||
.toList();
|
||||
|
||||
final assigned = <String>[];
|
||||
|
||||
for (final role in roles) {
|
||||
final candidates = remaining
|
||||
.where((p) => (p['positions'] as List<String>).contains(role))
|
||||
.toList();
|
||||
|
||||
if (candidates.isEmpty) {
|
||||
assigned.add('未登録');
|
||||
continue;
|
||||
}
|
||||
|
||||
final pick = candidates[_rand.nextInt(candidates.length)];
|
||||
assigned.add(pick['name'] as String);
|
||||
remaining.remove(pick);
|
||||
}
|
||||
return assigned;
|
||||
}
|
||||
|
||||
// ============================
|
||||
// プレスタ(触らない)
|
||||
// ============================
|
||||
static String _pickStyle(String pos, bool rareStyleOn) {
|
||||
final list = playerAttributes[pos];
|
||||
final baseList = (list == null || list.isEmpty) ? allPlayStyle : list;
|
||||
return _weightedRandomStyle(baseList, rareStyleOn);
|
||||
}
|
||||
|
||||
static String _weightedRandomStyle(List<String> styles, bool rareStyleOn) {
|
||||
const rareStyles = {
|
||||
'デコイラン',
|
||||
'ナンバー10',
|
||||
'ターゲットマン',
|
||||
'インナーラップサイドバック',
|
||||
'オーバーラップ',
|
||||
'インサイドレシーバー',
|
||||
};
|
||||
|
||||
if (rareStyleOn) {
|
||||
return styles[_rand.nextInt(styles.length)];
|
||||
}
|
||||
|
||||
final weights = <double>[];
|
||||
double total = 0;
|
||||
for (final s in styles) {
|
||||
final w = rareStyles.contains(s) ? 0.3 : 1.0;
|
||||
weights.add(w);
|
||||
total += w;
|
||||
}
|
||||
|
||||
final r = _rand.nextDouble() * total;
|
||||
double acc = 0;
|
||||
for (int i = 0; i < styles.length; i++) {
|
||||
acc += weights[i];
|
||||
if (r <= acc) return styles[i];
|
||||
}
|
||||
return styles.last;
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// フォメ + レイアウトに応じて役割を割り当て
|
||||
// =====================================
|
||||
static List<String> _assignRolesForFormation(
|
||||
String formation,
|
||||
List<List<Map<String, double>>> layout,
|
||||
) {
|
||||
final parts = formation.split('-').map(int.parse).toList();
|
||||
final int dfCount = parts[0];
|
||||
final int fwCount = parts.last;
|
||||
final bool isFourLineFormation = parts.length == 4; // 4-1-4-1, 4-2-3-1 など
|
||||
|
||||
// 中盤総人数(「4-4-2」なら 4、『4-2-3-1』なら 2+3=5)
|
||||
int mfPlayers = 0;
|
||||
if (parts.length > 2) {
|
||||
mfPlayers = parts.sublist(1, parts.length - 1).reduce((a, b) => a + b);
|
||||
}
|
||||
|
||||
final roles = <String>[];
|
||||
|
||||
int mfRowIndex = 0; // MF行の中での何列目か (0 = 2列目 = FW直下)
|
||||
|
||||
for (int row = 0; row < layout.length; row++) {
|
||||
final slots = layout[row].length;
|
||||
final isTop = row == 0;
|
||||
final isBottom = row == layout.length - 1;
|
||||
final isDfRow = row == layout.length - 2;
|
||||
|
||||
if (isTop) {
|
||||
// 一番上の行 → FW
|
||||
roles.addAll(_assignFW(fwCount));
|
||||
} else if (isBottom) {
|
||||
// 一番下の行 → GK(スロットすべて GK 扱いだが基本1人)
|
||||
for (int i = 0; i < slots; i++) {
|
||||
roles.add('GK');
|
||||
}
|
||||
} else if (isDfRow) {
|
||||
// DF行
|
||||
roles.addAll(_assignDF(dfCount));
|
||||
} else {
|
||||
// MF行
|
||||
roles.addAll(_assignMFRow(
|
||||
formation: formation,
|
||||
isFourLineFormation: isFourLineFormation,
|
||||
mfRowIndex: mfRowIndex,
|
||||
slots: slots,
|
||||
mfPlayers: mfPlayers,
|
||||
));
|
||||
mfRowIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
// =====================================
|
||||
static List<String> _assignFW(int count) {
|
||||
// 1トップ → LWG/RWG は出さない(CF/STだけ)
|
||||
if (count == 1) {
|
||||
return [_rand.nextDouble() < 0.8 ? 'CF' : 'ST'];
|
||||
}
|
||||
|
||||
// 2トップ → サイド禁止(CF/STだけ)
|
||||
if (count == 2) {
|
||||
final pool = <String>['CF', 'CF', 'CF', 'ST'];
|
||||
return List.generate(2, (_) => pool[_rand.nextInt(pool.length)]);
|
||||
}
|
||||
|
||||
// 3トップ → 左LWG / 中央CF or ST / 右RWG に固定
|
||||
if (count == 3) {
|
||||
return [
|
||||
'LWG',
|
||||
_rand.nextBool() ? 'CF' : 'ST',
|
||||
'RWG',
|
||||
];
|
||||
}
|
||||
|
||||
// 想定外 → とりあえずCFで埋める
|
||||
return List.filled(count, 'CF');
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// DF ロジック
|
||||
// =====================================
|
||||
static List<String> _assignDF(int count) {
|
||||
if (count == 5) {
|
||||
// 5バック
|
||||
return ['LSB', 'CB', 'CB', 'CB', 'RSB'];
|
||||
}
|
||||
if (count == 4) {
|
||||
// 4バック(3パターンから抽選)
|
||||
final patterns = [
|
||||
['LSB', 'CB', 'CB', 'RSB'],
|
||||
['LSB', 'CB', 'CB', 'CB'],
|
||||
['CB', 'CB', 'CB', 'RSB'],
|
||||
];
|
||||
return patterns[_rand.nextInt(patterns.length)];
|
||||
}
|
||||
if (count == 3) {
|
||||
// 3バック
|
||||
return ['CB', 'CB', 'CB'];
|
||||
}
|
||||
|
||||
// それ以外 → 全部CBで埋める
|
||||
return List.filled(count, 'CB');
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// MF行ごとの割り当て
|
||||
// =====================================
|
||||
static List<String> _assignMFRow({
|
||||
required String formation,
|
||||
required bool isFourLineFormation,
|
||||
required int mfRowIndex, // 0 = 最前線の一つ後ろ(2列目)
|
||||
required int slots, // そのMF行の人数(レイアウト上)
|
||||
required int mfPlayers, // 中盤総人数
|
||||
}) {
|
||||
final parts = formation.split('-').map(int.parse).toList();
|
||||
bool allowWideMF = false;
|
||||
|
||||
if (isFourLineFormation) {
|
||||
final int v2 = parts[1]; // 2列目MF
|
||||
final int v3 = parts[2]; // 3列目MF
|
||||
|
||||
// ★ 4列フォメの3列目(mfRowIndex == 1)には LMF/RMF を絶対出さない
|
||||
if (mfRowIndex == 1) {
|
||||
allowWideMF = false;
|
||||
} else {
|
||||
// 2列目(mfRowIndex == 0)のみチェック
|
||||
if (v2 + v3 >= 4 && mfRowIndex == 0 && slots >= 3) {
|
||||
allowWideMF = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 3列フォメ
|
||||
final int mid = parts[1];
|
||||
if (mid >= 4 && mfRowIndex == 0 && slots >= 3) {
|
||||
allowWideMF = true;
|
||||
}
|
||||
}
|
||||
|
||||
return _assignMFLogic(
|
||||
formation: formation,
|
||||
isFourLineFormation: isFourLineFormation,
|
||||
mfRowIndex: mfRowIndex,
|
||||
slots: slots,
|
||||
mfPlayers: mfPlayers,
|
||||
allowWideMF: allowWideMF,
|
||||
);
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// OMF / DMF の制約
|
||||
// =====================================
|
||||
static List<String> _applyMFConstraints(
|
||||
List<String> row, bool isFourLineFormation, int mfRowIndex) {
|
||||
// OMF制約
|
||||
row = row.map((p) {
|
||||
if (p == 'OMF') {
|
||||
if (!isFourLineFormation) return 'CMF';
|
||||
if (mfRowIndex != 0) return 'CMF';
|
||||
}
|
||||
return p;
|
||||
}).toList();
|
||||
|
||||
// DMF制約:4列フォメの2列目のみ禁止
|
||||
if (isFourLineFormation && mfRowIndex == 0) {
|
||||
row = row.map((p) => p == 'DMF' ? 'CMF' : p).toList();
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// MF ロジック本体
|
||||
// =====================================
|
||||
static List<String> _assignMFLogic({
|
||||
required String formation,
|
||||
required bool isFourLineFormation,
|
||||
required int mfRowIndex,
|
||||
required int slots,
|
||||
required int mfPlayers,
|
||||
required bool allowWideMF,
|
||||
}) {
|
||||
final r = _rand;
|
||||
final bool useWide = allowWideMF;
|
||||
|
||||
if (slots == 1) {
|
||||
final row = ['DMF'];
|
||||
return _applyMFConstraints(row, isFourLineFormation, mfRowIndex);
|
||||
}
|
||||
|
||||
if (useWide && slots >= 3) {
|
||||
List<String> row = List.filled(slots, 'CMF');
|
||||
row[0] = 'LMF';
|
||||
row[slots - 1] = 'RMF';
|
||||
|
||||
for (int i = 1; i < slots - 1; i++) {
|
||||
if (isFourLineFormation && mfRowIndex == 0) {
|
||||
final centerPool = ['OMF', 'OMF', 'OMF', 'CMF'];
|
||||
row[i] = centerPool[r.nextInt(centerPool.length)];
|
||||
} else if (isFourLineFormation) {
|
||||
final centerPool = ['DMF', 'DMF', 'DMF', 'CMF'];
|
||||
row[i] = centerPool[r.nextInt(centerPool.length)];
|
||||
} else {
|
||||
final centerPool = ['DMF', 'DMF', 'DMF', 'CMF'];
|
||||
row[i] = centerPool[r.nextInt(centerPool.length)];
|
||||
}
|
||||
}
|
||||
|
||||
return _applyMFConstraints(row, isFourLineFormation, mfRowIndex);
|
||||
}
|
||||
|
||||
if (slots == 2) {
|
||||
List<String> row;
|
||||
|
||||
if (isFourLineFormation) {
|
||||
if (mfRowIndex == 0) {
|
||||
final pool = ['OMF', 'OMF', 'OMF', 'CMF'];
|
||||
row = [
|
||||
pool[r.nextInt(pool.length)],
|
||||
pool[r.nextInt(pool.length)],
|
||||
];
|
||||
} else {
|
||||
final pool = ['DMF', 'DMF', 'DMF', 'CMF'];
|
||||
row = [
|
||||
pool[r.nextInt(pool.length)],
|
||||
pool[r.nextInt(pool.length)],
|
||||
];
|
||||
}
|
||||
} else {
|
||||
final pool = ['DMF', 'CMF', 'CMF'];
|
||||
row = [
|
||||
pool[r.nextInt(pool.length)],
|
||||
pool[r.nextInt(pool.length)],
|
||||
];
|
||||
}
|
||||
|
||||
return _applyMFConstraints(row, isFourLineFormation, mfRowIndex);
|
||||
}
|
||||
|
||||
if (slots == 3 && !useWide) {
|
||||
List<String> row;
|
||||
|
||||
if (isFourLineFormation) {
|
||||
if (mfRowIndex == 0) {
|
||||
final pool = ['OMF', 'OMF', 'CMF'];
|
||||
row = [
|
||||
pool[r.nextInt(pool.length)],
|
||||
pool[r.nextInt(pool.length)],
|
||||
pool[r.nextInt(pool.length)],
|
||||
];
|
||||
} else {
|
||||
row = ['DMF', 'DMF', 'CMF'];
|
||||
}
|
||||
} else {
|
||||
row = ['CMF', 'DMF', 'CMF'];
|
||||
}
|
||||
|
||||
return _applyMFConstraints(row, isFourLineFormation, mfRowIndex);
|
||||
}
|
||||
|
||||
List<String> row = List.filled(slots, 'CMF');
|
||||
final center = slots ~/ 2;
|
||||
row[center] = 'DMF';
|
||||
if (center - 1 >= 0) row[center - 1] = 'DMF';
|
||||
if (center + 1 < slots) row[center + 1] = 'DMF';
|
||||
return _applyMFConstraints(row, isFourLineFormation, mfRowIndex);
|
||||
}
|
||||
}
|
||||
11
lib/state/player_pool.dart
Normal file
11
lib/state/player_pool.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import '../models/player.dart';
|
||||
|
||||
class PlayerPool {
|
||||
final List<Player> players;
|
||||
|
||||
PlayerPool({List<Player>? players}) : players = players ?? [];
|
||||
|
||||
void add(Player p) => players.add(p);
|
||||
void removeAt(int index) => players.removeAt(index);
|
||||
void clear() => players.clear();
|
||||
}
|
||||
Reference in New Issue
Block a user