Compare commits

...

5 Commits

Author SHA1 Message Date
JiWoong Sul
d5c46ad04a refactor(ui): 화면 UI 정리
- front_screen: 프론트 화면 레이아웃 개선
- settings_screen: 설정 화면 간소화
- new_character_screen: 캐릭터 생성 화면 정리
2026-01-19 19:41:01 +09:00
JiWoong Sul
71740abe8f refactor(service): 서비스 로직 정리
- ad_service: 광고 서비스 코드 정리
- debug_settings_service: 디버그 설정 간소화
- stat_calculator: 스탯 계산 로직 정리
- character_roll_service: 캐릭터 롤 로직 수정
2026-01-19 19:40:54 +09:00
JiWoong Sul
0cccc17f1f refactor(model): 전투 상태 및 종족 데이터 수정
- combat_state: 전투 상태 모델 필드 추가
- race_traits: 종족 특성 정리
- race_data: 종족 데이터 업데이트
2026-01-19 19:40:48 +09:00
JiWoong Sul
5cccd28b77 refactor(engine): 전투 및 진행 로직 개선
- combat_tick_service: 전투 틱 처리 로직 확장
- progress_service: 진행 상태 처리 개선
- skill_service: 스킬 시스템 업데이트
- potion_service: 포션 처리 로직 수정
2026-01-19 19:40:42 +09:00
JiWoong Sul
109b4eb678 chore(android): 패키지명 변경 및 빌드 설정 업데이트
- 패키지명 com.example → com.naturebridgeai로 변경
- 기존 MainActivity.kt 삭제
- 새 패키지 경로에 MainActivity.kt 추가
2026-01-19 19:40:36 +09:00
16 changed files with 178 additions and 130 deletions

View File

@@ -6,7 +6,7 @@ plugins {
}
android {
namespace = "com.example.asciineverdie"
namespace = "com.naturebridgeai.asciineverdie"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -21,7 +21,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.asciineverdie"
applicationId = "com.naturebridgeai.asciineverdie"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion

View File

@@ -1,4 +1,4 @@
package com.example.asciineverdie
package com.naturebridgeai.asciineverdie
import io.flutter.embedding.android.FlutterActivity

View File

@@ -151,9 +151,9 @@ class RaceData {
},
passives: [
PassiveAbility(
type: PassiveType.deathEquipmentPreserve,
value: 1.0,
description: '사망 시 장비 1개 유지',
type: PassiveType.defenseBonus,
value: 0.10,
description: '방어력 +10%',
),
],
);

View File

