From d3fd9680c22ddee0128deedc1f5194be5993f72a Mon Sep 17 00:00:00 2001 From: Horoli Date: Thu, 4 Dec 2025 18:59:25 +0900 Subject: [PATCH] update --- lib/game/data/item_table.dart | 68 +++++++++++++-------------- lib/game/enums.dart | 1 + lib/game/model/entity.dart | 16 +++++++ lib/game/model/item.dart | 4 +- lib/game/model/stat_modifier.dart | 41 ++++++++++++++++ lib/providers/battle_provider.dart | 15 ++++-- lib/screens/battle_screen.dart | 11 +++-- lib/screens/inventory_screen.dart | 6 +++ prompt/00_project_context_restore.md | 14 ++++++ prompt/38_permanent_stat_modifiers.md | 34 ++++++++++++++ prompt/39_luck_system.md | 39 +++++++++++++++ 11 files changed, 206 insertions(+), 43 deletions(-) create mode 100644 lib/game/model/stat_modifier.dart create mode 100644 prompt/38_permanent_stat_modifiers.md create mode 100644 prompt/39_luck_system.md diff --git a/lib/game/data/item_table.dart b/lib/game/data/item_table.dart index 6fd5456..0427176 100644 --- a/lib/game/data/item_table.dart +++ b/lib/game/data/item_table.dart @@ -7,74 +7,70 @@ class ItemTemplate { final String id; final String name; final String description; - final int baseAtk; - final int baseHp; - final int baseArmor; + final int atkBonus; + final int hpBonus; + final int armorBonus; final EquipmentSlot slot; final List effects; final int price; final String? image; + final int luck; const ItemTemplate({ required this.id, required this.name, required this.description, - this.baseAtk = 0, - this.baseHp = 0, - this.baseArmor = 0, + required this.atkBonus, + required this.hpBonus, + required this.armorBonus, required this.slot, - this.effects = const [], - this.price = 0, + required this.effects, + required this.price, this.image, + this.luck = 0, }); factory ItemTemplate.fromJson(Map json) { + var effectsList = []; + if (json['effects'] != null) { + effectsList = (json['effects'] as List) + .map((e) => ItemEffect.fromJson(e)) + .toList(); + } + return ItemTemplate( - id: - json['id'] ?? - json['name'], // Fallback to name if id is missing (for backward compatibility during dev) + id: json['id'], name: json['name'], description: json['description'], - baseAtk: json['baseAtk'] ?? 0, - baseHp: json['baseHp'] ?? 0, - baseArmor: json['baseArmor'] ?? 0, + atkBonus: json['atkBonus'] ?? 0, + hpBonus: json['hpBonus'] ?? 0, + armorBonus: json['armorBonus'] ?? 0, slot: EquipmentSlot.values.firstWhere((e) => e.name == json['slot']), - effects: - (json['effects'] as List?) - ?.map((e) => ItemEffect.fromJson(e)) - .toList() ?? - [], - price: json['price'] ?? 0, + effects: effectsList, + price: json['price'] ?? 10, image: json['image'], + luck: json['luck'] ?? 0, ); } - // Create an instance of Item based on this template, optionally scaling with stage Item createItem({int stage = 1}) { - // Simple scaling logic: add stage-1 to relevant stats - // You can make this more complex (multiplier, tiering, etc.) - int scaledAtk = baseAtk > 0 ? baseAtk + (stage - 1) : 0; - int scaledHp = baseHp > 0 ? baseHp + (stage - 1) * 5 : 0; - int scaledArmor = baseArmor > 0 ? baseArmor + (stage - 1) : 0; - - // Use fixed price from template - int finalPrice = price; - // Optional: Increase price if stage > 1 (e.g. +10% per stage) - if (stage > 1) { - finalPrice = (price * (1 + (stage - 1) * 0.1)).toInt(); - } + // Scale stats based on stage + int scaledAtk = (atkBonus * (1 + (stage - 1) * 0.1)).toInt(); + int scaledHp = (hpBonus * (1 + (stage - 1) * 0.1)).toInt(); + int scaledArmor = (armorBonus * (1 + (stage - 1) * 0.1)).toInt(); return Item( id: id, - name: "$name${stage > 1 ? ' +${stage - 1}' : ''}", // Append +1, +2 etc. + name: "$name${stage > 1 ? ' +${stage - 1}' : ''}", description: description, atkBonus: scaledAtk, hpBonus: scaledHp, armorBonus: scaledArmor, slot: slot, - effects: effects, // Pass the effects to the Item - price: finalPrice, + effects: effects, + price: price, image: image, + luck: luck, ); } } diff --git a/lib/game/enums.dart b/lib/game/enums.dart index b57b9ff..0125ba0 100644 --- a/lib/game/enums.dart +++ b/lib/game/enums.dart @@ -33,3 +33,4 @@ enum EquipmentSlot { weapon, armor, shield, accessory } enum DamageType { normal, bleed, vulnerable } +enum StatType { maxHp, atk, defense } diff --git a/lib/game/model/entity.dart b/lib/game/model/entity.dart index 00d8da1..3cf9efe 100644 --- a/lib/game/model/entity.dart +++ b/lib/game/model/entity.dart @@ -1,5 +1,6 @@ import 'item.dart'; import 'status_effect.dart'; +import 'stat_modifier.dart'; import '../enums.dart'; class Character { @@ -19,6 +20,9 @@ class Character { // Active status effects List statusEffects = []; + // Permanent stat modifiers (e.g. Ascension, Potions) + List permanentModifiers = []; + Character({ required this.name, int? hp, @@ -66,6 +70,14 @@ class Character { statusEffects.removeWhere((e) => e.duration <= 0); } + void addPermanentModifier(PermanentStatModifier modifier) { + permanentModifiers.add(modifier); + } + + void removePermanentModifier(String id) { + permanentModifiers.removeWhere((m) => m.id == id); + } + /// Helper to check if character has a specific status bool hasStatus(StatusEffectType type) { return statusEffects.any((e) => e.type == type); @@ -86,6 +98,10 @@ class Character { return baseDefense + bonus; } + int get totalLuck { + return equipment.values.fold(0, (sum, item) => sum + item.luck); + } + bool get isDead => hp <= 0; // Adds an item to inventory, returns true if successful, false if inventory is full diff --git a/lib/game/model/item.dart b/lib/game/model/item.dart index bb139c2..bf20229 100644 --- a/lib/game/model/item.dart +++ b/lib/game/model/item.dart @@ -46,8 +46,9 @@ class Item { final List effects; // Status effects this item can inflict final int price; // New: Sell/Buy value final String? image; // New: Image path + final int luck; // Success rate bonus (e.g. 5 = 5%) - Item({ + const Item({ required this.id, required this.name, required this.description, @@ -58,6 +59,7 @@ class Item { this.effects = const [], // Default to no effects this.price = 0, this.image, + this.luck = 0, }); String get typeName { diff --git a/lib/game/model/stat_modifier.dart b/lib/game/model/stat_modifier.dart new file mode 100644 index 0000000..93e3a0c --- /dev/null +++ b/lib/game/model/stat_modifier.dart @@ -0,0 +1,41 @@ +import '../enums.dart'; + +class PermanentStatModifier { + final String id; + final String name; + final String description; + final StatType statType; + final ModifierType type; + final double value; + + const PermanentStatModifier({ + required this.id, + required this.name, + required this.description, + required this.statType, + required this.type, + required this.value, + }); + + factory PermanentStatModifier.fromJson(Map json) { + return PermanentStatModifier( + id: json['id'], + name: json['name'], + description: json['description'], + statType: StatType.values.firstWhere((e) => e.name == json['statType']), + type: ModifierType.values.firstWhere((e) => e.name == json['type']), + value: json['value'].toDouble(), + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'description': description, + 'statType': statType.name, + 'type': type.name, + 'value': value, + }; + } +} diff --git a/lib/providers/battle_provider.dart b/lib/providers/battle_provider.dart index dedf429..f993755 100644 --- a/lib/providers/battle_provider.dart +++ b/lib/providers/battle_provider.dart @@ -277,15 +277,24 @@ class BattleProvider with ChangeNotifier { switch (risk) { case RiskLevel.safe: - success = random.nextDouble() < 1.0; // 100% + // Safe: 100% base chance + luck + double chance = 1.0 + (player.totalLuck / 100.0); + if (chance > 1.0) chance = 1.0; + success = random.nextDouble() < chance; efficiency = 0.5; // 50% break; case RiskLevel.normal: - success = random.nextDouble() < 0.8; // 80% + // Normal: 80% base chance + luck + double chance = 0.8 + (player.totalLuck / 100.0); + if (chance > 1.0) chance = 1.0; + success = random.nextDouble() < chance; efficiency = 1.0; // 100% break; case RiskLevel.risky: - success = random.nextDouble() < 0.4; // 40% + // Risky: 40% base chance + luck + double chance = 0.4 + (player.totalLuck / 100.0); + if (chance > 1.0) chance = 1.0; + success = random.nextDouble() < chance; efficiency = 2.0; // 200% break; } diff --git a/lib/screens/battle_screen.dart b/lib/screens/battle_screen.dart index fea7ceb..12ce5bc 100644 --- a/lib/screens/battle_screen.dart +++ b/lib/screens/battle_screen.dart @@ -274,18 +274,23 @@ class _BattleScreenState extends State { : "Armor"; String successRate = ""; + double baseChance = 0.0; switch (risk) { case RiskLevel.safe: - successRate = "100%"; + baseChance = 1.0; break; case RiskLevel.normal: - successRate = "80%"; + baseChance = 0.8; break; case RiskLevel.risky: - successRate = "40%"; + baseChance = 0.4; break; } + double finalChance = baseChance + (player.totalLuck / 100.0); + if (finalChance > 1.0) finalChance = 1.0; + successRate = "${(finalChance * 100).toInt()}%"; + infoText = "Success: $successRate, Eff: ${(efficiency * 100).toInt()}% ($expectedValue $valueUnit)"; diff --git a/lib/screens/inventory_screen.dart b/lib/screens/inventory_screen.dart index 0294167..806bd7c 100644 --- a/lib/screens/inventory_screen.dart +++ b/lib/screens/inventory_screen.dart @@ -47,6 +47,11 @@ class InventoryScreen extends StatelessWidget { "${player.gold} G", color: Colors.amber, ), + _buildStatItem( + "Luck", + "${player.totalLuck}", + color: Colors.green, + ), ], ), ], @@ -575,6 +580,7 @@ class InventoryScreen extends StatelessWidget { if (item.atkBonus > 0) stats.add("+${item.atkBonus} ATK"); if (item.hpBonus > 0) stats.add("+${item.hpBonus} HP"); if (item.armorBonus > 0) stats.add("+${item.armorBonus} DEF"); + if (item.luck > 0) stats.add("+${item.luck} Luck"); // Include effects List effectTexts = item.effects.map((e) => e.description).toList(); diff --git a/prompt/00_project_context_restore.md b/prompt/00_project_context_restore.md index a2d0041..4857f5c 100644 --- a/prompt/00_project_context_restore.md +++ b/prompt/00_project_context_restore.md @@ -40,6 +40,11 @@ - **Action Feedback:** 공격 빗나감(MISS - 적 위치/회색) 및 방어 실패(FAILED - 내 위치/빨강) 텍스트 오버레이. - **Effect Icons:** 공격/방어 및 리스크 레벨에 따른 아이콘 애니메이션 출력. - **상태이상:** `Stun`, `Bleed`, `Vulnerable`, `DefenseForbidden`. +- **행운 시스템 (Luck System):** + - 아이템 옵션으로 `luck` 스탯 제공. + - `totalLuck` 수치만큼 행동(공격/방어) 성공 확률 증가 (1 Luck = +1%). + - 성공 확률은 최대 100%로 제한됨. + - **UI:** 인벤토리에서 Luck 수치 확인 가능, 전투 시 Risk 선택 창에서 보정된 확률 표시. ### C. 데이터 주도 설계 (Data-Driven Design) @@ -61,6 +66,12 @@ - **타입:** Battle, Shop, Rest, Elite. - **적 생성:** 스테이지 레벨에 따른 스탯 스케일링 적용. +- **게임 구조 (Game Structure):** + - **총 3라운드 (3 Rounds):** 각 라운드는 12개의 스테이지로 구성 (12/12/12). + - **라운드 구성:** + 1. **1라운드:** 지하 불법 투기장 (Underground Illegal Arena) + 2. **2라운드:** 콜로세움 (Colosseum) + 3. **3라운드:** 왕의 투기장 (King's Arena) - 최종 보스(Final Boss) 등장. ## 3. 핵심 파일 및 아키텍처 @@ -100,6 +111,9 @@ - 캐릭터별 이미지 추가 및 하스스톤 스타일의 공격 모션(대상에게 돌진 후 타격) 구현. - **Risky 공격:** 하스스톤의 7데미지 이상 타격감(화면 흔들림, 강렬한 파티클 및 임팩트) 재현. - [ ] **체력 조건부 특수 능력:** 캐릭터의 체력이 30% 미만일 때 발동 가능한 특수 능력 시스템 구현. +- [ ] **영구 스탯 수정자 로직 적용 (필수):** + - 현재 `Character` 클래스에 `permanentModifiers` 필드만 선언되어 있음. + - 추후 `totalAtk`, `totalDefense`, `totalMaxHp` 계산 시 이 수정자들을 반드시 반영해야 함. - [ ] **Google OAuth 로그인 및 계정 연동:** - Firebase Auth 등을 활용한 구글 로그인 구현. - Firestore 또는 Realtime Database에 유저 계정 정보(진행 상황, 재화 등) 저장 및 불러오기 기능 추가. diff --git a/prompt/38_permanent_stat_modifiers.md b/prompt/38_permanent_stat_modifiers.md new file mode 100644 index 0000000..ac12d51 --- /dev/null +++ b/prompt/38_permanent_stat_modifiers.md @@ -0,0 +1,34 @@ +# 영구 스탯 수정자 (Permanent Stat Modifiers) + +## 목표 + +Slay the Spire의 승천(Ascension)이나 유물 시스템처럼, 캐릭터의 스탯에 영구적인 영향을 주는 옵션을 부여할 수 있는 시스템을 구현합니다. 이는 일시적인 `StatusEffect`와는 구별됩니다. + +## 구현 계획 + +### 1. 데이터 모델 정의 + +- **Enum 추가 (`lib/game/enums.dart`):** + - `StatType`: 수정할 스탯의 종류 (`maxHp`, `atk`, `defense`). +- **클래스 생성 (`lib/game/model/stat_modifier.dart`):** + - `PermanentStatModifier`: 스탯 수정 정보를 담는 클래스. + - `id`: 식별자. + - `name`: 표시 이름. + - `description`: 설명. + - `statType`: 대상 스탯 (`StatType`). + - `type`: 연산 타입 (`ModifierType.flat` 또는 `percent`). + - `value`: 수정 값. + +### 2. 캐릭터 모델 변경 (`lib/game/model/entity.dart`) + +- `Character` 클래스에 `List permanentModifiers` 필드 추가. +- `addModifier(PermanentStatModifier modifier)` 메서드 추가. +- `removeModifier(String id)` 메서드 추가. +- **스탯 계산 로직(`totalAtk`, `totalDefense`, `totalMaxHp`) 업데이트:** + - 기존: `base + itemBonus` + - 변경: `(base + itemBonus + flatModifiers) * (1 + percentModifiers)` + +## 기대 효과 + +- 게임 진행 중 획득하는 영구적인 버프/디버프(예: "힘의 물약: 공격력 +2 영구 증가", "저주: 방어력 -1")를 구현할 수 있습니다. +- 로그라이크 요소(메타 프로그레션 또는 런 내 성장)의 기반이 됩니다. diff --git a/prompt/39_luck_system.md b/prompt/39_luck_system.md new file mode 100644 index 0000000..4945c95 --- /dev/null +++ b/prompt/39_luck_system.md @@ -0,0 +1,39 @@ +# 행운(Luck) 시스템 구현 (Luck System) + +## 목표 + +아이템 옵션을 통해 플레이어의 행동(공격/방어) 성공 확률을 높여주는 '행운(Luck)' 스탯을 구현합니다. + +## 구현 계획 + +### 1. 데이터 모델 변경 + +- **`lib/game/model/item.dart`**: `Item` 클래스에 `final int luck` 필드 추가 (기본값 0). +- **`lib/game/data/item_table.dart`**: `ItemTemplate` 클래스에 `luck` 필드 추가 및 JSON 파싱 로직 업데이트. +- **`assets/data/items.json`**: 아이템 데이터에 `"luck"` 필드 지원 (기존 아이템은 0 또는 생략 가능). + +### 2. 캐릭터 모델 변경 (`lib/game/model/entity.dart`) + +- `Character` 클래스에 `int get totalLuck` getter 추가. + - 장착된 모든 아이템의 `luck` 합계를 반환. + +### 3. 전투 로직 변경 (`lib/providers/battle_provider.dart`) + +- `playerAction` 메서드에서 성공 여부 계산 시 `totalLuck` 반영. + - 기본 성공률 + (totalLuck / 100.0). + - 예: Luck 5 = +5% 성공률. + +### 4. UI 구현 (UI Implementation) + +- **인벤토리 화면 (`lib/screens/inventory_screen.dart`):** + - 플레이어 스탯 정보 상단에 `Luck` 수치 표시 (초록색). + - 아이템 정보에 `luck` 보너스가 있는 경우 `+N Luck` 표시. +- **전투 화면 (`lib/screens/battle_screen.dart`):** + - 행동 선택(Risk Selection) 다이얼로그에서 표시되는 성공 확률에 행운 수치 반영. + - **확률 제한 (Cap):** 표시되는 확률과 실제 적용되는 확률 모두 **100% (1.0)를 초과하지 않도록 제한**. + +## 기대 효과 + +- 플레이어가 "행운의 부적" 같은 아이템을 장착하여 Risky 행동의 성공률을 높일 수 있습니다. +- 전략적 선택지(깡스탯 vs 확률 보정)가 늘어납니다. +- UI를 통해 자신의 행운 수치와 실제 적용되는 확률을 직관적으로 확인할 수 있습니다.