This commit is contained in:
Horoli 2025-12-04 18:59:25 +09:00
parent 0dccab9eec
commit d3fd9680c2
11 changed files with 206 additions and 43 deletions

View File

@ -7,74 +7,70 @@ class ItemTemplate {
final String id; final String id;
final String name; final String name;
final String description; final String description;
final int baseAtk; final int atkBonus;
final int baseHp; final int hpBonus;
final int baseArmor; final int armorBonus;
final EquipmentSlot slot; final EquipmentSlot slot;
final List<ItemEffect> effects; final List<ItemEffect> effects;
final int price; final int price;
final String? image; final String? image;
final int luck;
const ItemTemplate({ const ItemTemplate({
required this.id, required this.id,
required this.name, required this.name,
required this.description, required this.description,
this.baseAtk = 0, required this.atkBonus,
this.baseHp = 0, required this.hpBonus,
this.baseArmor = 0, required this.armorBonus,
required this.slot, required this.slot,
this.effects = const [], required this.effects,
this.price = 0, required this.price,
this.image, this.image,
this.luck = 0,
}); });
factory ItemTemplate.fromJson(Map<String, dynamic> json) { factory ItemTemplate.fromJson(Map<String, dynamic> json) {
var effectsList = <ItemEffect>[];
if (json['effects'] != null) {
effectsList = (json['effects'] as List)
.map((e) => ItemEffect.fromJson(e))
.toList();
}
return ItemTemplate( return ItemTemplate(
id: id: json['id'],
json['id'] ??
json['name'], // Fallback to name if id is missing (for backward compatibility during dev)
name: json['name'], name: json['name'],
description: json['description'], description: json['description'],
baseAtk: json['baseAtk'] ?? 0, atkBonus: json['atkBonus'] ?? 0,
baseHp: json['baseHp'] ?? 0, hpBonus: json['hpBonus'] ?? 0,
baseArmor: json['baseArmor'] ?? 0, armorBonus: json['armorBonus'] ?? 0,
slot: EquipmentSlot.values.firstWhere((e) => e.name == json['slot']), slot: EquipmentSlot.values.firstWhere((e) => e.name == json['slot']),
effects: effects: effectsList,
(json['effects'] as List<dynamic>?) price: json['price'] ?? 10,
?.map((e) => ItemEffect.fromJson(e))
.toList() ??
[],
price: json['price'] ?? 0,
image: json['image'], 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}) { Item createItem({int stage = 1}) {
// Simple scaling logic: add stage-1 to relevant stats // Scale stats based on stage
// You can make this more complex (multiplier, tiering, etc.) int scaledAtk = (atkBonus * (1 + (stage - 1) * 0.1)).toInt();
int scaledAtk = baseAtk > 0 ? baseAtk + (stage - 1) : 0; int scaledHp = (hpBonus * (1 + (stage - 1) * 0.1)).toInt();
int scaledHp = baseHp > 0 ? baseHp + (stage - 1) * 5 : 0; int scaledArmor = (armorBonus * (1 + (stage - 1) * 0.1)).toInt();
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();
}
return Item( return Item(
id: id, id: id,
name: "$name${stage > 1 ? ' +${stage - 1}' : ''}", // Append +1, +2 etc. name: "$name${stage > 1 ? ' +${stage - 1}' : ''}",
description: description, description: description,
atkBonus: scaledAtk, atkBonus: scaledAtk,
hpBonus: scaledHp, hpBonus: scaledHp,
armorBonus: scaledArmor, armorBonus: scaledArmor,
slot: slot, slot: slot,
effects: effects, // Pass the effects to the Item effects: effects,
price: finalPrice, price: price,
image: image, image: image,
luck: luck,
); );
} }
} }

View File

@ -33,3 +33,4 @@ enum EquipmentSlot { weapon, armor, shield, accessory }
enum DamageType { normal, bleed, vulnerable } enum DamageType { normal, bleed, vulnerable }
enum StatType { maxHp, atk, defense }

View File

