Initial commit with gitignore

This commit is contained in:
2026-01-13 14:11:59 +09:00
parent d01c0b781a
commit b302e25132
143 changed files with 7030 additions and 0 deletions

View 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),
),
),
],
),
);
}
}

View 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)),
),
);
},
),
)
],
),
),
);
}
}

View 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),
),
),
),
),
],
),
);
}
}