305 lines
8.4 KiB
Dart
305 lines
8.4 KiB
Dart
import 'dart:async';
|
||
import 'dart:math' as math;
|
||
|
||
import 'package:flutter/material.dart';
|
||
|
||
void main() {
|
||
runApp(const StarTaxiApp());
|
||
}
|
||
|
||
class StarTaxiApp extends StatelessWidget {
|
||
const StarTaxiApp({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MaterialApp(
|
||
debugShowCheckedModeBanner: false,
|
||
title: 'Star Taxi – APK Test',
|
||
theme: ThemeData.dark(useMaterial3: true),
|
||
home: const StarTaxiDemo(),
|
||
);
|
||
}
|
||
}
|
||
|
||
class StarTaxiDemo extends StatefulWidget {
|
||
const StarTaxiDemo({super.key});
|
||
|
||
@override
|
||
State<StarTaxiDemo> createState() => _StarTaxiDemoState();
|
||
}
|
||
|
||
class _StarTaxiDemoState extends State<StarTaxiDemo> {
|
||
static const int lanes = 3;
|
||
static const double playerY = 0.86;
|
||
|
||
final _rng = math.Random();
|
||
final List<_Obstacle> _obstacles = [];
|
||
|
||
Timer? _loop;
|
||
double _lane = 1;
|
||
double _speed = 0.010;
|
||
int _score = 0;
|
||
int _best = 0;
|
||
bool _running = true;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_spawnInitial();
|
||
_loop = Timer.periodic(const Duration(milliseconds: 16), (_) => _tick());
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_loop?.cancel();
|
||
super.dispose();
|
||
}
|
||
|
||
void _spawnInitial() {
|
||
_obstacles.clear();
|
||
for (var i = 0; i < 5; i++) {
|
||
_obstacles.add(_Obstacle(
|
||
lane: _rng.nextInt(lanes).toDouble(),
|
||
y: -0.8 - i * 0.45,
|
||
));
|
||
}
|
||
}
|
||
|
||
void _tick() {
|
||
if (!_running) return;
|
||
|
||
setState(() {
|
||
_speed = (_speed + 0.00001).clamp(0.010, 0.03);
|
||
_score += 1;
|
||
|
||
for (final o in _obstacles) {
|
||
o.y += _speed;
|
||
if (o.y > 1.15) {
|
||
o.y = -0.9 - _rng.nextDouble() * 0.8;
|
||
o.lane = _rng.nextInt(lanes).toDouble();
|
||
}
|
||
}
|
||
|
||
for (final o in _obstacles) {
|
||
if ((o.y - playerY).abs() < 0.085 && (o.lane - _lane).abs() < 0.1) {
|
||
_running = false;
|
||
_best = math.max(_best, _score);
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
void _move(int dir) {
|
||
if (!_running) return;
|
||
setState(() {
|
||
_lane = (_lane + dir).clamp(0, lanes - 1).toDouble();
|
||
});
|
||
}
|
||
|
||
void _restart() {
|
||
setState(() {
|
||
_score = 0;
|
||
_speed = 0.010;
|
||
_lane = 1;
|
||
_running = true;
|
||
_spawnInitial();
|
||
});
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
body: SafeArea(
|
||
child: Column(
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.all(12),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text('Score: $_score', style: const TextStyle(fontSize: 20)),
|
||
Text('Best: $_best', style: const TextStyle(fontSize: 18)),
|
||
],
|
||
),
|
||
),
|
||
Expanded(
|
||
child: AspectRatio(
|
||
aspectRatio: 9 / 16,
|
||
child: Container(
|
||
margin: const EdgeInsets.all(10),
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(16),
|
||
gradient: const LinearGradient(
|
||
begin: Alignment.topCenter,
|
||
end: Alignment.bottomCenter,
|
||
colors: [Color(0xFF0B1021), Color(0xFF1B2348)],
|
||
),
|
||
),
|
||
child: CustomPaint(
|
||
painter: _RoadPainter(
|
||
lane: _lane,
|
||
obstacles: _obstacles,
|
||
),
|
||
child: !_running
|
||
? Center(
|
||
child: Container(
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: Colors.black.withValues(alpha: 0.65),
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
const Text('Crash! 💥',
|
||
style: TextStyle(fontSize: 26)),
|
||
const SizedBox(height: 8),
|
||
ElevatedButton(
|
||
onPressed: _restart,
|
||
child: const Text('Nochmal'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
)
|
||
: null,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: FilledButton.icon(
|
||
onPressed: () => _move(-1),
|
||
icon: const Icon(Icons.arrow_left),
|
||
label: const Text('Links'),
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: FilledButton.icon(
|
||
onPressed: () => _move(1),
|
||
icon: const Icon(Icons.arrow_right),
|
||
label: const Text('Rechts'),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const Padding(
|
||
padding: EdgeInsets.only(bottom: 10),
|
||
child: Text('Star Taxi – kleines 3D-Arcade APK Testbuild'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _RoadPainter extends CustomPainter {
|
||
final double lane;
|
||
final List<_Obstacle> obstacles;
|
||
|
||
const _RoadPainter({required this.lane, required this.obstacles});
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
final road = Paint()..color = const Color(0xFF2A2F40);
|
||
final line = Paint()
|
||
..color = const Color(0xAAFFFFFF)
|
||
..strokeWidth = 2;
|
||
|
||
final topWidth = size.width * 0.35;
|
||
final bottomWidth = size.width * 0.95;
|
||
|
||
final roadPath = Path()
|
||
..moveTo((size.width - topWidth) / 2, 0)
|
||
..lineTo((size.width + topWidth) / 2, 0)
|
||
..lineTo((size.width + bottomWidth) / 2, size.height)
|
||
..lineTo((size.width - bottomWidth) / 2, size.height)
|
||
..close();
|
||
|
||
canvas.drawPath(roadPath, road);
|
||
|
||
for (int i = 1; i < 3; i++) {
|
||
final t = i / 3;
|
||
final xTop = size.width / 2 + (t - 0.5) * topWidth;
|
||
final xBottom = size.width / 2 + (t - 0.5) * bottomWidth;
|
||
canvas.drawLine(Offset(xTop, 0), Offset(xBottom, size.height), line);
|
||
}
|
||
|
||
for (double y = 0; y < 1.1; y += 0.12) {
|
||
final py = y * size.height;
|
||
final w = _roadWidthAtY(size, py, topWidth, bottomWidth);
|
||
canvas.drawLine(
|
||
Offset(size.width / 2 - w * 0.04, py),
|
||
Offset(size.width / 2 + w * 0.04, py),
|
||
Paint()
|
||
..color = Colors.white.withValues(alpha: 0.35)
|
||
..strokeWidth = 2,
|
||
);
|
||
}
|
||
|
||
for (final o in obstacles) {
|
||
_drawCar(
|
||
canvas,
|
||
size,
|
||
lane: o.lane,
|
||
yNorm: o.y,
|
||
color: const Color(0xFFE26D5A),
|
||
);
|
||
}
|
||
|
||
_drawCar(
|
||
canvas,
|
||
size,
|
||
lane: lane,
|
||
yNorm: _StarTaxiDemoState.playerY,
|
||
color: const Color(0xFF3DDC97),
|
||
);
|
||
}
|
||
|
||
void _drawCar(Canvas canvas, Size size,
|
||
{required double lane, required double yNorm, required Color color}) {
|
||
final y = yNorm * size.height;
|
||
final wRoad = _roadWidthAtY(size, y, size.width * 0.35, size.width * 0.95);
|
||
final laneWidth = wRoad / 3;
|
||
final left = size.width / 2 - wRoad / 2 + lane * laneWidth + laneWidth * 0.15;
|
||
final width = laneWidth * 0.7;
|
||
final height = width * 0.9;
|
||
|
||
final rect = RRect.fromRectAndRadius(
|
||
Rect.fromLTWH(left, y - height / 2, width, height),
|
||
const Radius.circular(8),
|
||
);
|
||
canvas.drawRRect(rect, Paint()..color = color);
|
||
canvas.drawRRect(
|
||
rect,
|
||
Paint()
|
||
..style = PaintingStyle.stroke
|
||
..strokeWidth = 1.2
|
||
..color = Colors.white.withValues(alpha: 0.65),
|
||
);
|
||
}
|
||
|
||
double _roadWidthAtY(Size size, double y, double topWidth, double bottomWidth) {
|
||
final t = (y / size.height).clamp(0.0, 1.0);
|
||
return topWidth + (bottomWidth - topWidth) * t;
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(covariant _RoadPainter oldDelegate) => true;
|
||
}
|
||
|
||
class _Obstacle {
|
||
_Obstacle({required this.lane, required this.y});
|
||
|
||
double lane;
|
||
double y;
|
||
}
|