@ -1,5 +1,6 @@
import 'item.dart'; import 'item.dart';
import 'status_effect.dart'; import 'status_effect.dart';
import 'stat_modifier.dart';
import '../enums.dart'; import '../enums.dart';
class Character { class Character {
@ -19,6 +20,9 @@ class Character {
// Active status effects // Active status effects
List<StatusEffect> statusEffects = []; List<StatusEffect> statusEffects = [];
// Permanent stat modifiers (e.g. Ascension, Potions)
List<PermanentStatModifier> permanentModifiers = [];
Character({ Character({
required this.name, required this.name,
int? hp, int? hp,
@ -66,6 +70,14 @@ class Character {
statusEffects.removeWhere((e) => e.duration <= 0); 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 /// Helper to check if character has a specific status
bool hasStatus(StatusEffectType type) { bool hasStatus(StatusEffectType type) {
return statusEffects.any((e) => e.type == type); return statusEffects.any((e) => e.type == type);
@ -86,6 +98,10 @@ class Character {
return baseDefense + bonus; return baseDefense + bonus;
} }
int get totalLuck {
return equipment.values.fold(0, (sum, item) => sum + item.luck);
}
bool get isDead => hp <= 0; bool get isDead => hp <= 0;
// Adds an item to inventory, returns true if successful, false if inventory is full // Adds an item to inventory, returns true if successful, false if inventory is full

View File

@ -46,8 +46,9 @@ class Item {
final List<ItemEffect> effects; // Status effects this item can inflict final List<ItemEffect> effects; // Status effects this item can inflict
final int price; // New: Sell/Buy value final int price; // New: Sell/Buy value
final String? image; // New: Image path final String? image; // New: Image path
final int luck; // Success rate bonus (e.g. 5 = 5%)
Item({ const Item({
required this.id, required this.id,
required this.name, required this.name,
required this.description, required this.description,
@ -58,6 +59,7 @@ class Item {
this.effects = const [], // Default to no effects this.effects = const [], // Default to no effects
this.price = 0, this.price = 0,
this.image, this.image,
this.luck = 0,
}); });
String get typeName { String get typeName {

View File

@ -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<String, dynamic> 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<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'description': description,
'statType': statType.name,
'type': type.name,
'value': value,
};
}
}

View File

@ -277,15 +277,24 @@ class BattleProvider with ChangeNotifier {
switch (risk) { switch (risk) {
case RiskLevel.safe: 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% efficiency = 0.5; // 50%
break; break;
case RiskLevel.normal: 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% efficiency = 1.0; // 100%
break; break;
case RiskLevel.risky: 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% efficiency = 2.0; // 200%
break; break;
} }

View File

@ -274,18 +274,23 @@ class _BattleScreenState extends State<BattleScreen> {
: "Armor"; : "Armor";
String successRate = ""; String successRate = "";
double baseChance = 0.0;
switch (risk) { switch (risk) {
case RiskLevel.safe: case RiskLevel.safe:
successRate = "100%"; baseChance = 1.0;
break; break;
case RiskLevel.normal: case RiskLevel.normal:
successRate = "80%"; baseChance = 0.8;
break; break;
case RiskLevel.risky: case RiskLevel.risky:
successRate = "40%"; baseChance = 0.4;
break; break;
} }
double finalChance = baseChance + (player.totalLuck / 100.0);
if (finalChance > 1.0) finalChance = 1.0;
successRate = "${(finalChance * 100).toInt()}%";
infoText = infoText =
"Success: $successRate, Eff: ${(efficiency * 100).toInt()}% ($expectedValue $valueUnit)"; "Success: $successRate, Eff: ${(efficiency * 100).toInt()}% ($expectedValue $valueUnit)";

View File

@ -47,6 +47,11 @@ class InventoryScreen extends StatelessWidget {
"${player.gold} G", "${player.gold} G",
color: Colors.amber, 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.atkBonus > 0) stats.add("+${item.atkBonus} ATK");
if (item.hpBonus > 0) stats.add("+${item.hpBonus} HP"); if (item.hpBonus > 0) stats.add("+${item.hpBonus} HP");
if (item.armorBonus > 0) stats.add("+${item.armorBonus} DEF"); if (item.armorBonus > 0) stats.add("+${item.armorBonus} DEF");
if (item.luck > 0) stats.add("+${item.luck} Luck");
// Include effects // Include effects
List<String> effectTexts = item.effects.map((e) => e.description).toList(); List<String> effectTexts = item.effects.map((e) => e.description).toList();

View File

@ -40,6 +40,11 @@
- **Action Feedback:** 공격 빗나감(MISS - 적 위치/회색) 및 방어 실패(FAILED - 내 위치/빨강) 텍스트 오버레이. - **Action Feedback:** 공격 빗나감(MISS - 적 위치/회색) 및 방어 실패(FAILED - 내 위치/빨강) 텍스트 오버레이.
- **Effect Icons:** 공격/방어 및 리스크 레벨에 따른 아이콘 애니메이션 출력. - **Effect Icons:** 공격/방어 및 리스크 레벨에 따른 아이콘 애니메이션 출력.
- **상태이상:** `Stun`, `Bleed`, `Vulnerable`, `DefenseForbidden`. - **상태이상:** `Stun`, `Bleed`, `Vulnerable`, `DefenseForbidden`.
- **행운 시스템 (Luck System):**
- 아이템 옵션으로 `luck` 스탯 제공.
- `totalLuck` 수치만큼 행동(공격/방어) 성공 확률 증가 (1 Luck = +1%).
- 성공 확률은 최대 100%로 제한됨.
- **UI:** 인벤토리에서 Luck 수치 확인 가능, 전투 시 Risk 선택 창에서 보정된 확률 표시.
### C. 데이터 주도 설계 (Data-Driven Design) ### C. 데이터 주도 설계 (Data-Driven Design)
@ -61,6 +66,12 @@
- **타입:** Battle, Shop, Rest, Elite. - **타입:** 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. 핵심 파일 및 아키텍처 ## 3. 핵심 파일 및 아키텍처
@ -100,6 +111,9 @@
- 캐릭터별 이미지 추가 및 하스스톤 스타일의 공격 모션(대상에게 돌진 후 타격) 구현. - 캐릭터별 이미지 추가 및 하스스톤 스타일의 공격 모션(대상에게 돌진 후 타격) 구현.
- **Risky 공격:** 하스스톤의 7데미지 이상 타격감(화면 흔들림, 강렬한 파티클 및 임팩트) 재현. - **Risky 공격:** 하스스톤의 7데미지 이상 타격감(화면 흔들림, 강렬한 파티클 및 임팩트) 재현.
- [ ] **체력 조건부 특수 능력:** 캐릭터의 체력이 30% 미만일 때 발동 가능한 특수 능력 시스템 구현. - [ ] **체력 조건부 특수 능력:** 캐릭터의 체력이 30% 미만일 때 발동 가능한 특수 능력 시스템 구현.
- [ ] **영구 스탯 수정자 로직 적용 (필수):**
- 현재 `Character` 클래스에 `permanentModifiers` 필드만 선언되어 있음.
- 추후 `totalAtk`, `totalDefense`, `totalMaxHp` 계산 시 이 수정자들을 반드시 반영해야 함.
- [ ] **Google OAuth 로그인 및 계정 연동:** - [ ] **Google OAuth 로그인 및 계정 연동:**
- Firebase Auth 등을 활용한 구글 로그인 구현. - Firebase Auth 등을 활용한 구글 로그인 구현.
- Firestore 또는 Realtime Database에 유저 계정 정보(진행 상황, 재화 등) 저장 및 불러오기 기능 추가. - Firestore 또는 Realtime Database에 유저 계정 정보(진행 상황, 재화 등) 저장 및 불러오기 기능 추가.

View File

@ -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<PermanentStatModifier> permanentModifiers` 필드 추가.
- `addModifier(PermanentStatModifier modifier)` 메서드 추가.
- `removeModifier(String id)` 메서드 추가.
- **스탯 계산 로직(`totalAtk`, `totalDefense`, `totalMaxHp`) 업데이트:**
- 기존: `base + itemBonus`
- 변경: `(base + itemBonus + flatModifiers) * (1 + percentModifiers)`
## 기대 효과
- 게임 진행 중 획득하는 영구적인 버프/디버프(예: "힘의 물약: 공격력 +2 영구 증가", "저주: 방어력 -1")를 구현할 수 있습니다.
- 로그라이크 요소(메타 프로그레션 또는 런 내 성장)의 기반이 됩니다.

39
prompt/39_luck_system.md Normal file
View File

@ -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를 통해 자신의 행운 수치와 실제 적용되는 확률을 직관적으로 확인할 수 있습니다.