style: dart format 적용
- 전체 Dart 소스 및 테스트 파일 포매팅 통일 - trailing comma, 줄바꿈, 인덴트 정리
This commit is contained in:
@@ -172,8 +172,7 @@ String returnRewardHoursAway(String time) =>
|
|||||||
String returnRewardChests(int count) =>
|
String returnRewardChests(int count) =>
|
||||||
_l('$count Treasure Chest(s)', '보물 상자 $count개', '宝箱 $count個');
|
_l('$count Treasure Chest(s)', '보물 상자 $count개', '宝箱 $count個');
|
||||||
String get returnRewardOpenChests => _l('Open Chests', '상자 열기', '宝箱を開ける');
|
String get returnRewardOpenChests => _l('Open Chests', '상자 열기', '宝箱を開ける');
|
||||||
String get returnRewardBonusChests =>
|
String get returnRewardBonusChests => _l('Bonus Chests', '보너스 상자', 'ボーナス宝箱');
|
||||||
_l('Bonus Chests', '보너스 상자', 'ボーナス宝箱');
|
|
||||||
String get returnRewardClaimBonus =>
|
String get returnRewardClaimBonus =>
|
||||||
_l('Get Bonus (AD)', '보너스 받기 (광고)', 'ボーナス受取 (広告)');
|
_l('Get Bonus (AD)', '보너스 받기 (광고)', 'ボーナス受取 (広告)');
|
||||||
String get returnRewardClaimBonusFree =>
|
String get returnRewardClaimBonusFree =>
|
||||||
@@ -193,8 +192,7 @@ String chestRewardExpAmount(int exp) =>
|
|||||||
_l('+$exp EXP', '+$exp 경험치', '+$exp 経験値');
|
_l('+$exp EXP', '+$exp 경험치', '+$exp 経験値');
|
||||||
String chestRewardPotionAmount(String name, int count) =>
|
String chestRewardPotionAmount(String name, int count) =>
|
||||||
_l('$name x$count', '$name x$count', '$name x$count');
|
_l('$name x$count', '$name x$count', '$name x$count');
|
||||||
String get chestRewardEquipped =>
|
String get chestRewardEquipped => _l('Equipped!', '장착됨!', '装備しました!');
|
||||||
_l('Equipped!', '장착됨!', '装備しました!');
|
|
||||||
String get chestRewardBetterItem =>
|
String get chestRewardBetterItem =>
|
||||||
_l('Better than current!', '현재보다 좋습니다!', '現在より良い!');
|
_l('Better than current!', '현재보다 좋습니다!', '現在より良い!');
|
||||||
|
|
||||||
@@ -1143,24 +1141,15 @@ String get uiSaveBattleLog => _l('Save Battle Log', '배틀로그 저장', 'バ
|
|||||||
String get iapRemoveAds => _l('Remove Ads', '광고 제거', '広告削除');
|
String get iapRemoveAds => _l('Remove Ads', '광고 제거', '広告削除');
|
||||||
String get iapRemoveAdsDesc =>
|
String get iapRemoveAdsDesc =>
|
||||||
_l('Enjoy ad-free experience', '광고 없이 플레이', '広告なしでプレイ');
|
_l('Enjoy ad-free experience', '광고 없이 플레이', '広告なしでプレイ');
|
||||||
String get iapBenefitTitle =>
|
String get iapBenefitTitle => _l('Premium Benefits', '프리미엄 혜택', 'プレミアム特典');
|
||||||
_l('Premium Benefits', '프리미엄 혜택', 'プレミアム特典');
|
String get iapBenefit1 => _l('Ad-free gameplay', '광고 없는 쾌적한 플레이', '広告なしの快適プレイ');
|
||||||
String get iapBenefit1 =>
|
|
||||||
_l('Ad-free gameplay', '광고 없는 쾌적한 플레이', '広告なしの快適プレイ');
|
|
||||||
String get iapBenefit2 =>
|
String get iapBenefit2 =>
|
||||||
_l('Unlimited speed boost', '속도 부스트 무제한', 'スピードブースト無制限');
|
_l('Unlimited speed boost', '속도 부스트 무제한', 'スピードブースト無制限');
|
||||||
String get iapBenefit3 => _l(
|
String get iapBenefit3 =>
|
||||||
'Stat reroll undo: 3 times',
|
_l('Stat reroll undo: 3 times', '신규 캐릭터 스탯 가챠 되돌리기 3회', '新キャラステ振り直し3回');
|
||||||
'신규 캐릭터 스탯 가챠 되돌리기 3회',
|
String get iapBenefit4 => _l('Unlimited rerolls', '굴리기 무제한', 'リロール無制限');
|
||||||
'新キャラステ振り直し3回',
|
String get iapBenefit5 =>
|
||||||
);
|
_l('2x offline time credited', '오프라인 시간 2배 인정', 'オフライン時間2倍適用');
|
||||||
String get iapBenefit4 =>
|
|
||||||
_l('Unlimited rerolls', '굴리기 무제한', 'リロール無制限');
|
|
||||||
String get iapBenefit5 => _l(
|
|
||||||
'2x offline time credited',
|
|
||||||
'오프라인 시간 2배 인정',
|
|
||||||
'オフライン時間2倍適用',
|
|
||||||
);
|
|
||||||
String get iapBenefit6 =>
|
String get iapBenefit6 =>
|
||||||
_l('Return chests: 10 max', '복귀 상자 최대 10개', '帰還ボックス最大10個');
|
_l('Return chests: 10 max', '복귀 상자 최대 10개', '帰還ボックス最大10個');
|
||||||
String get iapPurchaseButton => _l('Purchase', '구매하기', '購入する');
|
String get iapPurchaseButton => _l('Purchase', '구매하기', '購入する');
|
||||||
@@ -1232,17 +1221,12 @@ String get skillNoDetails => _l('No details', '상세 정보 없음', '詳細情
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
String get notifyLevelUp => _l('LEVEL UP!', '레벨 업!', 'レベルアップ!');
|
String get notifyLevelUp => _l('LEVEL UP!', '레벨 업!', 'レベルアップ!');
|
||||||
String notifyLevel(int level) =>
|
String notifyLevel(int level) => _l('Level $level', '레벨 $level', 'レベル $level');
|
||||||
_l('Level $level', '레벨 $level', 'レベル $level');
|
String get notifyQuestComplete => _l('QUEST COMPLETE!', '퀘스트 완료!', 'クエスト完了!');
|
||||||
String get notifyQuestComplete =>
|
|
||||||
_l('QUEST COMPLETE!', '퀘스트 완료!', 'クエスト完了!');
|
|
||||||
String get notifyPrologueComplete =>
|
String get notifyPrologueComplete =>
|
||||||
_l('PROLOGUE COMPLETE!', '프롤로그 완료!', 'プロローグ完了!');
|
_l('PROLOGUE COMPLETE!', '프롤로그 완료!', 'プロローグ完了!');
|
||||||
String notifyActComplete(int actNumber) => _l(
|
String notifyActComplete(int actNumber) =>
|
||||||
'ACT $actNumber COMPLETE!',
|
_l('ACT $actNumber COMPLETE!', '${actNumber}막 완료!', '第${actNumber}幕完了!');
|
||||||
'${actNumber}막 완료!',
|
|
||||||
'第${actNumber}幕完了!',
|
|
||||||
);
|
|
||||||
String get notifyNewSpell => _l('NEW SPELL!', '새 주문!', '新しい呪文!');
|
String get notifyNewSpell => _l('NEW SPELL!', '새 주문!', '新しい呪文!');
|
||||||
String get notifyNewEquipment => _l('NEW EQUIPMENT!', '새 장비!', '新しい装備!');
|
String get notifyNewEquipment => _l('NEW EQUIPMENT!', '새 장비!', '新しい装備!');
|
||||||
String get notifyBossDefeated => _l('BOSS DEFEATED!', '보스 처치!', 'ボス撃破!');
|
String get notifyBossDefeated => _l('BOSS DEFEATED!', '보스 처치!', 'ボス撃破!');
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
|||||||
///
|
///
|
||||||
/// ProgressService에서 추출된 Act 완료, 보스 생성 등의 로직 담당.
|
/// ProgressService에서 추출된 Act 완료, 보스 생성 등의 로직 담당.
|
||||||
class ActProgressionService {
|
class ActProgressionService {
|
||||||
const ActProgressionService({
|
const ActProgressionService({required this.config});
|
||||||
required this.config,
|
|
||||||
});
|
|
||||||
|
|
||||||
final PqConfig config;
|
final PqConfig config;
|
||||||
|
|
||||||
|
|||||||
@@ -64,8 +64,10 @@ class CharacterRollService {
|
|||||||
_resetUndoForNewSession();
|
_resetUndoForNewSession();
|
||||||
|
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
debugPrint('[CharacterRollService] Initialized: '
|
debugPrint(
|
||||||
'rolls=$_rollsRemaining, undo=$_undoRemaining');
|
'[CharacterRollService] Initialized: '
|
||||||
|
'rolls=$_rollsRemaining, undo=$_undoRemaining',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 저장된 상태 로드
|
/// 저장된 상태 로드
|
||||||
@@ -148,8 +150,10 @@ class CharacterRollService {
|
|||||||
// - 무료 유저: 1회 (광고 시청 필요)
|
// - 무료 유저: 1회 (광고 시청 필요)
|
||||||
_undoRemaining = _isPaidUser ? maxUndoPaidUser : maxUndoFreeUser;
|
_undoRemaining = _isPaidUser ? maxUndoPaidUser : maxUndoFreeUser;
|
||||||
|
|
||||||
debugPrint('[CharacterRollService] Rolled: remaining=$_rollsRemaining, '
|
debugPrint(
|
||||||
'history=${_rollHistory.length}, undo=$_undoRemaining');
|
'[CharacterRollService] Rolled: remaining=$_rollsRemaining, '
|
||||||
|
'history=${_rollHistory.length}, undo=$_undoRemaining',
|
||||||
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -214,8 +218,10 @@ class CharacterRollService {
|
|||||||
final snapshot = _rollHistory.removeAt(0);
|
final snapshot = _rollHistory.removeAt(0);
|
||||||
_undoRemaining--;
|
_undoRemaining--;
|
||||||
|
|
||||||
debugPrint('[CharacterRollService] Undo (paid): '
|
debugPrint(
|
||||||
'remaining=$_undoRemaining, history=${_rollHistory.length}');
|
'[CharacterRollService] Undo (paid): '
|
||||||
|
'remaining=$_undoRemaining, history=${_rollHistory.length}',
|
||||||
|
);
|
||||||
|
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
@@ -241,8 +247,10 @@ class CharacterRollService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (adResult == AdResult.completed || adResult == AdResult.debugSkipped) {
|
if (adResult == AdResult.completed || adResult == AdResult.debugSkipped) {
|
||||||
debugPrint('[CharacterRollService] Undo (free with ad): '
|
debugPrint(
|
||||||
'remaining=$_undoRemaining, history=${_rollHistory.length}');
|
'[CharacterRollService] Undo (free with ad): '
|
||||||
|
'remaining=$_undoRemaining, history=${_rollHistory.length}',
|
||||||
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
|||||||
/// 상자 내용물 생성 및 오픈 로직 담당
|
/// 상자 내용물 생성 및 오픈 로직 담당
|
||||||
class ChestService {
|
class ChestService {
|
||||||
ChestService({DeterministicRandom? rng})
|
ChestService({DeterministicRandom? rng})
|
||||||
: _rng = rng ?? DeterministicRandom(DateTime.now().millisecondsSinceEpoch);
|
: _rng = rng ?? DeterministicRandom(DateTime.now().millisecondsSinceEpoch);
|
||||||
|
|
||||||
final DeterministicRandom _rng;
|
final DeterministicRandom _rng;
|
||||||
|
|
||||||
@@ -100,7 +100,9 @@ class ChestService {
|
|||||||
rarity: rarity,
|
rarity: rarity,
|
||||||
);
|
);
|
||||||
|
|
||||||
debugPrint('[ChestService] Equipment reward: ${item.name} (${rarity.name})');
|
debugPrint(
|
||||||
|
'[ChestService] Equipment reward: ${item.name} (${rarity.name})',
|
||||||
|
);
|
||||||
return ChestReward.equipment(item);
|
return ChestReward.equipment(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +134,10 @@ class ChestService {
|
|||||||
ChestReward _generateGoldReward(int playerLevel) {
|
ChestReward _generateGoldReward(int playerLevel) {
|
||||||
final baseGold = playerLevel * _goldPerLevel;
|
final baseGold = playerLevel * _goldPerLevel;
|
||||||
final variance = _rng.nextInt(_goldVariance * 2 + 1) - _goldVariance;
|
final variance = _rng.nextInt(_goldVariance * 2 + 1) - _goldVariance;
|
||||||
final gold = (baseGold + (baseGold * variance / 100)).round().clamp(10, 99999);
|
final gold = (baseGold + (baseGold * variance / 100)).round().clamp(
|
||||||
|
10,
|
||||||
|
99999,
|
||||||
|
);
|
||||||
|
|
||||||
debugPrint('[ChestService] Gold reward: $gold');
|
debugPrint('[ChestService] Gold reward: $gold');
|
||||||
return ChestReward.gold(gold);
|
return ChestReward.gold(gold);
|
||||||
@@ -142,7 +147,10 @@ class ChestService {
|
|||||||
ChestReward _generateExperienceReward(int playerLevel) {
|
ChestReward _generateExperienceReward(int playerLevel) {
|
||||||
final baseExp = playerLevel * _expPerLevel;
|
final baseExp = playerLevel * _expPerLevel;
|
||||||
final variance = _rng.nextInt(_expVariance * 2 + 1) - _expVariance;
|
final variance = _rng.nextInt(_expVariance * 2 + 1) - _expVariance;
|
||||||
final exp = (baseExp + (baseExp * variance / 100)).round().clamp(10, 999999);
|
final exp = (baseExp + (baseExp * variance / 100)).round().clamp(
|
||||||
|
10,
|
||||||
|
999999,
|
||||||
|
);
|
||||||
|
|
||||||
debugPrint('[ChestService] Experience reward: $exp');
|
debugPrint('[ChestService] Experience reward: $exp');
|
||||||
return ChestReward.experience(exp);
|
return ChestReward.experience(exp);
|
||||||
@@ -208,49 +216,49 @@ class ChestService {
|
|||||||
|
|
||||||
return switch (slot) {
|
return switch (slot) {
|
||||||
EquipmentSlot.weapon => ItemStats(
|
EquipmentSlot.weapon => ItemStats(
|
||||||
atk: baseValue * 2,
|
atk: baseValue * 2,
|
||||||
criRate: 0.01 * (level ~/ 5),
|
criRate: 0.01 * (level ~/ 5),
|
||||||
parryRate: 0.005 * level,
|
parryRate: 0.005 * level,
|
||||||
),
|
),
|
||||||
EquipmentSlot.shield => ItemStats(
|
EquipmentSlot.shield => ItemStats(
|
||||||
def: baseValue,
|
def: baseValue,
|
||||||
blockRate: 0.02 * (level ~/ 3).clamp(1, 10),
|
blockRate: 0.02 * (level ~/ 3).clamp(1, 10),
|
||||||
),
|
),
|
||||||
EquipmentSlot.helm => ItemStats(
|
EquipmentSlot.helm => ItemStats(
|
||||||
def: baseValue ~/ 2,
|
def: baseValue ~/ 2,
|
||||||
magDef: baseValue ~/ 2,
|
magDef: baseValue ~/ 2,
|
||||||
intBonus: level ~/ 10,
|
intBonus: level ~/ 10,
|
||||||
),
|
),
|
||||||
EquipmentSlot.hauberk => ItemStats(def: baseValue, hpBonus: level * 2),
|
EquipmentSlot.hauberk => ItemStats(def: baseValue, hpBonus: level * 2),
|
||||||
EquipmentSlot.brassairts => ItemStats(
|
EquipmentSlot.brassairts => ItemStats(
|
||||||
def: baseValue ~/ 2,
|
def: baseValue ~/ 2,
|
||||||
strBonus: level ~/ 15,
|
strBonus: level ~/ 15,
|
||||||
),
|
),
|
||||||
EquipmentSlot.vambraces => ItemStats(
|
EquipmentSlot.vambraces => ItemStats(
|
||||||
def: baseValue ~/ 2,
|
def: baseValue ~/ 2,
|
||||||
dexBonus: level ~/ 15,
|
dexBonus: level ~/ 15,
|
||||||
),
|
),
|
||||||
EquipmentSlot.gauntlets => ItemStats(
|
EquipmentSlot.gauntlets => ItemStats(
|
||||||
atk: baseValue ~/ 2,
|
atk: baseValue ~/ 2,
|
||||||
def: baseValue ~/ 4,
|
def: baseValue ~/ 4,
|
||||||
),
|
),
|
||||||
EquipmentSlot.gambeson => ItemStats(
|
EquipmentSlot.gambeson => ItemStats(
|
||||||
def: baseValue ~/ 2,
|
def: baseValue ~/ 2,
|
||||||
conBonus: level ~/ 15,
|
conBonus: level ~/ 15,
|
||||||
),
|
),
|
||||||
EquipmentSlot.cuisses => ItemStats(
|
EquipmentSlot.cuisses => ItemStats(
|
||||||
def: baseValue ~/ 2,
|
def: baseValue ~/ 2,
|
||||||
evasion: 0.005 * level,
|
evasion: 0.005 * level,
|
||||||
),
|
),
|
||||||
EquipmentSlot.greaves => ItemStats(
|
EquipmentSlot.greaves => ItemStats(
|
||||||
def: baseValue ~/ 2,
|
def: baseValue ~/ 2,
|
||||||
evasion: 0.003 * level,
|
evasion: 0.003 * level,
|
||||||
),
|
),
|
||||||
EquipmentSlot.sollerets => ItemStats(
|
EquipmentSlot.sollerets => ItemStats(
|
||||||
def: baseValue ~/ 3,
|
def: baseValue ~/ 3,
|
||||||
evasion: 0.002 * level,
|
evasion: 0.002 * level,
|
||||||
dexBonus: level ~/ 20,
|
dexBonus: level ~/ 20,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -210,7 +210,8 @@ class CombatTickService {
|
|||||||
MonsterCombatStats monsterStats,
|
MonsterCombatStats monsterStats,
|
||||||
int totalDamageDealt,
|
int totalDamageDealt,
|
||||||
List<CombatEvent> events,
|
List<CombatEvent> events,
|
||||||
}) _processDotTicks({
|
})
|
||||||
|
_processDotTicks({
|
||||||
required List<DotEffect> activeDoTs,
|
required List<DotEffect> activeDoTs,
|
||||||
required MonsterCombatStats monsterStats,
|
required MonsterCombatStats monsterStats,
|
||||||
required int elapsedMs,
|
required int elapsedMs,
|
||||||
@@ -272,7 +273,8 @@ class CombatTickService {
|
|||||||
int lastPotionUsedMs,
|
int lastPotionUsedMs,
|
||||||
PotionInventory potionInventory,
|
PotionInventory potionInventory,
|
||||||
List<CombatEvent> events,
|
List<CombatEvent> events,
|
||||||
})? _tryEmergencyPotion({
|
})?
|
||||||
|
_tryEmergencyPotion({
|
||||||
required CombatStats playerStats,
|
required CombatStats playerStats,
|
||||||
required PotionInventory potionInventory,
|
required PotionInventory potionInventory,
|
||||||
required int lastPotionUsedMs,
|
required int lastPotionUsedMs,
|
||||||
@@ -371,7 +373,8 @@ class CombatTickService {
|
|||||||
int totalDamageDealt,
|
int totalDamageDealt,
|
||||||
List<CombatEvent> events,
|
List<CombatEvent> events,
|
||||||
bool isFirstPlayerAttack,
|
bool isFirstPlayerAttack,
|
||||||
}) _processPlayerAttack({
|
})
|
||||||
|
_processPlayerAttack({
|
||||||
required GameState state,
|
required GameState state,
|
||||||
required CombatStats playerStats,
|
required CombatStats playerStats,
|
||||||
required MonsterCombatStats monsterStats,
|
required MonsterCombatStats monsterStats,
|
||||||
@@ -508,10 +511,13 @@ class CombatTickService {
|
|||||||
newSkillSystem = skillResult.updatedSkillSystem.startGlobalCooldown();
|
newSkillSystem = skillResult.updatedSkillSystem.startGlobalCooldown();
|
||||||
|
|
||||||
if (skillResult.debuffEffect != null) {
|
if (skillResult.debuffEffect != null) {
|
||||||
newActiveBuffs = newActiveBuffs
|
newActiveBuffs =
|
||||||
.where((d) => d.effect.id != skillResult.debuffEffect!.effect.id)
|
newActiveBuffs
|
||||||
.toList()
|
.where(
|
||||||
..add(skillResult.debuffEffect!);
|
(d) => d.effect.id != skillResult.debuffEffect!.effect.id,
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
..add(skillResult.debuffEffect!);
|
||||||
}
|
}
|
||||||
|
|
||||||
events.add(
|
events.add(
|
||||||
@@ -601,11 +607,8 @@ class CombatTickService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 몬스터 공격 처리
|
/// 몬스터 공격 처리
|
||||||
({
|
({CombatStats playerStats, int totalDamageTaken, List<CombatEvent> events})
|
||||||
CombatStats playerStats,
|
_processMonsterAttack({
|
||||||
int totalDamageTaken,
|
|
||||||
List<CombatEvent> events,
|
|
||||||
}) _processMonsterAttack({
|
|
||||||
required CombatStats playerStats,
|
required CombatStats playerStats,
|
||||||
required MonsterCombatStats monsterStats,
|
required MonsterCombatStats monsterStats,
|
||||||
required List<ActiveBuff> activeDebuffs,
|
required List<ActiveBuff> activeDebuffs,
|
||||||
|
|||||||
@@ -131,9 +131,7 @@ class IAPService {
|
|||||||
final response = await _iap.queryProductDetails(IAPProductIds.all);
|
final response = await _iap.queryProductDetails(IAPProductIds.all);
|
||||||
|
|
||||||
if (response.notFoundIDs.isNotEmpty) {
|
if (response.notFoundIDs.isNotEmpty) {
|
||||||
debugPrint(
|
debugPrint('[IAPService] Products not found: ${response.notFoundIDs}');
|
||||||
'[IAPService] Products not found: ${response.notFoundIDs}',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final product in response.productDetails) {
|
for (final product in response.productDetails) {
|
||||||
@@ -238,14 +236,10 @@ class IAPService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 구매 요청
|
// 구매 요청
|
||||||
final purchaseParam = PurchaseParam(
|
final purchaseParam = PurchaseParam(productDetails: _removeAdsProduct!);
|
||||||
productDetails: _removeAdsProduct!,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final success = await _iap.buyNonConsumable(
|
final success = await _iap.buyNonConsumable(purchaseParam: purchaseParam);
|
||||||
purchaseParam: purchaseParam,
|
|
||||||
);
|
|
||||||
debugPrint('[IAPService] Purchase initiated: $success');
|
debugPrint('[IAPService] Purchase initiated: $success');
|
||||||
return success ? IAPResult.success : IAPResult.failed;
|
return success ? IAPResult.success : IAPResult.failed;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -164,8 +164,9 @@ class ItemService {
|
|||||||
final magDef = hasMagDef ? (def * 0.7).round() : 0;
|
final magDef = hasMagDef ? (def * 0.7).round() : 0;
|
||||||
|
|
||||||
// HP 보너스 (Uncommon 이상)
|
// HP 보너스 (Uncommon 이상)
|
||||||
final hpBonus =
|
final hpBonus = rarity.index >= ItemRarity.uncommon.index
|
||||||
rarity.index >= ItemRarity.uncommon.index ? baseValue ~/ 3 : 0;
|
? baseValue ~/ 3
|
||||||
|
: 0;
|
||||||
|
|
||||||
// CON 보너스 (Rare 이상)
|
// CON 보너스 (Rare 이상)
|
||||||
final conBonus = rarity.index >= ItemRarity.rare.index ? rarity.index : 0;
|
final conBonus = rarity.index >= ItemRarity.rare.index ? rarity.index : 0;
|
||||||
@@ -273,8 +274,7 @@ class ItemService {
|
|||||||
EquipmentSlot.greaves => ItemStats(
|
EquipmentSlot.greaves => ItemStats(
|
||||||
def: def,
|
def: def,
|
||||||
hpBonus: rarity.index >= ItemRarity.rare.index ? baseValue ~/ 3 : 0,
|
hpBonus: rarity.index >= ItemRarity.rare.index ? baseValue ~/ 3 : 0,
|
||||||
conBonus:
|
conBonus: rarity.index >= ItemRarity.rare.index ? rarity.index - 1 : 0,
|
||||||
rarity.index >= ItemRarity.rare.index ? rarity.index - 1 : 0,
|
|
||||||
evasion: rarity.index >= ItemRarity.epic.index ? 0.01 : 0.0,
|
evasion: rarity.index >= ItemRarity.epic.index ? 0.01 : 0.0,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
|||||||
|
|
||||||
/// 판매 처리 결과
|
/// 판매 처리 결과
|
||||||
class SellResult {
|
class SellResult {
|
||||||
const SellResult({
|
const SellResult({required this.state, required this.continuesSelling});
|
||||||
required this.state,
|
|
||||||
required this.continuesSelling,
|
|
||||||
});
|
|
||||||
|
|
||||||
final GameState state;
|
final GameState state;
|
||||||
final bool continuesSelling;
|
final bool continuesSelling;
|
||||||
|
|||||||
@@ -193,7 +193,11 @@ class ProgressService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. 시장/판매/구매 태스크 완료 처리
|
// 5. 시장/판매/구매 태스크 완료 처리
|
||||||
final marketResult = _handleMarketTaskCompletion(nextState, progress, queue);
|
final marketResult = _handleMarketTaskCompletion(
|
||||||
|
nextState,
|
||||||
|
progress,
|
||||||
|
queue,
|
||||||
|
);
|
||||||
if (marketResult.earlyReturn != null) return marketResult.earlyReturn!;
|
if (marketResult.earlyReturn != null) return marketResult.earlyReturn!;
|
||||||
nextState = marketResult.state;
|
nextState = marketResult.state;
|
||||||
progress = marketResult.progress;
|
progress = marketResult.progress;
|
||||||
@@ -209,7 +213,11 @@ class ProgressService {
|
|||||||
|
|
||||||
// 7. 퀘스트 진행 처리
|
// 7. 퀘스트 진행 처리
|
||||||
final questResult = _handleQuestProgress(
|
final questResult = _handleQuestProgress(
|
||||||
nextState, progress, queue, gain, incrementSeconds,
|
nextState,
|
||||||
|
progress,
|
||||||
|
queue,
|
||||||
|
gain,
|
||||||
|
incrementSeconds,
|
||||||
);
|
);
|
||||||
nextState = questResult.state;
|
nextState = questResult.state;
|
||||||
progress = questResult.progress;
|
progress = questResult.progress;
|
||||||
@@ -217,9 +225,7 @@ class ProgressService {
|
|||||||
questDone = questResult.completed;
|
questDone = questResult.completed;
|
||||||
|
|
||||||
// 8. 플롯 진행 및 Act Boss 소환 처리
|
// 8. 플롯 진행 및 Act Boss 소환 처리
|
||||||
progress = _handlePlotProgress(
|
progress = _handlePlotProgress(nextState, progress, gain, incrementSeconds);
|
||||||
nextState, progress, gain, incrementSeconds,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 9. 다음 태스크 디큐/생성
|
// 9. 다음 태스크 디큐/생성
|
||||||
final dequeueResult = _handleTaskDequeue(nextState, progress, queue);
|
final dequeueResult = _handleTaskDequeue(nextState, progress, queue);
|
||||||
@@ -341,7 +347,8 @@ class ProgressService {
|
|||||||
ProgressState progress,
|
ProgressState progress,
|
||||||
QueueState queue,
|
QueueState queue,
|
||||||
ProgressTickResult? earlyReturn,
|
ProgressTickResult? earlyReturn,
|
||||||
}) _handleKillTaskCompletion(
|
})
|
||||||
|
_handleKillTaskCompletion(
|
||||||
GameState state,
|
GameState state,
|
||||||
ProgressState progress,
|
ProgressState progress,
|
||||||
QueueState queue,
|
QueueState queue,
|
||||||
@@ -358,8 +365,9 @@ class ProgressService {
|
|||||||
|
|
||||||
final klass = ClassData.findById(nextState.traits.classId);
|
final klass = ClassData.findById(nextState.traits.classId);
|
||||||
if (klass != null) {
|
if (klass != null) {
|
||||||
final postCombatHealRate =
|
final postCombatHealRate = klass.getPassiveValue(
|
||||||
klass.getPassiveValue(ClassPassiveType.postCombatHeal);
|
ClassPassiveType.postCombatHeal,
|
||||||
|
);
|
||||||
if (postCombatHealRate > 0) {
|
if (postCombatHealRate > 0) {
|
||||||
healAmount += (maxHp * postCombatHealRate).round();
|
healAmount += (maxHp * postCombatHealRate).round();
|
||||||
}
|
}
|
||||||
@@ -446,7 +454,8 @@ class ProgressService {
|
|||||||
ProgressState progress,
|
ProgressState progress,
|
||||||
QueueState queue,
|
QueueState queue,
|
||||||
ProgressTickResult? earlyReturn,
|
ProgressTickResult? earlyReturn,
|
||||||
}) _handleMarketTaskCompletion(
|
})
|
||||||
|
_handleMarketTaskCompletion(
|
||||||
GameState state,
|
GameState state,
|
||||||
ProgressState progress,
|
ProgressState progress,
|
||||||
QueueState queue,
|
QueueState queue,
|
||||||
@@ -520,12 +529,8 @@ class ProgressService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 퀘스트 진행 처리
|
/// 퀘스트 진행 처리
|
||||||
({
|
({GameState state, ProgressState progress, QueueState queue, bool completed})
|
||||||
GameState state,
|
_handleQuestProgress(
|
||||||
ProgressState progress,
|
|
||||||
QueueState queue,
|
|
||||||
bool completed,
|
|
||||||
}) _handleQuestProgress(
|
|
||||||
GameState state,
|
GameState state,
|
||||||
ProgressState progress,
|
ProgressState progress,
|
||||||
QueueState queue,
|
QueueState queue,
|
||||||
@@ -603,7 +608,8 @@ class ProgressService {
|
|||||||
QueueState queue,
|
QueueState queue,
|
||||||
bool actDone,
|
bool actDone,
|
||||||
bool gameComplete,
|
bool gameComplete,
|
||||||
}) _handleTaskDequeue(
|
})
|
||||||
|
_handleTaskDequeue(
|
||||||
GameState state,
|
GameState state,
|
||||||
ProgressState progress,
|
ProgressState progress,
|
||||||
QueueState queue,
|
QueueState queue,
|
||||||
@@ -705,10 +711,7 @@ class ProgressService {
|
|||||||
4 * 1000,
|
4 * 1000,
|
||||||
);
|
);
|
||||||
final updatedProgress = taskResult.progress.copyWith(
|
final updatedProgress = taskResult.progress.copyWith(
|
||||||
currentTask: TaskInfo(
|
currentTask: TaskInfo(caption: taskResult.caption, type: TaskType.market),
|
||||||
caption: taskResult.caption,
|
|
||||||
type: TaskType.market,
|
|
||||||
),
|
|
||||||
currentCombat: null,
|
currentCombat: null,
|
||||||
);
|
);
|
||||||
return (progress: updatedProgress, queue: queue);
|
return (progress: updatedProgress, queue: queue);
|
||||||
@@ -1171,8 +1174,10 @@ class ProgressService {
|
|||||||
final shouldLoseEquipment = roll < lossChancePercent;
|
final shouldLoseEquipment = roll < lossChancePercent;
|
||||||
|
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
print('[Death] Lv$level lossChance=$lossChancePercent% roll=$roll '
|
print(
|
||||||
'shouldLose=$shouldLoseEquipment');
|
'[Death] Lv$level lossChance=$lossChancePercent% roll=$roll '
|
||||||
|
'shouldLose=$shouldLoseEquipment',
|
||||||
|
);
|
||||||
|
|
||||||
if (shouldLoseEquipment) {
|
if (shouldLoseEquipment) {
|
||||||
// 무기(슬롯 0)를 제외한 장착된 장비 중 1개를 제물로 삭제
|
// 무기(슬롯 0)를 제외한 장착된 장비 중 1개를 제물로 삭제
|
||||||
|
|||||||
@@ -346,7 +346,10 @@ class ResurrectionService {
|
|||||||
|
|
||||||
// 해당 슬롯에 아이템 복원
|
// 해당 슬롯에 아이템 복원
|
||||||
final slotIndex = lostSlot.index;
|
final slotIndex = lostSlot.index;
|
||||||
final updatedEquipment = state.equipment.setItemByIndex(slotIndex, lostItem);
|
final updatedEquipment = state.equipment.setItemByIndex(
|
||||||
|
slotIndex,
|
||||||
|
lostItem,
|
||||||
|
);
|
||||||
|
|
||||||
// DeathInfo에서 상실 아이템 정보 제거 (복구 완료)
|
// DeathInfo에서 상실 아이템 정보 제거 (복구 완료)
|
||||||
final updatedDeathInfo = deathInfo.copyWith(
|
final updatedDeathInfo = deathInfo.copyWith(
|
||||||
|
|||||||
@@ -94,8 +94,10 @@ class ReturnRewardsService {
|
|||||||
// 보너스 상자 (광고 시청 시 동일 개수 추가)
|
// 보너스 상자 (광고 시청 시 동일 개수 추가)
|
||||||
final bonusChestCount = chestCount;
|
final bonusChestCount = chestCount;
|
||||||
|
|
||||||
debugPrint('[ReturnRewards] $hoursAway hours away, '
|
debugPrint(
|
||||||
'chests=$chestCount, bonus=$bonusChestCount, paid=$isPaidUser');
|
'[ReturnRewards] $hoursAway hours away, '
|
||||||
|
'chests=$chestCount, bonus=$bonusChestCount, paid=$isPaidUser',
|
||||||
|
);
|
||||||
|
|
||||||
return ReturnChestReward(
|
return ReturnChestReward(
|
||||||
hoursAway: hoursAway,
|
hoursAway: hoursAway,
|
||||||
@@ -125,9 +127,14 @@ class ReturnRewardsService {
|
|||||||
/// [reward] 복귀 보상 데이터
|
/// [reward] 복귀 보상 데이터
|
||||||
/// [playerLevel] 플레이어 레벨
|
/// [playerLevel] 플레이어 레벨
|
||||||
/// Returns: 오픈된 상자 보상 목록
|
/// Returns: 오픈된 상자 보상 목록
|
||||||
List<ChestReward> claimBasicReward(ReturnChestReward reward, int playerLevel) {
|
List<ChestReward> claimBasicReward(
|
||||||
|
ReturnChestReward reward,
|
||||||
|
int playerLevel,
|
||||||
|
) {
|
||||||
if (!reward.hasReward) return [];
|
if (!reward.hasReward) return [];
|
||||||
debugPrint('[ReturnRewards] Basic reward claimed: ${reward.chestCount} chests');
|
debugPrint(
|
||||||
|
'[ReturnRewards] Basic reward claimed: ${reward.chestCount} chests',
|
||||||
|
);
|
||||||
return openChests(reward.chestCount, playerLevel);
|
return openChests(reward.chestCount, playerLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,8 +153,10 @@ class ReturnRewardsService {
|
|||||||
|
|
||||||
// 유료 유저는 무료 보너스
|
// 유료 유저는 무료 보너스
|
||||||
if (IAPService.instance.isAdRemovalPurchased) {
|
if (IAPService.instance.isAdRemovalPurchased) {
|
||||||
debugPrint('[ReturnRewards] Bonus claimed (paid user): '
|
debugPrint(
|
||||||
'${reward.bonusChestCount} chests');
|
'[ReturnRewards] Bonus claimed (paid user): '
|
||||||
|
'${reward.bonusChestCount} chests',
|
||||||
|
);
|
||||||
return openChests(reward.bonusChestCount, playerLevel);
|
return openChests(reward.bonusChestCount, playerLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,8 +170,10 @@ class ReturnRewardsService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (adResult == AdResult.completed || adResult == AdResult.debugSkipped) {
|
if (adResult == AdResult.completed || adResult == AdResult.debugSkipped) {
|
||||||
debugPrint('[ReturnRewards] Bonus claimed (free user with ad): '
|
debugPrint(
|
||||||
'${bonusRewards.length} chests');
|
'[ReturnRewards] Bonus claimed (free user with ad): '
|
||||||
|
'${bonusRewards.length} chests',
|
||||||
|
);
|
||||||
return bonusRewards;
|
return bonusRewards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,46 +57,64 @@ class CombatStats with _$CombatStats {
|
|||||||
// 기본 스탯
|
// 기본 스탯
|
||||||
/// 힘: 물리 공격력 보정
|
/// 힘: 물리 공격력 보정
|
||||||
required int str,
|
required int str,
|
||||||
|
|
||||||
/// 체력: HP, 방어력 보정
|
/// 체력: HP, 방어력 보정
|
||||||
required int con,
|
required int con,
|
||||||
|
|
||||||
/// 민첩: 회피율, 크리티컬율, 명중률, 공격 속도
|
/// 민첩: 회피율, 크리티컬율, 명중률, 공격 속도
|
||||||
required int dex,
|
required int dex,
|
||||||
|
|
||||||
/// 지능: 마법 공격력, MP
|
/// 지능: 마법 공격력, MP
|
||||||
required int intelligence,
|
required int intelligence,
|
||||||
|
|
||||||
/// 지혜: 마법 방어력, MP 회복
|
/// 지혜: 마법 방어력, MP 회복
|
||||||
required int wis,
|
required int wis,
|
||||||
|
|
||||||
/// 매력: 상점 가격, 드롭률 보정
|
/// 매력: 상점 가격, 드롭률 보정
|
||||||
required int cha,
|
required int cha,
|
||||||
// 파생 스탯 (전투용)
|
// 파생 스탯 (전투용)
|
||||||
/// 물리 공격력
|
/// 물리 공격력
|
||||||
required int atk,
|
required int atk,
|
||||||
|
|
||||||
/// 물리 방어력
|
/// 물리 방어력
|
||||||
required int def,
|
required int def,
|
||||||
|
|
||||||
/// 마법 공격력
|
/// 마법 공격력
|
||||||
required int magAtk,
|
required int magAtk,
|
||||||
|
|
||||||
/// 마법 방어력
|
/// 마법 방어력
|
||||||
required int magDef,
|
required int magDef,
|
||||||
|
|
||||||
/// 크리티컬 확률 (0.0 ~ 1.0)
|
/// 크리티컬 확률 (0.0 ~ 1.0)
|
||||||
required double criRate,
|
required double criRate,
|
||||||
|
|
||||||
/// 크리티컬 데미지 배율 (1.5 ~ 3.0)
|
/// 크리티컬 데미지 배율 (1.5 ~ 3.0)
|
||||||
required double criDamage,
|
required double criDamage,
|
||||||
|
|
||||||
/// 회피율 (0.0 ~ 0.5)
|
/// 회피율 (0.0 ~ 0.5)
|
||||||
required double evasion,
|
required double evasion,
|
||||||
|
|
||||||
/// 명중률 (0.8 ~ 1.0)
|
/// 명중률 (0.8 ~ 1.0)
|
||||||
required double accuracy,
|
required double accuracy,
|
||||||
|
|
||||||
/// 방패 방어율 (0.0 ~ 0.4)
|
/// 방패 방어율 (0.0 ~ 0.4)
|
||||||
required double blockRate,
|
required double blockRate,
|
||||||
|
|
||||||
/// 무기로 쳐내기 확률 (0.0 ~ 0.3)
|
/// 무기로 쳐내기 확률 (0.0 ~ 0.3)
|
||||||
required double parryRate,
|
required double parryRate,
|
||||||
|
|
||||||
/// 공격 딜레이 (밀리초)
|
/// 공격 딜레이 (밀리초)
|
||||||
required int attackDelayMs,
|
required int attackDelayMs,
|
||||||
// 자원
|
// 자원
|
||||||
/// 최대 HP
|
/// 최대 HP
|
||||||
required int hpMax,
|
required int hpMax,
|
||||||
|
|
||||||
/// 현재 HP
|
/// 현재 HP
|
||||||
required int hpCurrent,
|
required int hpCurrent,
|
||||||
|
|
||||||
/// 최대 MP
|
/// 최대 MP
|
||||||
required int mpMax,
|
required int mpMax,
|
||||||
|
|
||||||
/// 현재 MP
|
/// 현재 MP
|
||||||
required int mpCurrent,
|
required int mpCurrent,
|
||||||
}) = _CombatStats;
|
}) = _CombatStats;
|
||||||
|
|||||||
@@ -41,36 +41,52 @@ class ItemStats with _$ItemStats {
|
|||||||
const factory ItemStats({
|
const factory ItemStats({
|
||||||
/// 물리 공격력 보정
|
/// 물리 공격력 보정
|
||||||
@Default(0) int atk,
|
@Default(0) int atk,
|
||||||
|
|
||||||
/// 물리 방어력 보정
|
/// 물리 방어력 보정
|
||||||
@Default(0) int def,
|
@Default(0) int def,
|
||||||
|
|
||||||
/// 마법 공격력 보정
|
/// 마법 공격력 보정
|
||||||
@Default(0) int magAtk,
|
@Default(0) int magAtk,
|
||||||
|
|
||||||
/// 마법 방어력 보정
|
/// 마법 방어력 보정
|
||||||
@Default(0) int magDef,
|
@Default(0) int magDef,
|
||||||
|
|
||||||
/// 크리티컬 확률 보정 (0.0 ~ 1.0)
|
/// 크리티컬 확률 보정 (0.0 ~ 1.0)
|
||||||
@Default(0.0) double criRate,
|
@Default(0.0) double criRate,
|
||||||
|
|
||||||
/// 회피율 보정 (0.0 ~ 1.0)
|
/// 회피율 보정 (0.0 ~ 1.0)
|
||||||
@Default(0.0) double evasion,
|
@Default(0.0) double evasion,
|
||||||
|
|
||||||
/// 방패 방어율 (방패 전용, 0.0 ~ 1.0)
|
/// 방패 방어율 (방패 전용, 0.0 ~ 1.0)
|
||||||
@Default(0.0) double blockRate,
|
@Default(0.0) double blockRate,
|
||||||
|
|
||||||
/// 무기 쳐내기 확률 (무기 전용, 0.0 ~ 1.0)
|
/// 무기 쳐내기 확률 (무기 전용, 0.0 ~ 1.0)
|
||||||
@Default(0.0) double parryRate,
|
@Default(0.0) double parryRate,
|
||||||
|
|
||||||
/// HP 보너스
|
/// HP 보너스
|
||||||
@Default(0) int hpBonus,
|
@Default(0) int hpBonus,
|
||||||
|
|
||||||
/// MP 보너스
|
/// MP 보너스
|
||||||
@Default(0) int mpBonus,
|
@Default(0) int mpBonus,
|
||||||
|
|
||||||
/// STR 보너스
|
/// STR 보너스
|
||||||
@Default(0) int strBonus,
|
@Default(0) int strBonus,
|
||||||
|
|
||||||
/// CON 보너스
|
/// CON 보너스
|
||||||
@Default(0) int conBonus,
|
@Default(0) int conBonus,
|
||||||
|
|
||||||
/// DEX 보너스
|
/// DEX 보너스
|
||||||
@Default(0) int dexBonus,
|
@Default(0) int dexBonus,
|
||||||
|
|
||||||
/// INT 보너스
|
/// INT 보너스
|
||||||
@Default(0) int intBonus,
|
@Default(0) int intBonus,
|
||||||
|
|
||||||
/// WIS 보너스
|
/// WIS 보너스
|
||||||
@Default(0) int wisBonus,
|
@Default(0) int wisBonus,
|
||||||
|
|
||||||
/// CHA 보너스
|
/// CHA 보너스
|
||||||
@Default(0) int chaBonus,
|
@Default(0) int chaBonus,
|
||||||
|
|
||||||
/// 무기 공격속도 (밀리초, 무기 전용)
|
/// 무기 공격속도 (밀리초, 무기 전용)
|
||||||
///
|
///
|
||||||
/// 0이면 기본값(1000ms) 사용, 값이 클수록 느린 공격.
|
/// 0이면 기본값(1000ms) 사용, 값이 클수록 느린 공격.
|
||||||
|
|||||||
@@ -121,16 +121,18 @@ class MonetizationState with _$MonetizationState {
|
|||||||
List<Map<String, dynamic>>? _statsListToJson(List<Stats>? stats) {
|
List<Map<String, dynamic>>? _statsListToJson(List<Stats>? stats) {
|
||||||
if (stats == null) return null;
|
if (stats == null) return null;
|
||||||
return stats
|
return stats
|
||||||
.map((s) => {
|
.map(
|
||||||
'str': s.str,
|
(s) => {
|
||||||
'con': s.con,
|
'str': s.str,
|
||||||
'dex': s.dex,
|
'con': s.con,
|
||||||
'int': s.intelligence,
|
'dex': s.dex,
|
||||||
'wis': s.wis,
|
'int': s.intelligence,
|
||||||
'cha': s.cha,
|
'wis': s.wis,
|
||||||
'hpMax': s.hpMax,
|
'cha': s.cha,
|
||||||
'mpMax': s.mpMax,
|
'hpMax': s.hpMax,
|
||||||
})
|
'mpMax': s.mpMax,
|
||||||
|
},
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,9 +62,7 @@ class Potion {
|
|||||||
///
|
///
|
||||||
/// 보유 물약 수량 관리 (쿨타임은 CombatState에서 관리)
|
/// 보유 물약 수량 관리 (쿨타임은 CombatState에서 관리)
|
||||||
class PotionInventory {
|
class PotionInventory {
|
||||||
const PotionInventory({
|
const PotionInventory({this.potions = const {}});
|
||||||
this.potions = const {},
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 보유 물약 (물약 ID → 수량)
|
/// 보유 물약 (물약 ID → 수량)
|
||||||
final Map<String, int> potions;
|
final Map<String, int> potions;
|
||||||
@@ -99,11 +97,7 @@ class PotionInventory {
|
|||||||
/// 빈 인벤토리
|
/// 빈 인벤토리
|
||||||
static const empty = PotionInventory();
|
static const empty = PotionInventory();
|
||||||
|
|
||||||
PotionInventory copyWith({
|
PotionInventory copyWith({Map<String, int>? potions}) {
|
||||||
Map<String, int>? potions,
|
return PotionInventory(potions: potions ?? this.potions);
|
||||||
}) {
|
|
||||||
return PotionInventory(
|
|
||||||
potions: potions ?? this.potions,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ class ChestReward {
|
|||||||
|
|
||||||
/// 장비 보상 생성
|
/// 장비 보상 생성
|
||||||
factory ChestReward.equipment(EquipmentItem item) {
|
factory ChestReward.equipment(EquipmentItem item) {
|
||||||
return ChestReward._(
|
return ChestReward._(type: ChestRewardType.equipment, equipment: item);
|
||||||
type: ChestRewardType.equipment,
|
|
||||||
equipment: item,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 포션 보상 생성
|
/// 포션 보상 생성
|
||||||
@@ -45,18 +42,12 @@ class ChestReward {
|
|||||||
|
|
||||||
/// 골드 보상 생성
|
/// 골드 보상 생성
|
||||||
factory ChestReward.gold(int amount) {
|
factory ChestReward.gold(int amount) {
|
||||||
return ChestReward._(
|
return ChestReward._(type: ChestRewardType.gold, gold: amount);
|
||||||
type: ChestRewardType.gold,
|
|
||||||
gold: amount,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 경험치 보상 생성
|
/// 경험치 보상 생성
|
||||||
factory ChestReward.experience(int amount) {
|
factory ChestReward.experience(int amount) {
|
||||||
return ChestReward._(
|
return ChestReward._(type: ChestRewardType.experience, experience: amount);
|
||||||
type: ChestRewardType.experience,
|
|
||||||
experience: amount,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 보상 타입
|
/// 보상 타입
|
||||||
|
|||||||
@@ -714,7 +714,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 데스크톱 앱바
|
/// 데스크톱 앱바
|
||||||
PreferredSizeWidget _buildDesktopAppBar(BuildContext context, GameState state) {
|
PreferredSizeWidget _buildDesktopAppBar(
|
||||||
|
BuildContext context,
|
||||||
|
GameState state,
|
||||||
|
) {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
backgroundColor: RetroColors.darkBrown,
|
backgroundColor: RetroColors.darkBrown,
|
||||||
title: Text(
|
title: Text(
|
||||||
@@ -969,9 +972,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
// Potions (물약 인벤토리)
|
// Potions (물약 인벤토리)
|
||||||
_buildSectionHeader(game_l10n.uiPotions),
|
_buildSectionHeader(game_l10n.uiPotions),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: PotionInventoryPanel(
|
child: PotionInventoryPanel(inventory: state.potionInventory),
|
||||||
inventory: state.potionInventory,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Encumbrance 바
|
// Encumbrance 바
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
DateTime Function()? now,
|
DateTime Function()? now,
|
||||||
StatisticsStorage? statisticsStorage,
|
StatisticsStorage? statisticsStorage,
|
||||||
HallOfFameStorage? hallOfFameStorage,
|
HallOfFameStorage? hallOfFameStorage,
|
||||||
}) : _tickInterval = tickInterval,
|
}) : _tickInterval = tickInterval,
|
||||||
_now = now ?? DateTime.now {
|
_now = now ?? DateTime.now {
|
||||||
// 매니저 초기화
|
// 매니저 초기화
|
||||||
_statisticsManager = GameStatisticsManager(
|
_statisticsManager = GameStatisticsManager(
|
||||||
statisticsStorage: statisticsStorage,
|
statisticsStorage: statisticsStorage,
|
||||||
@@ -136,9 +136,9 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
int get speedBoostDuration => _speedBoostManager.speedBoostDuration;
|
int get speedBoostDuration => _speedBoostManager.speedBoostDuration;
|
||||||
|
|
||||||
int get speedBoostRemainingSeconds => _speedBoostManager.getRemainingSeconds(
|
int get speedBoostRemainingSeconds => _speedBoostManager.getRemainingSeconds(
|
||||||
_monetization,
|
_monetization,
|
||||||
_state?.skillSystem.elapsedMs ?? 0,
|
_state?.skillSystem.elapsedMs ?? 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
int get currentSpeedMultiplier =>
|
int get currentSpeedMultiplier =>
|
||||||
_speedBoostManager.getCurrentSpeedMultiplier(_loop);
|
_speedBoostManager.getCurrentSpeedMultiplier(_loop);
|
||||||
@@ -472,12 +472,12 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
|
|
||||||
/// 속도 부스트 활성화 (광고 시청 후)
|
/// 속도 부스트 활성화 (광고 시청 후)
|
||||||
Future<bool> activateSpeedBoost() async {
|
Future<bool> activateSpeedBoost() async {
|
||||||
final (success, updatedMonetization) =
|
final (success, updatedMonetization) = await _speedBoostManager
|
||||||
await _speedBoostManager.activateSpeedBoost(
|
.activateSpeedBoost(
|
||||||
loop: _loop,
|
loop: _loop,
|
||||||
monetization: _monetization,
|
monetization: _monetization,
|
||||||
currentElapsedMs: _state?.skillSystem.elapsedMs ?? 0,
|
currentElapsedMs: _state?.skillSystem.elapsedMs ?? 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
_monetization = updatedMonetization;
|
_monetization = updatedMonetization;
|
||||||
|
|||||||
@@ -308,15 +308,14 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
// 핸들 바
|
// 핸들 바
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8, bottom: 4),
|
padding: const EdgeInsets.only(top: 8, bottom: 4),
|
||||||
child: Container(
|
child: Container(width: 60, height: 4, color: border),
|
||||||
width: 60,
|
|
||||||
height: 4,
|
|
||||||
color: border,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
// 헤더
|
// 헤더
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: RetroColors.panelBgOf(context),
|
color: RetroColors.panelBgOf(context),
|
||||||
@@ -515,7 +514,8 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
],
|
],
|
||||||
|
|
||||||
// === 디버그 도구 섹션 ===
|
// === 디버그 도구 섹션 ===
|
||||||
if (kDebugMode && widget.onCreateTestCharacter != null) ...[
|
if (kDebugMode &&
|
||||||
|
widget.onCreateTestCharacter != null) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
RetroMenuSection(
|
RetroMenuSection(
|
||||||
title: L10n.of(context).debugToolsTitle,
|
title: L10n.of(context).debugToolsTitle,
|
||||||
@@ -526,7 +526,9 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
icon: Icons.science,
|
icon: Icons.science,
|
||||||
iconColor: RetroColors.warningOf(context),
|
iconColor: RetroColors.warningOf(context),
|
||||||
label: L10n.of(context).debugCreateTestCharacter,
|
label: L10n.of(context).debugCreateTestCharacter,
|
||||||
subtitle: L10n.of(context).debugCreateTestCharacterDesc,
|
subtitle: L10n.of(
|
||||||
|
context,
|
||||||
|
).debugCreateTestCharacterDesc,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
_showTestCharacterDialog(context);
|
_showTestCharacterDialog(context);
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ import 'package:asciineverdie/src/core/storage/statistics_storage.dart';
|
|||||||
/// 세션 통계와 누적 통계를 관리하고, 게임 상태 변화에 따라
|
/// 세션 통계와 누적 통계를 관리하고, 게임 상태 변화에 따라
|
||||||
/// 통계를 자동 업데이트합니다.
|
/// 통계를 자동 업데이트합니다.
|
||||||
class GameStatisticsManager {
|
class GameStatisticsManager {
|
||||||
GameStatisticsManager({
|
GameStatisticsManager({StatisticsStorage? statisticsStorage})
|
||||||
StatisticsStorage? statisticsStorage,
|
: _statisticsStorage = statisticsStorage ?? StatisticsStorage();
|
||||||
}) : _statisticsStorage = statisticsStorage ?? StatisticsStorage();
|
|
||||||
|
|
||||||
final StatisticsStorage _statisticsStorage;
|
final StatisticsStorage _statisticsStorage;
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,8 @@ import 'package:flutter/foundation.dart';
|
|||||||
///
|
///
|
||||||
/// 게임 클리어 시 캐릭터 등록, 테스트 캐릭터 생성 등을 담당합니다.
|
/// 게임 클리어 시 캐릭터 등록, 테스트 캐릭터 생성 등을 담당합니다.
|
||||||
class HallOfFameManager {
|
class HallOfFameManager {
|
||||||
HallOfFameManager({
|
HallOfFameManager({HallOfFameStorage? hallOfFameStorage})
|
||||||
HallOfFameStorage? hallOfFameStorage,
|
: _hallOfFameStorage = hallOfFameStorage ?? HallOfFameStorage();
|
||||||
}) : _hallOfFameStorage = hallOfFameStorage ?? HallOfFameStorage();
|
|
||||||
|
|
||||||
final HallOfFameStorage _hallOfFameStorage;
|
final HallOfFameStorage _hallOfFameStorage;
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,8 @@ class ResurrectionManager {
|
|||||||
updatedMonetization = monetization.copyWith(autoReviveEndMs: buffEndMs);
|
updatedMonetization = monetization.copyWith(autoReviveEndMs: buffEndMs);
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'[Resurrection] Ad revive complete, auto-revive buff until $buffEndMs ms');
|
'[Resurrection] Ad revive complete, auto-revive buff until $buffEndMs ms',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 유료 유저는 광고 없이 부활
|
// 유료 유저는 광고 없이 부활
|
||||||
|
|||||||
@@ -49,8 +49,10 @@ class ReturnRewardsManager {
|
|||||||
|
|
||||||
if (reward.hasReward) {
|
if (reward.hasReward) {
|
||||||
_pendingReturnReward = reward;
|
_pendingReturnReward = reward;
|
||||||
debugPrint('[ReturnRewards] Reward available: ${reward.chestCount} chests, '
|
debugPrint(
|
||||||
'${reward.hoursAway} hours away');
|
'[ReturnRewards] Reward available: ${reward.chestCount} chests, '
|
||||||
|
'${reward.hoursAway} hours away',
|
||||||
|
);
|
||||||
|
|
||||||
// UI에서 다이얼로그 표시를 위해 콜백 호출
|
// UI에서 다이얼로그 표시를 위해 콜백 호출
|
||||||
// startNew 후에 호출하도록 딜레이
|
// startNew 후에 호출하도록 딜레이
|
||||||
@@ -91,11 +93,13 @@ class ReturnRewardsManager {
|
|||||||
loop?.replaceState(updatedState); // ProgressLoop 상태도 업데이트
|
loop?.replaceState(updatedState); // ProgressLoop 상태도 업데이트
|
||||||
|
|
||||||
// 저장
|
// 저장
|
||||||
unawaited(saveManager.saveState(
|
unawaited(
|
||||||
updatedState,
|
saveManager.saveState(
|
||||||
cheatsEnabled: cheatsEnabled,
|
updatedState,
|
||||||
monetization: monetization,
|
cheatsEnabled: cheatsEnabled,
|
||||||
));
|
monetization: monetization,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
_pendingReturnReward = null;
|
_pendingReturnReward = null;
|
||||||
|
|
||||||
@@ -129,18 +133,19 @@ class ReturnRewardsManager {
|
|||||||
reward.equipment!.itemWeight > currentItem.itemWeight) {
|
reward.equipment!.itemWeight > currentItem.itemWeight) {
|
||||||
debugPrint('[ReturnRewards] Equipped: ${reward.equipment!.name}');
|
debugPrint('[ReturnRewards] Equipped: ${reward.equipment!.name}');
|
||||||
return state.copyWith(
|
return state.copyWith(
|
||||||
equipment: state.equipment.setItemByIndex(
|
equipment: state.equipment.setItemByIndex(slotIndex, reward.equipment!),
|
||||||
slotIndex,
|
|
||||||
reward.equipment!,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 더 좋지 않으면 판매 (골드로 변환)
|
// 더 좋지 않으면 판매 (골드로 변환)
|
||||||
final sellPrice =
|
final sellPrice = (reward.equipment!.level * 50 * 0.3).round().clamp(
|
||||||
(reward.equipment!.level * 50 * 0.3).round().clamp(1, 99999);
|
1,
|
||||||
debugPrint('[ReturnRewards] Sold: ${reward.equipment!.name} '
|
99999,
|
||||||
'for $sellPrice gold');
|
);
|
||||||
|
debugPrint(
|
||||||
|
'[ReturnRewards] Sold: ${reward.equipment!.name} '
|
||||||
|
'for $sellPrice gold',
|
||||||
|
);
|
||||||
return state.copyWith(
|
return state.copyWith(
|
||||||
inventory: state.inventory.copyWith(
|
inventory: state.inventory.copyWith(
|
||||||
gold: state.inventory.gold + sellPrice,
|
gold: state.inventory.gold + sellPrice,
|
||||||
@@ -152,8 +157,10 @@ class ReturnRewardsManager {
|
|||||||
GameState _applyPotionReward(GameState state, ChestReward reward) {
|
GameState _applyPotionReward(GameState state, ChestReward reward) {
|
||||||
if (reward.potionId == null) return state;
|
if (reward.potionId == null) return state;
|
||||||
|
|
||||||
debugPrint('[ReturnRewards] Added potion: ${reward.potionId} '
|
debugPrint(
|
||||||
'x${reward.potionCount}');
|
'[ReturnRewards] Added potion: ${reward.potionId} '
|
||||||
|
'x${reward.potionCount}',
|
||||||
|
);
|
||||||
return state.copyWith(
|
return state.copyWith(
|
||||||
potionInventory: state.potionInventory.addPotion(
|
potionInventory: state.potionInventory.addPotion(
|
||||||
reward.potionId!,
|
reward.potionId!,
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ class SpeedBoostManager {
|
|||||||
SpeedBoostManager({
|
SpeedBoostManager({
|
||||||
required bool Function() cheatsEnabledGetter,
|
required bool Function() cheatsEnabledGetter,
|
||||||
required Future<List<int>> Function() getAvailableSpeeds,
|
required Future<List<int>> Function() getAvailableSpeeds,
|
||||||
}) : _cheatsEnabledGetter = cheatsEnabledGetter,
|
}) : _cheatsEnabledGetter = cheatsEnabledGetter,
|
||||||
_getAvailableSpeeds = getAvailableSpeeds;
|
_getAvailableSpeeds = getAvailableSpeeds;
|
||||||
|
|
||||||
final bool Function() _cheatsEnabledGetter;
|
final bool Function() _cheatsEnabledGetter;
|
||||||
final Future<List<int>> Function() _getAvailableSpeeds;
|
final Future<List<int>> Function() _getAvailableSpeeds;
|
||||||
@@ -52,7 +52,10 @@ class SpeedBoostManager {
|
|||||||
int get speedBoostDuration => _speedBoostDuration;
|
int get speedBoostDuration => _speedBoostDuration;
|
||||||
|
|
||||||
/// 속도 부스트 남은 시간 (초) - 게임 시간(elapsedMs) 기준 계산
|
/// 속도 부스트 남은 시간 (초) - 게임 시간(elapsedMs) 기준 계산
|
||||||
int getRemainingSeconds(MonetizationState monetization, int currentElapsedMs) {
|
int getRemainingSeconds(
|
||||||
|
MonetizationState monetization,
|
||||||
|
int currentElapsedMs,
|
||||||
|
) {
|
||||||
if (!_isSpeedBoostActive) return 0;
|
if (!_isSpeedBoostActive) return 0;
|
||||||
final endMs = monetization.speedBoostEndMs;
|
final endMs = monetization.speedBoostEndMs;
|
||||||
if (endMs == null) return 0;
|
if (endMs == null) return 0;
|
||||||
@@ -203,7 +206,10 @@ class SpeedBoostManager {
|
|||||||
if (_isSpeedBoostActive) {
|
if (_isSpeedBoostActive) {
|
||||||
// 부스트 상태: 부스트 배속만 사용, 기본 배속 저장
|
// 부스트 상태: 부스트 배속만 사용, 기본 배속 저장
|
||||||
savedSpeedMultiplier = baseSpeed;
|
savedSpeedMultiplier = baseSpeed;
|
||||||
return (speeds: [speedBoostMultiplier], initialSpeed: speedBoostMultiplier);
|
return (
|
||||||
|
speeds: [speedBoostMultiplier],
|
||||||
|
initialSpeed: speedBoostMultiplier,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// 일반 상태: 기본 배속 사용
|
// 일반 상태: 기본 배속 사용
|
||||||
return (speeds: baseAvailableSpeeds, initialSpeed: baseSpeed);
|
return (speeds: baseAvailableSpeeds, initialSpeed: baseSpeed);
|
||||||
|
|||||||
@@ -37,9 +37,7 @@ class InventoryPage extends StatelessWidget {
|
|||||||
_buildSectionHeader(context, l10n.uiPotions),
|
_buildSectionHeader(context, l10n.uiPotions),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: PotionInventoryPanel(
|
child: PotionInventoryPanel(inventory: potionInventory),
|
||||||
inventory: potionInventory,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// 무게 (Encumbrance)
|
// 무게 (Encumbrance)
|
||||||
|
|||||||
@@ -147,7 +147,11 @@ class _SkillTile extends StatelessWidget {
|
|||||||
if (isOnCooldown)
|
if (isOnCooldown)
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.only(right: 8),
|
padding: EdgeInsets.only(right: 8),
|
||||||
child: Icon(Icons.hourglass_empty, size: 14, color: Colors.orange),
|
child: Icon(
|
||||||
|
Icons.hourglass_empty,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
_RankBadge(rank: rank),
|
_RankBadge(rank: rank),
|
||||||
],
|
],
|
||||||
@@ -273,10 +277,12 @@ class _SkillStatsGrid extends StatelessWidget {
|
|||||||
|
|
||||||
// 공통: MP, 쿨타임
|
// 공통: MP, 쿨타임
|
||||||
entries.add(_StatEntry(l10n.skillMpCost, '${skill.mpCost}'));
|
entries.add(_StatEntry(l10n.skillMpCost, '${skill.mpCost}'));
|
||||||
entries.add(_StatEntry(
|
entries.add(
|
||||||
l10n.skillCooldown,
|
_StatEntry(
|
||||||
'${(skill.cooldownMs / 1000).toStringAsFixed(1)}${l10n.skillSeconds}',
|
l10n.skillCooldown,
|
||||||
));
|
'${(skill.cooldownMs / 1000).toStringAsFixed(1)}${l10n.skillSeconds}',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// 타입별 스탯 추가
|
// 타입별 스탯 추가
|
||||||
switch (skill.type) {
|
switch (skill.type) {
|
||||||
@@ -309,33 +315,40 @@ class _SkillStatsGrid extends StatelessWidget {
|
|||||||
|
|
||||||
// DOT 정보
|
// DOT 정보
|
||||||
if (skill.isDot && skill.baseDotDamage != null) {
|
if (skill.isDot && skill.baseDotDamage != null) {
|
||||||
final dotDps = skill.baseDotDamage! *
|
final dotDps =
|
||||||
|
skill.baseDotDamage! *
|
||||||
(skill.baseDotDurationMs! / skill.baseDotTickMs!);
|
(skill.baseDotDurationMs! / skill.baseDotTickMs!);
|
||||||
entries.add(_StatEntry(l10n.skillDot, '${dotDps.round()}'));
|
entries.add(_StatEntry(l10n.skillDot, '${dotDps.round()}'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// HP 흡수
|
// HP 흡수
|
||||||
if (skill.lifestealPercent > 0) {
|
if (skill.lifestealPercent > 0) {
|
||||||
entries.add(_StatEntry(
|
entries.add(
|
||||||
l10n.skillLifesteal,
|
_StatEntry(
|
||||||
'${(skill.lifestealPercent * 100).round()}%',
|
l10n.skillLifesteal,
|
||||||
));
|
'${(skill.lifestealPercent * 100).round()}%',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 방어 무시
|
// 방어 무시
|
||||||
if (skill.targetDefReduction > 0) {
|
if (skill.targetDefReduction > 0) {
|
||||||
entries.add(_StatEntry(
|
entries.add(
|
||||||
l10n.skillDefPen,
|
_StatEntry(
|
||||||
'${(skill.targetDefReduction * 100).round()}%',
|
l10n.skillDefPen,
|
||||||
));
|
'${(skill.targetDefReduction * 100).round()}%',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 자해 데미지
|
// 자해 데미지
|
||||||
if (skill.selfDamagePercent > 0) {
|
if (skill.selfDamagePercent > 0) {
|
||||||
entries.add(_StatEntry(
|
entries.add(
|
||||||
l10n.skillSelfDmg,
|
_StatEntry(
|
||||||
'${(skill.selfDamagePercent * 100).round()}%',
|
l10n.skillSelfDmg,
|
||||||
));
|
'${(skill.selfDamagePercent * 100).round()}%',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,10 +360,12 @@ class _SkillStatsGrid extends StatelessWidget {
|
|||||||
|
|
||||||
// % 회복
|
// % 회복
|
||||||
if (skill.healPercent > 0) {
|
if (skill.healPercent > 0) {
|
||||||
entries.add(_StatEntry(
|
entries.add(
|
||||||
l10n.skillHealPercent,
|
_StatEntry(
|
||||||
'${(skill.healPercent * 100).round()}%',
|
l10n.skillHealPercent,
|
||||||
));
|
'${(skill.healPercent * 100).round()}%',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MP 회복
|
// MP 회복
|
||||||
@@ -361,10 +376,12 @@ class _SkillStatsGrid extends StatelessWidget {
|
|||||||
// 부가 버프
|
// 부가 버프
|
||||||
if (skill.buff != null) {
|
if (skill.buff != null) {
|
||||||
final buff = skill.buff!;
|
final buff = skill.buff!;
|
||||||
entries.add(_StatEntry(
|
entries.add(
|
||||||
l10n.skillBuffDuration,
|
_StatEntry(
|
||||||
'${(buff.durationMs / 1000).round()}${l10n.skillSeconds}',
|
l10n.skillBuffDuration,
|
||||||
));
|
'${(buff.durationMs / 1000).round()}${l10n.skillSeconds}',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,39 +390,49 @@ class _SkillStatsGrid extends StatelessWidget {
|
|||||||
final buff = skill.buff!;
|
final buff = skill.buff!;
|
||||||
|
|
||||||
// 지속시간
|
// 지속시간
|
||||||
entries.add(_StatEntry(
|
entries.add(
|
||||||
l10n.skillBuffDuration,
|
_StatEntry(
|
||||||
'${(buff.durationMs / 1000).round()}${l10n.skillSeconds}',
|
l10n.skillBuffDuration,
|
||||||
));
|
'${(buff.durationMs / 1000).round()}${l10n.skillSeconds}',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// 각 보정치
|
// 각 보정치
|
||||||
if (buff.atkModifier != 0) {
|
if (buff.atkModifier != 0) {
|
||||||
final sign = buff.atkModifier > 0 ? '+' : '';
|
final sign = buff.atkModifier > 0 ? '+' : '';
|
||||||
entries.add(_StatEntry(
|
entries.add(
|
||||||
l10n.skillAtkMod,
|
_StatEntry(
|
||||||
'$sign${(buff.atkModifier * 100).round()}%',
|
l10n.skillAtkMod,
|
||||||
));
|
'$sign${(buff.atkModifier * 100).round()}%',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (buff.defModifier != 0) {
|
if (buff.defModifier != 0) {
|
||||||
final sign = buff.defModifier > 0 ? '+' : '';
|
final sign = buff.defModifier > 0 ? '+' : '';
|
||||||
entries.add(_StatEntry(
|
entries.add(
|
||||||
l10n.skillDefMod,
|
_StatEntry(
|
||||||
'$sign${(buff.defModifier * 100).round()}%',
|
l10n.skillDefMod,
|
||||||
));
|
'$sign${(buff.defModifier * 100).round()}%',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (buff.criRateModifier != 0) {
|
if (buff.criRateModifier != 0) {
|
||||||
final sign = buff.criRateModifier > 0 ? '+' : '';
|
final sign = buff.criRateModifier > 0 ? '+' : '';
|
||||||
entries.add(_StatEntry(
|
entries.add(
|
||||||
l10n.skillCriMod,
|
_StatEntry(
|
||||||
'$sign${(buff.criRateModifier * 100).round()}%',
|
l10n.skillCriMod,
|
||||||
));
|
'$sign${(buff.criRateModifier * 100).round()}%',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (buff.evasionModifier != 0) {
|
if (buff.evasionModifier != 0) {
|
||||||
final sign = buff.evasionModifier > 0 ? '+' : '';
|
final sign = buff.evasionModifier > 0 ? '+' : '';
|
||||||
entries.add(_StatEntry(
|
entries.add(
|
||||||
l10n.skillEvaMod,
|
_StatEntry(
|
||||||
'$sign${(buff.evasionModifier * 100).round()}%',
|
l10n.skillEvaMod,
|
||||||
));
|
'$sign${(buff.evasionModifier * 100).round()}%',
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,23 +441,23 @@ class _SkillStatsGrid extends StatelessWidget {
|
|||||||
final buff = skill.buff!;
|
final buff = skill.buff!;
|
||||||
|
|
||||||
// 지속시간
|
// 지속시간
|
||||||
entries.add(_StatEntry(
|
entries.add(
|
||||||
l10n.skillBuffDuration,
|
_StatEntry(
|
||||||
'${(buff.durationMs / 1000).round()}${l10n.skillSeconds}',
|
l10n.skillBuffDuration,
|
||||||
));
|
'${(buff.durationMs / 1000).round()}${l10n.skillSeconds}',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// 디버프 효과 (보통 음수)
|
// 디버프 효과 (보통 음수)
|
||||||
if (buff.atkModifier != 0) {
|
if (buff.atkModifier != 0) {
|
||||||
entries.add(_StatEntry(
|
entries.add(
|
||||||
l10n.skillAtkMod,
|
_StatEntry(l10n.skillAtkMod, '${(buff.atkModifier * 100).round()}%'),
|
||||||
'${(buff.atkModifier * 100).round()}%',
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
if (buff.defModifier != 0) {
|
if (buff.defModifier != 0) {
|
||||||
entries.add(_StatEntry(
|
entries.add(
|
||||||
l10n.skillDefMod,
|
_StatEntry(l10n.skillDefMod, '${(buff.defModifier * 100).round()}%'),
|
||||||
'${(buff.defModifier * 100).round()}%',
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -333,7 +333,8 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
TextSpan(
|
TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '[${_getSlotName(deathInfo.lostItemSlot)}] ',
|
text:
|
||||||
|
'[${_getSlotName(deathInfo.lostItemSlot)}] ',
|
||||||
style: TextStyle(color: muted),
|
style: TextStyle(color: muted),
|
||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
@@ -485,7 +486,10 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
border: Border(
|
border: Border(
|
||||||
top: BorderSide(color: gold, width: 3),
|
top: BorderSide(color: gold, width: 3),
|
||||||
left: BorderSide(color: gold, width: 3),
|
left: BorderSide(color: gold, width: 3),
|
||||||
bottom: BorderSide(color: goldDark.withValues(alpha: 0.8), width: 3),
|
bottom: BorderSide(
|
||||||
|
color: goldDark.withValues(alpha: 0.8),
|
||||||
|
width: 3,
|
||||||
|
),
|
||||||
right: BorderSide(color: goldDark.withValues(alpha: 0.8), width: 3),
|
right: BorderSide(color: goldDark.withValues(alpha: 0.8), width: 3),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -495,10 +499,7 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text('✨', style: TextStyle(fontSize: 20, color: gold)),
|
||||||
'✨',
|
|
||||||
style: TextStyle(fontSize: 20, color: gold),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
l10n.deathAdRevive.toUpperCase(),
|
l10n.deathAdRevive.toUpperCase(),
|
||||||
@@ -551,7 +552,8 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
_buildBenefitRow(
|
_buildBenefitRow(
|
||||||
context,
|
context,
|
||||||
icon: '🔄',
|
icon: '🔄',
|
||||||
text: '${l10n.deathAdReviveItem}: ${deathInfo.lostItemName}',
|
text:
|
||||||
|
'${l10n.deathAdReviveItem}: ${deathInfo.lostItemName}',
|
||||||
color: itemRarityColor,
|
color: itemRarityColor,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ class RetroOptionItem extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected ? gold.withValues(alpha: 0.15) : Colors.transparent,
|
color: isSelected
|
||||||
|
? gold.withValues(alpha: 0.15)
|
||||||
|
: Colors.transparent,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isSelected ? gold : border,
|
color: isSelected ? gold : border,
|
||||||
width: isSelected ? 2 : 1,
|
width: isSelected ? 2 : 1,
|
||||||
@@ -101,7 +103,9 @@ class RetroOptionItem extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'PressStart2P',
|
fontFamily: 'PressStart2P',
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
color: isSelected ? gold : RetroColors.textPrimaryOf(context),
|
color: isSelected
|
||||||
|
? gold
|
||||||
|
: RetroColors.textPrimaryOf(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -300,7 +300,9 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
child: _buildBuffChip(
|
child: _buildBuffChip(
|
||||||
icon: '⚡',
|
icon: '⚡',
|
||||||
label: '${widget.adSpeedMultiplier}x',
|
label: '${widget.adSpeedMultiplier}x',
|
||||||
remainingMs: widget.isPaidUser ? -1 : _speedBoostRemainingMs,
|
remainingMs: widget.isPaidUser
|
||||||
|
? -1
|
||||||
|
: _speedBoostRemainingMs,
|
||||||
color: Colors.orange,
|
color: Colors.orange,
|
||||||
isPermanent: widget.isPaidUser,
|
isPermanent: widget.isPaidUser,
|
||||||
),
|
),
|
||||||
@@ -401,7 +403,9 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
child: SizedBox.expand(
|
child: SizedBox.expand(
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: ratio.clamp(0.0, 1.0),
|
value: ratio.clamp(0.0, 1.0),
|
||||||
backgroundColor: Colors.red.withValues(alpha: 0.2),
|
backgroundColor: Colors.red.withValues(
|
||||||
|
alpha: 0.2,
|
||||||
|
),
|
||||||
valueColor: AlwaysStoppedAnimation(
|
valueColor: AlwaysStoppedAnimation(
|
||||||
isLow ? Colors.red : Colors.red.shade600,
|
isLow ? Colors.red : Colors.red.shade600,
|
||||||
),
|
),
|
||||||
@@ -502,7 +506,9 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
child: SizedBox.expand(
|
child: SizedBox.expand(
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: ratio.clamp(0.0, 1.0),
|
value: ratio.clamp(0.0, 1.0),
|
||||||
backgroundColor: Colors.blue.withValues(alpha: 0.2),
|
backgroundColor: Colors.blue.withValues(
|
||||||
|
alpha: 0.2,
|
||||||
|
),
|
||||||
valueColor: AlwaysStoppedAnimation(
|
valueColor: AlwaysStoppedAnimation(
|
||||||
Colors.blue.shade600,
|
Colors.blue.shade600,
|
||||||
),
|
),
|
||||||
@@ -619,7 +625,10 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
color: Colors.black.withValues(alpha: 0.8),
|
color: Colors.black.withValues(alpha: 0.8),
|
||||||
blurRadius: 2,
|
blurRadius: 2,
|
||||||
),
|
),
|
||||||
const Shadow(color: Colors.black, blurRadius: 4),
|
const Shadow(
|
||||||
|
color: Colors.black,
|
||||||
|
blurRadius: 4,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -783,8 +792,9 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
),
|
),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: _getStatusMessage(),
|
text: _getStatusMessage(),
|
||||||
style:
|
style: gradeColor != null
|
||||||
gradeColor != null ? TextStyle(color: gradeColor) : null,
|
? TextStyle(color: gradeColor)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -844,10 +854,7 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(icon, style: TextStyle(fontSize: 12, color: color)),
|
||||||
icon,
|
|
||||||
style: TextStyle(fontSize: 12, color: color),
|
|
||||||
),
|
|
||||||
if (label != null) ...[
|
if (label != null) ...[
|
||||||
const SizedBox(width: 2),
|
const SizedBox(width: 2),
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -326,8 +326,10 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
|
|||||||
height: 14,
|
height: 14,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: emptyColor.withValues(alpha: 0.3),
|
color: emptyColor.withValues(alpha: 0.3),
|
||||||
border:
|
border: Border.all(
|
||||||
Border.all(color: RetroColors.panelBorderOuter, width: 1),
|
color: RetroColors.panelBorderOuter,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: List.generate(segmentCount, (index) {
|
children: List.generate(segmentCount, (index) {
|
||||||
@@ -341,10 +343,8 @@ class _HpMpBarState extends State<HpMpBar> with TickerProviderStateMixin {
|
|||||||
border: Border(
|
border: Border(
|
||||||
right: index < segmentCount - 1
|
right: index < segmentCount - 1
|
||||||
? BorderSide(
|
? BorderSide(
|
||||||
color:
|
color: RetroColors.panelBorderOuter
|
||||||
RetroColors.panelBorderOuter.withValues(
|
.withValues(alpha: 0.3),
|
||||||
alpha: 0.3,
|
|
||||||
),
|
|
||||||
width: 1,
|
width: 1,
|
||||||
)
|
)
|
||||||
: BorderSide.none,
|
: BorderSide.none,
|
||||||
|
|||||||
@@ -4,11 +4,7 @@ import 'package:asciineverdie/src/shared/retro_colors.dart';
|
|||||||
|
|
||||||
/// 메뉴 섹션 타이틀
|
/// 메뉴 섹션 타이틀
|
||||||
class RetroMenuSection extends StatelessWidget {
|
class RetroMenuSection extends StatelessWidget {
|
||||||
const RetroMenuSection({
|
const RetroMenuSection({super.key, required this.title, this.color});
|
||||||
super.key,
|
|
||||||
required this.title,
|
|
||||||
this.color,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final Color? color;
|
final Color? color;
|
||||||
@@ -182,10 +178,7 @@ class RetroSpeedChip extends StatelessWidget {
|
|||||||
if (isAdBased && !isSelected && !isDisabled)
|
if (isAdBased && !isSelected && !isDisabled)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 2),
|
padding: const EdgeInsets.only(right: 2),
|
||||||
child: Text(
|
child: Text('▶', style: TextStyle(fontSize: 7, color: warning)),
|
||||||
'▶',
|
|
||||||
style: TextStyle(fontSize: 7, color: warning),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${speed}x',
|
'${speed}x',
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ import 'package:asciineverdie/src/core/model/potion.dart';
|
|||||||
/// 보유 중인 물약 목록과 수량을 표시.
|
/// 보유 중인 물약 목록과 수량을 표시.
|
||||||
/// HP 물약은 빨간색, MP 물약은 파란색으로 구분.
|
/// HP 물약은 빨간색, MP 물약은 파란색으로 구분.
|
||||||
class PotionInventoryPanel extends StatelessWidget {
|
class PotionInventoryPanel extends StatelessWidget {
|
||||||
const PotionInventoryPanel({
|
const PotionInventoryPanel({super.key, required this.inventory});
|
||||||
super.key,
|
|
||||||
required this.inventory,
|
|
||||||
});
|
|
||||||
|
|
||||||
final PotionInventory inventory;
|
final PotionInventory inventory;
|
||||||
|
|
||||||
@@ -38,10 +35,7 @@ class PotionInventoryPanel extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final entry = potionEntries[index];
|
final entry = potionEntries[index];
|
||||||
return _PotionRow(
|
return _PotionRow(potion: entry.potion, quantity: entry.quantity);
|
||||||
potion: entry.potion,
|
|
||||||
quantity: entry.quantity,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -82,10 +76,7 @@ class _PotionEntry {
|
|||||||
|
|
||||||
/// 물약 행 위젯
|
/// 물약 행 위젯
|
||||||
class _PotionRow extends StatelessWidget {
|
class _PotionRow extends StatelessWidget {
|
||||||
const _PotionRow({
|
const _PotionRow({required this.potion, required this.quantity});
|
||||||
required this.potion,
|
|
||||||
required this.quantity,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Potion potion;
|
final Potion potion;
|
||||||
final int quantity;
|
final int quantity;
|
||||||
|
|||||||
@@ -264,7 +264,8 @@ class _ReturnRewardsDialogState extends State<ReturnRewardsDialog>
|
|||||||
return Transform.translate(
|
return Transform.translate(
|
||||||
offset: isOpening
|
offset: isOpening
|
||||||
? Offset(
|
? Offset(
|
||||||
_shakeAnimation.value * 2 *
|
_shakeAnimation.value *
|
||||||
|
2 *
|
||||||
((_animController.value * 10).round() % 2 == 0
|
((_animController.value * 10).round() % 2 == 0
|
||||||
? 1
|
? 1
|
||||||
: -1),
|
: -1),
|
||||||
@@ -314,8 +315,8 @@ class _ReturnRewardsDialogState extends State<ReturnRewardsDialog>
|
|||||||
isGold
|
isGold
|
||||||
? l10n.returnRewardOpenChests
|
? l10n.returnRewardOpenChests
|
||||||
: (isPaidUser
|
: (isPaidUser
|
||||||
? l10n.returnRewardClaimBonusFree
|
? l10n.returnRewardClaimBonusFree
|
||||||
: l10n.returnRewardClaimBonus),
|
: l10n.returnRewardClaimBonus),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'PressStart2P',
|
fontFamily: 'PressStart2P',
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
@@ -365,10 +366,7 @@ class _ReturnRewardsDialogState extends State<ReturnRewardsDialog>
|
|||||||
count,
|
count,
|
||||||
(index) => Text(
|
(index) => Text(
|
||||||
'📦',
|
'📦',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 24, color: enabled ? null : muted),
|
||||||
fontSize: 24,
|
|
||||||
color: enabled ? null : muted,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -387,7 +385,9 @@ class _ReturnRewardsDialogState extends State<ReturnRewardsDialog>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: rewards.map((reward) => _buildRewardItem(context, reward)).toList(),
|
children: rewards
|
||||||
|
.map((reward) => _buildRewardItem(context, reward))
|
||||||
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,10 +65,7 @@ class SpeedBoostButton extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text('⚡', style: TextStyle(fontSize: 18, color: expColor)),
|
||||||
'⚡',
|
|
||||||
style: TextStyle(fontSize: 18, color: expColor),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'${boostMultiplier}x',
|
'${boostMultiplier}x',
|
||||||
@@ -113,10 +110,7 @@ class SpeedBoostButton extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text('⚡', style: TextStyle(fontSize: 18, color: gold)),
|
||||||
'⚡',
|
|
||||||
style: TextStyle(fontSize: 18, color: gold),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'${boostMultiplier}x',
|
'${boostMultiplier}x',
|
||||||
@@ -130,10 +124,7 @@ class SpeedBoostButton extends StatelessWidget {
|
|||||||
if (!isPaidUser) ...[
|
if (!isPaidUser) ...[
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||||
horizontal: 4,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withValues(alpha: 0.2),
|
color: Colors.white.withValues(alpha: 0.2),
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
|||||||
@@ -257,7 +257,11 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (!isPaidUser) ...[
|
if (!isPaidUser) ...[
|
||||||
const Icon(Icons.play_circle, size: 14, color: RetroColors.gold),
|
const Icon(
|
||||||
|
Icons.play_circle,
|
||||||
|
size: 14,
|
||||||
|
color: RetroColors.gold,
|
||||||
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
],
|
],
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -105,8 +105,9 @@ class _ClassInfo extends StatelessWidget {
|
|||||||
final percent = (passive.value * 100).round();
|
final percent = (passive.value * 100).round();
|
||||||
return switch (passive.type) {
|
return switch (passive.type) {
|
||||||
ClassPassiveType.hpBonus => game_l10n.passiveHpBonus(percent),
|
ClassPassiveType.hpBonus => game_l10n.passiveHpBonus(percent),
|
||||||
ClassPassiveType.physicalDamageBonus =>
|
ClassPassiveType.physicalDamageBonus => game_l10n.passivePhysicalBonus(
|
||||||
game_l10n.passivePhysicalBonus(percent),
|
percent,
|
||||||
|
),
|
||||||
ClassPassiveType.defenseBonus => game_l10n.passiveDefenseBonus(percent),
|
ClassPassiveType.defenseBonus => game_l10n.passiveDefenseBonus(percent),
|
||||||
ClassPassiveType.magicDamageBonus => game_l10n.passiveMagicBonus(percent),
|
ClassPassiveType.magicDamageBonus => game_l10n.passiveMagicBonus(percent),
|
||||||
ClassPassiveType.evasionBonus => game_l10n.passiveEvasionBonus(percent),
|
ClassPassiveType.evasionBonus => game_l10n.passiveEvasionBonus(percent),
|
||||||
|
|||||||
@@ -62,9 +62,15 @@ class StatsSection extends StatelessWidget {
|
|||||||
// 스탯 그리드
|
// 스탯 그리드
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _StatTile(label: l10n.statStr, value: str)),
|
Expanded(
|
||||||
Expanded(child: _StatTile(label: l10n.statCon, value: con)),
|
child: _StatTile(label: l10n.statStr, value: str),
|
||||||
Expanded(child: _StatTile(label: l10n.statDex, value: dex)),
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _StatTile(label: l10n.statCon, value: con),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _StatTile(label: l10n.statDex, value: dex),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -73,8 +79,12 @@ class StatsSection extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: _StatTile(label: l10n.statInt, value: intelligence),
|
child: _StatTile(label: l10n.statInt, value: intelligence),
|
||||||
),
|
),
|
||||||
Expanded(child: _StatTile(label: l10n.statWis, value: wis)),
|
Expanded(
|
||||||
Expanded(child: _StatTile(label: l10n.statCha, value: cha)),
|
child: _StatTile(label: l10n.statWis, value: wis),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _StatTile(label: l10n.statCha, value: cha),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -116,10 +126,7 @@ class StatsSection extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
_UndoButton(
|
_UndoButton(canUndo: canUndo, onPressed: onUndo),
|
||||||
canUndo: canUndo,
|
|
||||||
onPressed: onUndo,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
_RollButton(
|
_RollButton(
|
||||||
canRoll: canRoll,
|
canRoll: canRoll,
|
||||||
@@ -222,11 +229,7 @@ class _UndoButton extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
// 무료 유저는 광고 아이콘 표시
|
// 무료 유저는 광고 아이콘 표시
|
||||||
if (!isPaidUser && canUndo) ...[
|
if (!isPaidUser && canUndo) ...[
|
||||||
const Icon(
|
const Icon(Icons.play_circle, size: 14, color: RetroColors.gold),
|
||||||
Icons.play_circle,
|
|
||||||
size: 14,
|
|
||||||
color: RetroColors.gold,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
],
|
],
|
||||||
Icon(
|
Icon(
|
||||||
@@ -240,7 +243,9 @@ class _UndoButton extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'PressStart2P',
|
fontFamily: 'PressStart2P',
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: canUndo ? RetroColors.textLight : RetroColors.textDisabled,
|
color: canUndo
|
||||||
|
? RetroColors.textLight
|
||||||
|
: RetroColors.textDisabled,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ void main() {
|
|||||||
group('playerAttackMonster', () {
|
group('playerAttackMonster', () {
|
||||||
test('데미지 = (ATK * variation) - (DEF * 0.4)', () {
|
test('데미지 = (ATK * variation) - (DEF * 0.4)', () {
|
||||||
// 고정 시드로 예측 가능한 결과
|
// 고정 시드로 예측 가능한 결과
|
||||||
final rng = DeterministicRandom( 42);
|
final rng = DeterministicRandom(42);
|
||||||
final calculator = CombatCalculator(rng: rng);
|
final calculator = CombatCalculator(rng: rng);
|
||||||
|
|
||||||
final player = CombatStats.empty().copyWith(
|
final player = CombatStats.empty().copyWith(
|
||||||
@@ -49,7 +49,7 @@ void main() {
|
|||||||
|
|
||||||
test('크리티컬 발동 시 데미지 배율 적용', () {
|
test('크리티컬 발동 시 데미지 배율 적용', () {
|
||||||
// 크리티컬이 항상 발동하도록 criRate = 1.0
|
// 크리티컬이 항상 발동하도록 criRate = 1.0
|
||||||
final rng = DeterministicRandom( 123);
|
final rng = DeterministicRandom(123);
|
||||||
final calculator = CombatCalculator(rng: rng);
|
final calculator = CombatCalculator(rng: rng);
|
||||||
|
|
||||||
final player = CombatStats.empty().copyWith(
|
final player = CombatStats.empty().copyWith(
|
||||||
@@ -87,7 +87,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('회피 발동 시 0 데미지', () {
|
test('회피 발동 시 0 데미지', () {
|
||||||
final rng = DeterministicRandom( 42);
|
final rng = DeterministicRandom(42);
|
||||||
final calculator = CombatCalculator(rng: rng);
|
final calculator = CombatCalculator(rng: rng);
|
||||||
|
|
||||||
final player = CombatStats.empty().copyWith(
|
final player = CombatStats.empty().copyWith(
|
||||||
@@ -130,7 +130,7 @@ void main() {
|
|||||||
test('블록 발동 시 70% 감소', () {
|
test('블록 발동 시 70% 감소', () {
|
||||||
// 블록이 발동하는 시드 찾기
|
// 블록이 발동하는 시드 찾기
|
||||||
// blockRate = 1.0으로 항상 블록
|
// blockRate = 1.0으로 항상 블록
|
||||||
final rng = DeterministicRandom( 99);
|
final rng = DeterministicRandom(99);
|
||||||
final calculator = CombatCalculator(rng: rng);
|
final calculator = CombatCalculator(rng: rng);
|
||||||
|
|
||||||
final monster = MonsterCombatStats(
|
final monster = MonsterCombatStats(
|
||||||
@@ -170,7 +170,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('패리 발동 시 50% 감소', () {
|
test('패리 발동 시 50% 감소', () {
|
||||||
final rng = DeterministicRandom( 77);
|
final rng = DeterministicRandom(77);
|
||||||
final calculator = CombatCalculator(rng: rng);
|
final calculator = CombatCalculator(rng: rng);
|
||||||
|
|
||||||
final monster = MonsterCombatStats(
|
final monster = MonsterCombatStats(
|
||||||
@@ -212,7 +212,7 @@ void main() {
|
|||||||
|
|
||||||
group('estimateCombatDurationMs', () {
|
group('estimateCombatDurationMs', () {
|
||||||
test('범위 2000~30000ms 내 반환', () {
|
test('범위 2000~30000ms 내 반환', () {
|
||||||
final rng = DeterministicRandom( 42);
|
final rng = DeterministicRandom(42);
|
||||||
final calculator = CombatCalculator(rng: rng);
|
final calculator = CombatCalculator(rng: rng);
|
||||||
|
|
||||||
final player = CombatStats.empty().copyWith(
|
final player = CombatStats.empty().copyWith(
|
||||||
@@ -247,7 +247,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('고레벨 몬스터는 더 긴 전투 시간', () {
|
test('고레벨 몬스터는 더 긴 전투 시간', () {
|
||||||
final rng = DeterministicRandom( 42);
|
final rng = DeterministicRandom(42);
|
||||||
final calculator = CombatCalculator(rng: rng);
|
final calculator = CombatCalculator(rng: rng);
|
||||||
|
|
||||||
final player = CombatStats.empty().copyWith(
|
final player = CombatStats.empty().copyWith(
|
||||||
@@ -305,7 +305,7 @@ void main() {
|
|||||||
|
|
||||||
group('evaluateDifficulty', () {
|
group('evaluateDifficulty', () {
|
||||||
test('범위 0.0~1.0 내 반환', () {
|
test('범위 0.0~1.0 내 반환', () {
|
||||||
final rng = DeterministicRandom( 42);
|
final rng = DeterministicRandom(42);
|
||||||
final calculator = CombatCalculator(rng: rng);
|
final calculator = CombatCalculator(rng: rng);
|
||||||
|
|
||||||
final player = CombatStats.empty().copyWith(
|
final player = CombatStats.empty().copyWith(
|
||||||
|
|||||||
@@ -125,7 +125,10 @@ void main() {
|
|||||||
// ATK 100 * 2.0 - DEF 50 * 0.3 = 200 - 15 = 185
|
// ATK 100 * 2.0 - DEF 50 * 0.3 = 200 - 15 = 185
|
||||||
expect(result.result.success, isTrue);
|
expect(result.result.success, isTrue);
|
||||||
expect(result.result.damage, equals(185));
|
expect(result.result.damage, equals(185));
|
||||||
expect(result.updatedPlayer.mpCurrent, equals(20)); // 50 - 30 (mpCost 30)
|
expect(
|
||||||
|
result.updatedPlayer.mpCurrent,
|
||||||
|
equals(20),
|
||||||
|
); // 50 - 30 (mpCost 30)
|
||||||
expect(result.updatedMonster.hpCurrent, equals(315)); // 500 - 185
|
expect(result.updatedMonster.hpCurrent, equals(315)); // 500 - 185
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -349,10 +352,7 @@ void main() {
|
|||||||
|
|
||||||
const skill = SkillData.memoryDump;
|
const skill = SkillData.memoryDump;
|
||||||
// baseDotDamage: 10, baseDotDurationMs: 6000, baseDotTickMs: 1000
|
// baseDotDamage: 10, baseDotDurationMs: 6000, baseDotTickMs: 1000
|
||||||
final player = CombatStats.empty().copyWith(
|
final player = CombatStats.empty().copyWith(mpMax: 100, mpCurrent: 50);
|
||||||
mpMax: 100,
|
|
||||||
mpCurrent: 50,
|
|
||||||
);
|
|
||||||
final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000);
|
final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000);
|
||||||
|
|
||||||
final result = service.useDotSkill(
|
final result = service.useDotSkill(
|
||||||
@@ -379,10 +379,7 @@ void main() {
|
|||||||
final service = SkillService(rng: rng);
|
final service = SkillService(rng: rng);
|
||||||
|
|
||||||
const skill = SkillData.memoryDump;
|
const skill = SkillData.memoryDump;
|
||||||
final player = CombatStats.empty().copyWith(
|
final player = CombatStats.empty().copyWith(mpMax: 100, mpCurrent: 50);
|
||||||
mpMax: 100,
|
|
||||||
mpCurrent: 50,
|
|
||||||
);
|
|
||||||
final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000);
|
final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000);
|
||||||
|
|
||||||
final result = service.useDotSkill(
|
final result = service.useDotSkill(
|
||||||
@@ -403,10 +400,7 @@ void main() {
|
|||||||
final service = SkillService(rng: rng);
|
final service = SkillService(rng: rng);
|
||||||
|
|
||||||
const skill = SkillData.memoryDump;
|
const skill = SkillData.memoryDump;
|
||||||
final player = CombatStats.empty().copyWith(
|
final player = CombatStats.empty().copyWith(mpMax: 100, mpCurrent: 50);
|
||||||
mpMax: 100,
|
|
||||||
mpCurrent: 50,
|
|
||||||
);
|
|
||||||
final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000);
|
final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000);
|
||||||
|
|
||||||
final result = service.useDotSkill(
|
final result = service.useDotSkill(
|
||||||
@@ -555,10 +549,7 @@ void main() {
|
|||||||
final service = SkillService(rng: rng);
|
final service = SkillService(rng: rng);
|
||||||
|
|
||||||
const skill = SkillData.debugMode; // ATK +25% 버프, mpCost: 100
|
const skill = SkillData.debugMode; // ATK +25% 버프, mpCost: 100
|
||||||
final player = CombatStats.empty().copyWith(
|
final player = CombatStats.empty().copyWith(mpMax: 200, mpCurrent: 150);
|
||||||
mpMax: 200,
|
|
||||||
mpCurrent: 150,
|
|
||||||
);
|
|
||||||
final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000);
|
final skillSystem = SkillSystemState.empty().copyWith(elapsedMs: 5000);
|
||||||
|
|
||||||
final result = service.useBuffSkill(
|
final result = service.useBuffSkill(
|
||||||
@@ -569,10 +560,7 @@ void main() {
|
|||||||
|
|
||||||
expect(result.result.success, isTrue);
|
expect(result.result.success, isTrue);
|
||||||
expect(result.result.appliedBuff, isNotNull);
|
expect(result.result.appliedBuff, isNotNull);
|
||||||
expect(
|
expect(result.result.appliedBuff!.effect.atkModifier, equals(0.25));
|
||||||
result.result.appliedBuff!.effect.atkModifier,
|
|
||||||
equals(0.25),
|
|
||||||
);
|
|
||||||
expect(result.updatedSkillSystem.activeBuffs.length, equals(1));
|
expect(result.updatedSkillSystem.activeBuffs.length, equals(1));
|
||||||
expect(result.updatedPlayer.mpCurrent, equals(50)); // 150 - 100
|
expect(result.updatedPlayer.mpCurrent, equals(50)); // 150 - 100
|
||||||
});
|
});
|
||||||
@@ -582,10 +570,7 @@ void main() {
|
|||||||
final service = SkillService(rng: rng);
|
final service = SkillService(rng: rng);
|
||||||
|
|
||||||
const skill = SkillData.debugMode;
|
const skill = SkillData.debugMode;
|
||||||
final player = CombatStats.empty().copyWith(
|
final player = CombatStats.empty().copyWith(mpMax: 100, mpCurrent: 50);
|
||||||
mpMax: 100,
|
|
||||||
mpCurrent: 50,
|
|
||||||
);
|
|
||||||
final existingBuff = const ActiveBuff(
|
final existingBuff = const ActiveBuff(
|
||||||
effect: BuffEffect(
|
effect: BuffEffect(
|
||||||
id: 'debug_mode_buff',
|
id: 'debug_mode_buff',
|
||||||
|
|||||||
@@ -26,10 +26,7 @@ void main() {
|
|||||||
final race = RaceTraits(
|
final race = RaceTraits(
|
||||||
raceId: 'test_race',
|
raceId: 'test_race',
|
||||||
name: 'Test Race',
|
name: 'Test Race',
|
||||||
statModifiers: const {
|
statModifiers: const {StatType.str: 2, StatType.intelligence: -1},
|
||||||
StatType.str: 2,
|
|
||||||
StatType.intelligence: -1,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 보정 없는 클래스
|
// 보정 없는 클래스
|
||||||
@@ -73,10 +70,7 @@ void main() {
|
|||||||
const klass = ClassTraits(
|
const klass = ClassTraits(
|
||||||
classId: 'test_class',
|
classId: 'test_class',
|
||||||
name: 'Test Class',
|
name: 'Test Class',
|
||||||
statModifiers: {
|
statModifiers: {StatType.con: 3, StatType.dex: 1},
|
||||||
StatType.con: 3,
|
|
||||||
StatType.dex: 1,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final result = calculator.applyModifiers(
|
final result = calculator.applyModifiers(
|
||||||
@@ -248,10 +242,7 @@ void main() {
|
|||||||
name: 'Test Class',
|
name: 'Test Class',
|
||||||
statModifiers: {},
|
statModifiers: {},
|
||||||
passives: [
|
passives: [
|
||||||
ClassPassive(
|
ClassPassive(type: ClassPassiveType.evasionBonus, value: 0.15),
|
||||||
type: ClassPassiveType.evasionBonus,
|
|
||||||
value: 0.15,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -265,10 +256,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('물리 공격력 보너스 적용', () {
|
test('물리 공격력 보너스 적용', () {
|
||||||
final combatStats = CombatStats.empty().copyWith(
|
final combatStats = CombatStats.empty().copyWith(atk: 100, def: 20);
|
||||||
atk: 100,
|
|
||||||
def: 20,
|
|
||||||
);
|
|
||||||
|
|
||||||
const race = RaceTraits(
|
const race = RaceTraits(
|
||||||
raceId: 'test_race',
|
raceId: 'test_race',
|
||||||
@@ -322,10 +310,7 @@ void main() {
|
|||||||
name: 'Test Class',
|
name: 'Test Class',
|
||||||
statModifiers: {},
|
statModifiers: {},
|
||||||
passives: [
|
passives: [
|
||||||
ClassPassive(
|
ClassPassive(type: ClassPassiveType.postCombatHeal, value: 0.05),
|
||||||
type: ClassPassiveType.postCombatHeal,
|
|
||||||
value: 0.05,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ void main() {
|
|||||||
test('loadAndStart surfaces save load errors', () {
|
test('loadAndStart surfaces save load errors', () {
|
||||||
fakeAsync((async) {
|
fakeAsync((async) {
|
||||||
final saveManager = FakeSaveManager()
|
final saveManager = FakeSaveManager()
|
||||||
..onLoad = (_) => (const SaveOutcome.failure('boom'), null, false, null);
|
..onLoad = (_) =>
|
||||||
|
(const SaveOutcome.failure('boom'), null, false, null);
|
||||||
final controller = buildController(async, saveManager);
|
final controller = buildController(async, saveManager);
|
||||||
|
|
||||||
controller.loadAndStart(fileName: 'bad.pqf');
|
controller.loadAndStart(fileName: 'bad.pqf');
|
||||||
|
|||||||
@@ -95,10 +95,7 @@ class MockFactories {
|
|||||||
///
|
///
|
||||||
/// [seed]: 결정론적 랜덤 시드
|
/// [seed]: 결정론적 랜덤 시드
|
||||||
/// [level]: 캐릭터 레벨
|
/// [level]: 캐릭터 레벨
|
||||||
static GameState createGameState({
|
static GameState createGameState({int seed = 42, int level = 1}) {
|
||||||
int seed = 42,
|
|
||||||
int level = 1,
|
|
||||||
}) {
|
|
||||||
return GameState.withSeed(seed: seed);
|
return GameState.withSeed(seed: seed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user