Compare commits
5 Commits
d90543dd86
...
d5c46ad04a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5c46ad04a | ||
|
|
71740abe8f | ||
|
|
0cccc17f1f | ||
|
|
5cccd28b77 | ||
|
|
109b4eb678 |
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.example.asciineverdie
|
||||
package com.naturebridgeai.asciineverdie
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
@@ -151,9 +151,9 @@ class RaceData {
|
||||
},
|
||||
passives: [
|
||||
PassiveAbility(
|
||||
type: PassiveType.deathEquipmentPreserve,
|
||||
value: 1.0,
|
||||
description: '사망 시 장비 1개 유지',
|
||||
type: PassiveType.defenseBonus,
|
||||
value: 0.10,
|
||||
description: '방어력 +10%',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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; // 실제 회복량
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,6 @@ enum PassiveType {
|
||||
|
||||
/// MP 배율 보너스
|
||||
mpBonus,
|
||||
|
||||
/// 사망 시 장비 보존
|
||||
deathEquipmentPreserve,
|
||||
}
|
||||
|
||||
/// 패시브 능력 (passive ability)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user