first commit

This commit is contained in:
2025-11-25 17:57:43 +09:00
commit 514b49f7d9
133 changed files with 5320 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
// lib/game/game_instance.dart
/// 앱 실행 시 가장 먼저 생성되는 싱글톤 진입점.
/// 게임의 핵심 데이터를 로드하고 관리한다.
class GameInstance {
// 싱글톤 인스턴스
static final GameInstance _instance = GameInstance._internal();
// 팩토리 생성자를 통해 싱글톤 인스턴스를 반환한다.
factory GameInstance() {
return _instance;
}
// 내부 생성자. 외부에서 직접 인스턴스 생성을 막는다.
GameInstance._internal();
bool _isInitialized = false;
/// 게임이 초기화되었는지 여부를 반환한다.
bool get isInitialized => _isInitialized;
/// 게임 초기화 로직.
/// 필요한 게임 데이터를 로드하고 시스템을 설정한다.
Future<void> initialize() async {
if (_isInitialized) {
print('GameInstance already initialized.');
return;
}
print('Initializing GameInstance...');
// TODO: 여기에 실제 게임 데이터 로드 및 초기화 로직 구현
// 예: 몬스터 데이터, 아이템 데이터, 플레이어 초기 데이터 등 로드
await Future.delayed(const Duration(seconds: 1)); // 초기화 지연 시뮬레이션
_isInitialized = true;
print('GameInstance initialized successfully.');
}
// TODO: 게임 전역에서 공유될 데이터 및 유틸리티 메서드 추가
}
+92
View File
@@ -0,0 +1,92 @@
// lib/game/game_manager.dart
import 'package:flutter/foundation.dart'; // ChangeNotifier를 사용하기 위해 필요
import 'package:game_test/game/model/entity.dart';
import 'package:game_test/game/game_instance.dart';
/// 게임의 상태(State)와 흐름을 관리하는 지휘자.
/// ChangeNotifier를 상속받아 UI에 게임 상태 변경을 알릴 수 있다.
class GameManager extends ChangeNotifier {
Player? _player;
List<Enemy> _currentEnemies = [];
GameManager() {
_init();
}
void _init() async {
// GameInstance가 초기화되었는지 확인
if (!GameInstance().isInitialized) {
await GameInstance().initialize();
}
// TODO: 게임 시작 시 필요한 초기화 로직 구현
// 예: 새로운 플레이어 생성, 첫 스테이지 몬스터 로드 등
_player = Player(id: 'player_001', name: '용감한 검투사', baseHp: 100);
_currentEnemies = [
Enemy(id: 'goblin_001', name: '고블린', baseHp: 50),
Enemy(id: 'goblin_002', name: '고블린', baseHp: 55),
];
print('GameManager initialized. Player: ${_player?.name}, Enemies: ${_currentEnemies.length}');
notifyListeners(); // UI에 초기 상태 변경 알림
}
/// 현재 플레이어를 반환한다.
Player? get player => _player;
/// 현재 전투 중인 적 리스트를 반환한다.
List<Enemy> get currentEnemies => _currentEnemies;
/// 플레이어의 턴 로직.
/// 선택된 행동(공격, 방어 등)과 강도(Risk)에 따라 게임 상태를 변경한다.
void playerTurn({required String action, required double risk}) {
// TODO: 행동 및 강도에 따른 로직 구현
print('Player performs $action with risk $risk');
// 예시: 간단한 공격 로직
if (action == 'attack' && _currentEnemies.isNotEmpty) {
final targetEnemy = _currentEnemies.first; // 첫 번째 적 공격
double damage = _player!.attack.value * risk; // 플레이어의 공격력과 강도에 따라 피해량 계산
targetEnemy.takeDamage(damage);
print('${_player?.name} attacked ${targetEnemy.name} for ${damage.toInt()} damage.');
print('${targetEnemy.name} HP: ${targetEnemy.hp.value.toInt()}');
if (!targetEnemy.isAlive) {
_currentEnemies.remove(targetEnemy);
print('${targetEnemy.name} defeated!');
}
}
notifyListeners(); // 게임 상태 변경 알림
// TODO: 적 턴 시작 로직 호출
if (_currentEnemies.isNotEmpty) {
_enemyTurn();
} else {
print('All enemies defeated! Moving to next stage.');
// TODO: 다음 스테이지 로직 구현
}
}
/// 적의 턴 로직.
void _enemyTurn() {
print('Enemy turn...');
for (var enemy in _currentEnemies) {
if (enemy.isAlive && _player != null) {
// TODO: 적 AI 로직 구현
double damageToPlayer = enemy.attack.value; // 적의 공격력 사용
_player!.takeDamage(damageToPlayer);
print('${enemy.name} attacked ${_player!.name} for ${damageToPlayer.toInt()} damage.');
print('${_player!.name} HP: ${_player!.hp.value.toInt()}');
if (!_player!.isAlive) {
print('Game Over!');
// TODO: 게임 오버 로직 구현
break;
}
}
}
notifyListeners(); // 게임 상태 변경 알림
}
// TODO: 추가적인 게임 흐름 관리 메서드 (예: 아이템 사용, 스킬 사용, 스테이지 전환 등)
}
+126
View File
@@ -0,0 +1,126 @@
// lib/game/model/entity.dart
import 'package:game_test/game/model/stat.dart';
import 'package:game_test/game/model/item.dart'; // Add this import
/// 모든 게임 엔티티의 기본 클래스.
/// 고유 ID와 이름을 가진다.
abstract class BaseEntity {
final String id;
String name;
BaseEntity({required this.id, required this.name});
@override
String toString() => '$name (ID: $id)';
}
/// 생명력을 가진 엔티티 (플레이어, 적 등)의 추상 클래스.
/// BaseEntity를 상속받고, 체력(HP)과 스탯 맵을 포함한다.
abstract class LivingEntity extends BaseEntity {
Stat hp; // Health Points
final Map<String, Stat> stats = {}; // 다양한 스탯들을 관리하는 맵
LivingEntity({
required super.id,
required super.name,
required double baseHp,
}) : hp = Stat(baseValue: baseHp);
/// 특정 스탯을 추가한다.
void addStat(String statName, Stat stat) {
stats[statName] = stat;
}
/// 특정 스탯을 가져온다.
Stat? getStat(String statName) {
return stats[statName];
}
/// 엔티티가 살아있는지 여부를 반환한다.
bool get isAlive => hp.value > 0;
/// 엔티티에게 피해를 입힌다.
void takeDamage(double amount) {
hp.baseValue -= amount; // HP는 baseValue를 직접 감소시키는 것으로 처리.
if (hp.baseValue < 0) {
hp.baseValue = 0;
}
}
/// 엔티티를 치유한다.
void heal(double amount) {
hp.baseValue += amount;
// TODO: 최대 HP 제한 로직 추가 필요
}
@override
String toString() {
return '${super.toString()}, HP: ${hp.value.toInt()}/${hp.baseValue.toInt()}';
}
}
/// 플레이어 엔티티 클래스.
/// LivingEntity를 상속받으며 플레이어 특유의 로직을 추가할 수 있다.
class Player extends LivingEntity {
// 장비 슬롯 맵
final Map<EquipmentSlot, Weapon> _equippedWeapons = {};
/// 플레이어의 공격 스탯.
Stat attack;
Player({
required super.id,
required super.name,
required super.baseHp,
}) : attack = Stat(baseValue: 0.0) { // 공격 스탯 초기화
// 시작 시 맨손 무장
equipWeapon(Weapon.unArmed);
}
/// 현재 장비된 무기를 반환한다.
Weapon? get equippedWeapon => _equippedWeapons[EquipmentSlot.mainHand];
/// 무기를 장비한다.
/// 기존에 해당 슬롯에 장비된 무기가 있다면 해제하고 새로운 무기를 장비한다.
void equipWeapon(Weapon newWeapon) {
// 기존 무기가 있다면 해제
final currentWeapon = _equippedWeapons[newWeapon.slot];
if (currentWeapon != null) {
attack.removeModifier(currentWeapon.attackModifier);
}
// 새로운 무기 장비 및 수정자 적용
_equippedWeapons[newWeapon.slot] = newWeapon;
attack.addModifier(newWeapon.attackModifier);
}
/// 장비된 무기를 해제한다.
/// 맨손 상태로 돌아간다.
void unequipWeapon(EquipmentSlot slot) {
final currentWeapon = _equippedWeapons[slot];
if (currentWeapon != null && currentWeapon != Weapon.unArmed) {
attack.removeModifier(currentWeapon.attackModifier);
_equippedWeapons.remove(slot);
// 맨손 상태로 복귀
equipWeapon(Weapon.unArmed);
}
}
// TODO: 플레이어 고유의 인벤토리, 장비, 스킬 등의 시스템 추가
}
/// 적 엔티티 클래스.
/// LivingEntity를 상속받으며 적 특유의 로직을 추가할 수 있다.
class Enemy extends LivingEntity {
Stat attack; // 적도 공격 스탯을 가질 수 있도록 추가
Enemy({
required super.id,
required super.name,
required super.baseHp,
double baseAttack = 5.0, // 기본 공격력 설정
}) : attack = Stat(baseValue: baseAttack);
// TODO: 적 고유의 AI, 드롭 아이템 등의 시스템 추가
}
+55
View File
@@ -0,0 +1,55 @@
// lib/game/model/item.dart
import 'package:game_test/game/model/stat.dart';
/// 장비할 수 있는 슬롯의 종류.
enum EquipmentSlot {
mainHand,
offHand,
head,
chest,
legs,
feet,
accessory,
}
/// 모든 게임 아이템의 기본 클래스.
/// ID, 이름, 설명을 가진다.
abstract class Item {
final String id;
final String name;
final String description;
Item({
required this.id,
required this.name,
this.description = '',
});
@override
String toString() => name;
}
/// 무기 아이템 클래스.
/// 공격 스탯에 영향을 주는 수정자를 포함할 수 있다.
class Weapon extends Item {
final Modifier attackModifier;
final EquipmentSlot slot;
Weapon({
required super.id,
required super.name,
super.description,
required this.attackModifier,
this.slot = EquipmentSlot.mainHand,
});
/// 플레이어가 무장하지 않았을 때의 기본 무기.
/// 베이스 공격력 1을 제공한다.
static Weapon get unArmed => Weapon(
id: 'unarmed_weapon',
name: '맨주먹',
description: '아무것도 장비하지 않은 상태의 공격.',
attackModifier: Modifier(type: ModifierType.flat, value: 1),
);
}
+111
View File
@@ -0,0 +1,111 @@
// lib/game/model/stat.dart
/// 스탯에 적용될 수 있는 수정자(Modifier)의 타입 정의.
/// Flat: 기본 값에 직접 더해지는 값.
/// Percent: 기본 값에 비율로 곱해지는 값.
enum ModifierType {
flat,
percent,
}
/// 스탯 수정자 클래스.
/// 특정 스탯에 적용되어 최종 값을 변경한다.
class Modifier {
final ModifierType type;
final double value; // Flat 값 또는 Percent 비율 (예: 0.10 for 10%)
Modifier({required this.type, required this.value});
@override
String toString() {
String sign = value >= 0 ? '+' : '';
if (type == ModifierType.flat) {
return '$sign${value.toInt()}';
} else {
return '$sign${(value * 100).toInt()}%';
}
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Modifier &&
runtimeType == other.runtimeType &&
type == other.type &&
value == other.value;
@override
int get hashCode => type.hashCode ^ value.hashCode;
}
/// 게임 내 모든 엔티티의 스탯을 표현하는 클래스.
/// 기본 값(BaseValue)과 여러 수정자(Modifier)들을 조합하여 최종 값(Value)을 계산한다.
class Stat {
double _baseValue;
final List<Modifier> _modifiers = [];
Stat({required double baseValue}) : _baseValue = baseValue {
_recalculateValue();
}
/// 현재 스탯의 기본 값.
double get baseValue => _baseValue;
/// 기본 값을 설정한다. 이 경우 최종 값도 다시 계산된다.
set baseValue(double newValue) {
_baseValue = newValue;
_recalculateValue();
}
/// 모든 수정자들이 적용된 최종 스탯 값.
/// 이 값은 기본 값이나 수정자가 변경될 때마다 자동으로 다시 계산된다.
double _value = 0.0;
double get value => _value;
/// 수정자를 추가하고 최종 스탯 값을 다시 계산한다.
void addModifier(Modifier modifier) {
_modifiers.add(modifier);
_recalculateValue();
}
/// 특정 수정자를 제거하고 최종 스탯 값을 다시 계산한다.
void removeModifier(Modifier modifier) {
_modifiers.remove(modifier);
_recalculateValue();
}
/// 모든 수정자를 제거하고 최종 스탯 값을 다시 계산한다.
void clearModifiers() {
_modifiers.clear();
_recalculateValue();
}
/// 기본 값과 모든 수정자들을 바탕으로 최종 스탯 값을 계산한다.
/// Flat 수정자가 먼저 적용된 후 Percent 수정자가 적용된다.
void _recalculateValue() {
double tempValue = _baseValue;
double percentAdditive = 0.0;
// Flat 수정자 먼저 적용
for (var modifier in _modifiers) {
if (modifier.type == ModifierType.flat) {
tempValue += modifier.value;
}
}
// Percent 수정자 값 합산 (예: 10% + 5% = 15%)
for (var modifier in _modifiers) {
if (modifier.type == ModifierType.percent) {
percentAdditive += modifier.value;
}
}
// 합산된 Percent 수정자 적용
_value = tempValue * (1 + percentAdditive);
}
@override
String toString() {
return 'Stat(base: $_baseValue, final: $_value, modifiers: ${_modifiers.length})';
}
}
+122
View File
@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}