@@ -5,6 +5,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:asciineverdie/src/core/engine/iap_service.dart';
/// 광고 타입
enum AdType {
/// 부활용 리워드 광고 (30초)
@@ -105,9 +107,6 @@ class AdService {
bool _isInitialized = false;
/// 디버그 모드에서 광고 활성화 여부
bool _debugAdEnabled = true;
/// 로드된 리워드 광고
RewardedAd? _rewardedAd;
@@ -144,23 +143,14 @@ class AdService {
}
// ===========================================================================
// 디버그 설
// 광고 스킵 판
// ===========================================================================
/// 디버그 모드 광고 활성화 여부
bool get debugAdEnabled => _debugAdEnabled;
/// 디버그 모드 광고 토글
set debugAdEnabled(bool value) {
_debugAdEnabled = value;
debugPrint('[AdService] Debug ad enabled: $value');
}
/// 광고를 스킵할지 여부
///
/// 스킵 조건:
/// - 비모바일 플랫폼 (macOS, Windows, Linux, Web)
/// - 디버그 모드에서 광고 비활성화
/// - IAP로 광고 제거 구매 완료 (디버그 시뮬레이션 포함)
bool get _shouldSkipAd {
// 웹에서는 항상 스킵
if (kIsWeb) return true;
@@ -168,8 +158,9 @@ class AdService {
if (!Platform.isAndroid && !Platform.isIOS) {
return true;
}
// 디버그 모드에서 광고 비활성화 시 스킵
return kDebugMode && !_debugAdEnabled;
// IAP 광고 제거 구매 시 스킵 (디버그 시뮬레이션 포함)
if (IAPService.instance.isAdRemovalPurchased) return true;
return false;
}
// ===========================================================================
@@ -195,7 +186,9 @@ class AdService {
},
onAdFailedToLoad: (error) {
_isLoadingRewardedAd = false;
debugPrint('[AdService] Rewarded ad failed to load: ${error.message}');
debugPrint(
'[AdService] Rewarded ad failed to load: ${error.message}',
);
},
),
);
@@ -249,7 +242,9 @@ class AdService {
_loadRewardedAd(); // 다음 광고 미리 로드
// 보상 수령 여부에 따라 결과 반환
if (!completer.isCompleted) {
completer.complete(rewarded ? AdResult.completed : AdResult.cancelled);
completer.complete(
rewarded ? AdResult.completed : AdResult.cancelled,
);
}
},
onAdFailedToShowFullScreenContent: (ad, error) {

View File

@@ -143,11 +143,10 @@ class CharacterRollService {
_rollsRemaining--;
_saveRollsRemaining();
// 무료 유저: 새 굴리기마다 되돌리기 기회 1회 부여 (광고 시청 필요)
// 유료 유저: 세션당 최대 횟수 유지
if (!_isPaidUser && _undoRemaining < maxUndoFreeUser) {
_undoRemaining = maxUndoFreeUser;
}
// 매 굴림마다 되돌리기 횟수 리셋
// - 유료 유저: 3회
// - 무료 유저: 1회 (광고 시청 필요)
_undoRemaining = _isPaidUser ? maxUndoPaidUser : maxUndoFreeUser;
debugPrint('[CharacterRollService] Rolled: remaining=$_rollsRemaining, '
'history=${_rollHistory.length}, undo=$_undoRemaining');

View File

@@ -1,5 +1,7 @@
import 'package:asciineverdie/data/class_data.dart';
import 'package:asciineverdie/data/skill_data.dart';
import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
import 'package:asciineverdie/src/core/model/class_traits.dart';
import 'package:asciineverdie/src/core/engine/potion_service.dart';
import 'package:asciineverdie/src/core/engine/skill_service.dart';
import 'package:asciineverdie/src/core/model/combat_event.dart';
@@ -94,6 +96,17 @@ class CombatTickService {
totalDamageDealt = dotResult.totalDamageDealt;
newEvents.addAll(dotResult.events);
// 클래스 패시브 조회 (healingBonus, firstStrikeBonus, multiAttack)
final klass = ClassData.findById(state.traits.classId);
final healingBonus =
klass?.getPassiveValue(ClassPassiveType.healingBonus) ?? 0.0;
final healingMultiplier = 1.0 + healingBonus;
final firstStrikeBonus =
klass?.getPassiveValue(ClassPassiveType.firstStrikeBonus) ?? 0.0;
final hasMultiAttack =
klass?.hasPassive(ClassPassiveType.multiAttack) ?? false;
var isFirstPlayerAttack = combat.isFirstPlayerAttack;
// 긴급 물약 자동 사용 (HP < 30% 또는 MP < 50%)
final potionResult = _tryEmergencyPotion(
playerStats: playerStats,
@@ -102,6 +115,7 @@ class CombatTickService {
playerLevel: state.traits.level,
timestamp: timestamp,
potionService: potionService,
healingMultiplier: healingMultiplier,
);
if (potionResult != null) {
playerStats = potionResult.playerStats;
@@ -123,6 +137,10 @@ class CombatTickService {
timestamp: timestamp,
calculator: calculator,
skillService: skillService,
isFirstPlayerAttack: isFirstPlayerAttack,
firstStrikeBonus: firstStrikeBonus > 0 ? firstStrikeBonus : 1.0,
hasMultiAttack: hasMultiAttack,
healingMultiplier: healingMultiplier,
);
playerStats = attackResult.playerStats;
@@ -132,6 +150,7 @@ class CombatTickService {
activeDebuffs = attackResult.activeDebuffs;
totalDamageDealt = attackResult.totalDamageDealt;
newEvents.addAll(attackResult.events);
isFirstPlayerAttack = attackResult.isFirstPlayerAttack;
playerAccumulator -= playerStats.attackDelayMs;
turnsElapsed++;
@@ -178,6 +197,7 @@ class CombatTickService {
activeDoTs: activeDoTs,
lastPotionUsedMs: lastPotionUsedMs,
activeDebuffs: activeDebuffs,
isFirstPlayerAttack: isFirstPlayerAttack,
),
skillSystem: updatedSkillSystem,
potionInventory: updatedPotionInventory,
@@ -259,6 +279,7 @@ class CombatTickService {
required int playerLevel,
required int timestamp,
required PotionService potionService,
double healingMultiplier = 1.0,
}) {
// 글로벌 쿨타임 체크
if (timestamp - lastPotionUsedMs < PotionService.globalPotionCooldownMs) {
@@ -281,6 +302,7 @@ class CombatTickService {
maxHp: playerStats.hpMax,
currentMp: playerStats.mpCurrent,
maxMp: playerStats.mpMax,
healingMultiplier: healingMultiplier,
);
if (result.success) {
@@ -316,6 +338,7 @@ class CombatTickService {
maxHp: playerStats.hpMax,
currentMp: playerStats.mpCurrent,
maxMp: playerStats.mpMax,
healingMultiplier: healingMultiplier,
);
if (result.success) {
@@ -347,6 +370,7 @@ class CombatTickService {
List<ActiveBuff> activeDebuffs,
int totalDamageDealt,
List<CombatEvent> events,
bool isFirstPlayerAttack,
}) _processPlayerAttack({
required GameState state,
required CombatStats playerStats,
@@ -358,6 +382,10 @@ class CombatTickService {
required int timestamp,
required CombatCalculator calculator,
required SkillService skillService,
required bool isFirstPlayerAttack,
required double firstStrikeBonus,
required bool hasMultiAttack,
double healingMultiplier = 1.0,
}) {
final events = <CombatEvent>[];
var newPlayerStats = playerStats;
@@ -442,6 +470,7 @@ class CombatTickService {
skill: selectedSkill,
player: newPlayerStats,
skillSystem: newSkillSystem,
healingMultiplier: healingMultiplier,
);
newPlayerStats = skillResult.updatedPlayer;
newSkillSystem = skillResult.updatedSkillSystem.startGlobalCooldown();
@@ -499,7 +528,22 @@ class CombatTickService {
defender: newMonsterStats,
);
newMonsterStats = attackResult.updatedDefender;
newTotalDamageDealt += attackResult.result.damage;
// 첫 공격 배율 적용 (예: Pointer Assassin 1.5배)
var damage = attackResult.result.damage;
if (isFirstPlayerAttack && firstStrikeBonus > 1.0) {
damage = (damage * firstStrikeBonus).round();
// 첫 공격 배율이 적용된 데미지로 몬스터 HP 재계산
final extraDamage = damage - attackResult.result.damage;
if (extraDamage > 0) {
final newHp = (newMonsterStats.hpCurrent - extraDamage).clamp(
0,
newMonsterStats.hpMax,
);
newMonsterStats = newMonsterStats.copyWith(hpCurrent: newHp);
}
}
newTotalDamageDealt += damage;
final result = attackResult.result;
if (result.isEvaded) {
@@ -513,13 +557,35 @@ class CombatTickService {
events.add(
CombatEvent.playerAttack(
timestamp: timestamp,
damage: result.damage,
damage: damage,
targetName: newMonsterStats.name,
isCritical: result.isCritical,
attackDelayMs: newPlayerStats.attackDelayMs,
),
);
}
// 연속 공격 (Refactor Monk 패시브) - 30% 확률로 추가 공격
if (hasMultiAttack && newMonsterStats.isAlive && rng.nextDouble() < 0.3) {
final extraAttack = calculator.playerAttackMonster(
attacker: newPlayerStats,
defender: newMonsterStats,
);
newMonsterStats = extraAttack.updatedDefender;
newTotalDamageDealt += extraAttack.result.damage;
if (!extraAttack.result.isEvaded) {
events.add(
CombatEvent.playerAttack(
timestamp: timestamp,
damage: extraAttack.result.damage,
targetName: newMonsterStats.name,
isCritical: extraAttack.result.isCritical,
attackDelayMs: newPlayerStats.attackDelayMs,
),
);
}
}
}
return (
@@ -530,6 +596,7 @@ class CombatTickService {
activeDebuffs: newActiveBuffs,
totalDamageDealt: newTotalDamageDealt,
events: events,
isFirstPlayerAttack: false, // 첫 공격 이후에는 false
);
}

View File

@@ -1,7 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:asciineverdie/src/core/engine/ad_service.dart';
import 'package:asciineverdie/src/core/engine/iap_service.dart';
/// 디버그 설정 서비스 (Phase 8)
@@ -23,7 +22,6 @@ class DebugSettingsService {
// 상수
// ===========================================================================
static const String _keyAdEnabled = 'debug_ad_enabled';
static const String _keyIapSimulated = 'debug_iap_simulated';
static const String _keyOfflineHours = 'debug_offline_hours';
@@ -36,9 +34,6 @@ class DebugSettingsService {
bool _isInitialized = false;
/// 광고 활성화 여부 (디버그 모드 전용)
bool _adEnabled = true;
/// IAP 구매 시뮬레이션 여부 (디버그 모드 전용)
bool _iapSimulated = false;
@@ -58,7 +53,6 @@ class DebugSettingsService {
}
final prefs = await SharedPreferences.getInstance();
_adEnabled = prefs.getBool(_keyAdEnabled) ?? true;
_iapSimulated = prefs.getBool(_keyIapSimulated) ?? false;
_offlineHours = prefs.getInt(_keyOfflineHours) ?? 0;
@@ -66,13 +60,14 @@ class DebugSettingsService {
_syncToServices();
_isInitialized = true;
debugPrint('[DebugSettings] Initialized: ad=$_adEnabled, '
'iap=$_iapSimulated, offline=$_offlineHours');
debugPrint(
'[DebugSettings] Initialized: '
'iap=$_iapSimulated, offline=$_offlineHours',
);
}
/// 설정을 다른 서비스에 동기화
void _syncToServices() {
AdService.instance.debugAdEnabled = _adEnabled;
IAPService.instance.debugIAPSimulated = _iapSimulated;
}
@@ -83,29 +78,6 @@ class DebugSettingsService {
/// 디버그 모드 활성화 여부
bool get isDebugMode => kDebugMode;
// ===========================================================================
// 광고 설정
// ===========================================================================
/// 광고 활성화 여부
///
/// - true: 실제 광고 표시
/// - false: 광고 버튼 클릭 시 바로 보상 지급
bool get adEnabled => _adEnabled;
/// 광고 활성화 토글
Future<void> setAdEnabled(bool value) async {
if (!kDebugMode) return;
_adEnabled = value;
AdService.instance.debugAdEnabled = value;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_keyAdEnabled, value);
debugPrint('[DebugSettings] Ad enabled: $value');
}
// ===========================================================================
// IAP 설정
// ===========================================================================
@@ -171,7 +143,6 @@ class DebugSettingsService {
Future<void> resetAll() async {
if (!kDebugMode) return;
await setAdEnabled(true);
await setIapSimulated(false);
await setOfflineHours(0);

View File

@@ -52,6 +52,7 @@ class PotionService {
/// [maxHp] 최대 HP
/// [currentMp] 현재 MP
/// [maxMp] 최대 MP
/// [healingMultiplier] 회복력 배율 (기본 1.0, 클래스 패시브 적용)
PotionUseResult usePotion({
required String potionId,
required PotionInventory inventory,
@@ -59,6 +60,7 @@ class PotionService {
required int maxHp,
required int currentMp,
required int maxMp,
double healingMultiplier = 1.0,
}) {
final (canUse, failReason) = canUsePotion(potionId, inventory);
if (!canUse) {
@@ -71,11 +73,15 @@ class PotionService {
int newMp = currentMp;
if (potion.isHpPotion) {
healedAmount = potion.calculateHeal(maxHp);
// 회복력 보너스 적용 (예: Debugger Paladin +10%)
final baseHeal = potion.calculateHeal(maxHp);
healedAmount = (baseHeal * healingMultiplier).round();
newHp = (currentHp + healedAmount).clamp(0, maxHp);
healedAmount = newHp - currentHp; // 실제 회복량
} else if (potion.isMpPotion) {
healedAmount = potion.calculateHeal(maxMp);
// MP 물약에도 회복력 보너스 적용
final baseHeal = potion.calculateHeal(maxMp);
healedAmount = (baseHeal * healingMultiplier).round();
newMp = (currentMp + healedAmount).clamp(0, maxMp);
healedAmount = newMp - currentMp; // 실제 회복량
}

View File

@@ -1,6 +1,9 @@
import 'dart:math' as math;
import 'package:asciineverdie/data/class_data.dart';
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
import 'package:asciineverdie/data/race_data.dart';
import 'package:asciineverdie/src/core/model/class_traits.dart';
import 'package:asciineverdie/src/core/animation/monster_size.dart';
import 'package:asciineverdie/src/core/engine/act_progression_service.dart';
import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
@@ -273,10 +276,21 @@ class ProgressService {
final remainingHp = combat.playerStats.hpCurrent;
final maxHp = combat.playerStats.hpMax;
// 전투 승리 시 HP 회복 (50% + CON/2)
// 전투 승리 시 HP 회복 (50% + CON/2 + 클래스 패시브)
// 아이들 게임 특성상 전투 사이 HP가 회복되어야 지속 플레이 가능
final conBonus = nextState.stats.con ~/ 2;
final healAmount = (maxHp * 0.5).round() + conBonus;
var healAmount = (maxHp * 0.5).round() + conBonus;
// 클래스 패시브: 전투 후 HP 회복 (예: Garbage Collector +5%)
final klass = ClassData.findById(nextState.traits.classId);
if (klass != null) {
final postCombatHealRate =
klass.getPassiveValue(ClassPassiveType.postCombatHeal);
if (postCombatHealRate > 0) {
healAmount += (maxHp * postCombatHealRate).round();
}
}
final newHp = (remainingHp + healAmount).clamp(0, maxHp);
nextState = nextState.copyWith(
@@ -384,7 +398,11 @@ class ProgressService {
// Gain XP / level up (몬스터 경험치 기반)
// 최대 레벨(100) 제한: 100레벨에서는 더 이상 레벨업하지 않음
if (gain && nextState.traits.level < 100 && monsterExpReward > 0) {
final newExpPos = progress.exp.position + monsterExpReward;
// 종족 경험치 배율 적용 (예: Byte Human +5%, Callback Seraph +3%)
final race = RaceData.findById(nextState.traits.raceId);
final expMultiplier = race?.expMultiplier ?? 1.0;
final adjustedExp = (monsterExpReward * expMultiplier).round();
final newExpPos = progress.exp.position + adjustedExp;
// 레벨업 체크 (경험치가 필요량 이상일 때)
if (newExpPos >= progress.exp.max) {

View File

@@ -112,6 +112,8 @@ class SkillService {
}
/// 회복 스킬 사용
///
/// [healingMultiplier] 회복력 배율 (기본 1.0, 클래스 패시브 적용)
({
SkillUseResult result,
CombatStats updatedPlayer,
@@ -121,6 +123,7 @@ class SkillService {
required Skill skill,
required CombatStats player,
required SkillSystemState skillSystem,
double healingMultiplier = 1.0,
}) {
// 회복량 계산
int healAmount = skill.healAmount;
@@ -128,6 +131,9 @@ class SkillService {
healAmount += (player.hpMax * skill.healPercent).round();
}
// 회복력 보너스 적용 (예: Debugger Paladin +10%, Exception Handler +15%)
healAmount = (healAmount * healingMultiplier).round();
// HP 회복
var updatedPlayer = player.applyHeal(healAmount);

View File

@@ -187,17 +187,6 @@ class StatCalculator {
return bonus > 0 ? bonus : 1.0;
}
/// 사망 시 보존할 장비 개수 (Coredump Undead 패시브)
///
/// [race] 종족 특성
/// Returns: 보존 장비 개수
int calculateDeathEquipmentPreserve(RaceTraits race) {
if (race.hasPassive(PassiveType.deathEquipmentPreserve)) {
return race.getPassiveValue(PassiveType.deathEquipmentPreserve).round();
}
return 0;
}
/// 연속 공격 가능 여부 (Refactor Monk 패시브)
bool hasMultiAttack(ClassTraits klass) {
return klass.hasPassive(ClassPassiveType.multiAttack);

View File

@@ -21,6 +21,7 @@ class CombatState {
this.activeDoTs = const [],
this.lastPotionUsedMs = 0,
this.activeDebuffs = const [],
this.isFirstPlayerAttack = true,
});
/// 플레이어 전투 스탯
@@ -59,6 +60,9 @@ class CombatState {
/// 몬스터에 적용된 활성 디버프 목록
final List<ActiveBuff> activeDebuffs;
/// 첫 번째 플레이어 공격 여부 (firstStrikeBonus 적용용)
final bool isFirstPlayerAttack;
// ============================================================================
// 유틸리티
// ============================================================================
@@ -124,6 +128,7 @@ class CombatState {
List<DotEffect>? activeDoTs,
int? lastPotionUsedMs,
List<ActiveBuff>? activeDebuffs,
bool? isFirstPlayerAttack,
}) {
return CombatState(
playerStats: playerStats ?? this.playerStats,
@@ -140,6 +145,7 @@ class CombatState {
activeDoTs: activeDoTs ?? this.activeDoTs,
lastPotionUsedMs: lastPotionUsedMs ?? this.lastPotionUsedMs,
activeDebuffs: activeDebuffs ?? this.activeDebuffs,
isFirstPlayerAttack: isFirstPlayerAttack ?? this.isFirstPlayerAttack,
);
}

View File

@@ -20,9 +20,6 @@ enum PassiveType {
/// MP 배율 보너스
mpBonus,
/// 사망 시 장비 보존
deathEquipmentPreserve,
}
/// 패시브 능력 (passive ability)

View File

@@ -321,8 +321,8 @@ class _ActionButtons extends StatelessWidget {
onPressed: onSettings,
isPrimary: false,
),
// IAP 구매 (광고 제거) - 스토어 사용 가능하고 미구매 상태일 때만 표시
if (removeAdsPrice != null && !isAdRemovalPurchased) ...[
// IAP 구매 (광고 제거) - 스토어 사용 가능할 때 표시 (구매 완료 시 비활성화)
if (removeAdsPrice != null) ...[
const SizedBox(height: 20),
const Divider(color: RetroColors.panelBorderInner, height: 1),
const SizedBox(height: 12),
@@ -330,13 +330,12 @@ class _ActionButtons extends StatelessWidget {
price: removeAdsPrice!,
onPurchase: onPurchaseRemoveAds,
onRestore: onRestorePurchase,
enabled: !isAdRemovalPurchased,
),
],
// 이미 구매된 경우 표시
// 이미 구매된 경우 배지 표시
if (isAdRemovalPurchased) ...[
const SizedBox(height: 20),
const Divider(color: RetroColors.panelBorderInner, height: 1),
const SizedBox(height: 12),
const SizedBox(height: 8),
_PurchasedBadge(),
],
],
@@ -412,11 +411,13 @@ class _IapPurchaseButton extends StatelessWidget {
required this.price,
this.onPurchase,
this.onRestore,
this.enabled = true,
});
final String price;
final VoidCallback? onPurchase;
final VoidCallback? onRestore;
final bool enabled;
void _showPurchaseDialog(BuildContext context) {
showDialog<void>(
@@ -433,23 +434,33 @@ class _IapPurchaseButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 비활성화 상태의 색상
final buttonColor = enabled ? RetroColors.gold : RetroColors.textDisabled;
final bgGradient = enabled
? const LinearGradient(
colors: [Color(0xFF4A3B2A), Color(0xFF3D2E1F)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
)
: const LinearGradient(
colors: [Color(0xFF3A3A3A), Color(0xFF2A2A2A)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 구매 버튼 (클릭 시 팝업)
Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF4A3B2A), Color(0xFF3D2E1F)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
border: Border.all(color: RetroColors.gold, width: 2),
gradient: bgGradient,
border: Border.all(color: buttonColor, width: 2),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _showPurchaseDialog(context),
onTap: enabled ? () => _showPurchaseDialog(context) : null,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
@@ -457,7 +468,7 @@ class _IapPurchaseButton extends StatelessWidget {
),
child: Row(
children: [
const Icon(Icons.block, color: RetroColors.gold, size: 24),
Icon(Icons.block, color: buttonColor, size: 24),
const SizedBox(width: 12),
Expanded(
child: Column(
@@ -465,10 +476,10 @@ class _IapPurchaseButton extends StatelessWidget {
children: [
Text(
game_l10n.iapRemoveAds,
style: const TextStyle(
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
color: RetroColors.gold,
color: buttonColor,
),
),
const SizedBox(height: 4),
@@ -484,11 +495,7 @@ class _IapPurchaseButton extends StatelessWidget {
),
),
// 화살표 아이콘 (상세 보기)
const Icon(
Icons.arrow_forward_ios,
color: RetroColors.gold,
size: 16,
),
Icon(Icons.arrow_forward_ios, color: buttonColor, size: 16),
],
),
),
@@ -499,7 +506,7 @@ class _IapPurchaseButton extends StatelessWidget {
// 복원 버튼
Center(
child: TextButton(
onPressed: onRestore,
onPressed: enabled ? onRestore : null,
child: Text(
game_l10n.iapRestorePurchase,
style: const TextStyle(
@@ -569,7 +576,10 @@ class _IapPurchaseDialog extends StatelessWidget {
const SizedBox(height: 8),
_BenefitItem(icon: Icons.speed, text: game_l10n.iapBenefit5),
const SizedBox(height: 8),
_BenefitItem(icon: Icons.inventory_2, text: game_l10n.iapBenefit6),
_BenefitItem(
icon: Icons.inventory_2,
text: game_l10n.iapBenefit6,
),
const SizedBox(height: 20),
// 가격 + 구매 버튼
Container(

View File

@@ -803,7 +803,6 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
PassiveType.magicDamageBonus => game_l10n.passiveMagicBonus(percent),
PassiveType.criticalBonus => game_l10n.passiveCritBonus(percent),
PassiveType.expBonus => passive.description,
PassiveType.deathEquipmentPreserve => passive.description,
};
}

View File

@@ -64,7 +64,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
bool _isLoading = true;
// 디버그 설정 상태 (Phase 8)
bool _debugAdEnabled = true;
bool _debugIapSimulated = false;
int _debugOfflineHours = 0;
@@ -80,7 +79,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
// 디버그 설정 로드 (Phase 8)
final debugSettings = DebugSettingsService.instance;
final adEnabled = debugSettings.adEnabled;
final iapSimulated = debugSettings.iapSimulated;
final offlineHours = debugSettings.offlineHours;
@@ -88,7 +86,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {
_bgmVolume = bgm;
_sfxVolume = sfx;
_debugAdEnabled = adEnabled;
_debugIapSimulated = iapSimulated;
_debugOfflineHours = offlineHours;
_isLoading = false;
@@ -304,7 +301,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
// 헤더
Row(
children: [
Icon(Icons.bug_report, color: RetroColors.hpOf(context), size: 16),
Icon(
Icons.bug_report,
color: RetroColors.hpOf(context),
size: 16,
),
const SizedBox(width: 8),
Text(
'DEVELOPER TOOLS',
@@ -318,19 +319,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
const SizedBox(height: 16),
// 광고 ON/OFF 토글
_RetroDebugToggle(
icon: Icons.ad_units,
label: 'ADS ENABLED',
description: 'OFF: 광고 버튼 클릭 시 바로 보상',
value: _debugAdEnabled,
onChanged: (value) async {
await DebugSettingsService.instance.setAdEnabled(value);
setState(() => _debugAdEnabled = value);
},
),
const SizedBox(height: 12),
// IAP 시뮬레이션 토글
_RetroDebugToggle(
icon: Icons.shopping_cart,
@@ -448,7 +436,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
context: context,
builder: (context) => _RetroConfirmDialog(
title: 'CREATE TEST CHARACTER?',
message: '현재 캐릭터가 레벨 100으로 변환되어\n'
message:
'현재 캐릭터가 레벨 100으로 변환되어\n'
'명예의 전당에 등록됩니다.\n\n'
'⚠️ 현재 세이브 파일이 삭제됩니다.\n'
'이 작업은 되돌릴 수 없습니다.',
@@ -480,11 +469,7 @@ class _RetroSectionTitle extends StatelessWidget {
Widget build(BuildContext context) {
return Row(
children: [
Container(
width: 4,
height: 14,
color: RetroColors.goldOf(context),
),
Container(width: 4, height: 14, color: RetroColors.goldOf(context)),
const SizedBox(width: 8),
Text(
title.toUpperCase(),