feat(death): 사망/부활 시스템 개선
- DeathInfo에 lostItem 필드 추가 (광고 부활 시 복구용) - 세이브 데이터 v4: MonetizationState 포함 - 사망 오버레이 UI 개선 - 부활 서비스 광고 연동
This commit is contained in:
@@ -240,6 +240,129 @@ class ResurrectionService {
|
|||||||
return state.inventory.gold >= cost;
|
return state.inventory.gold >= cost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 광고 부활 (HP 100% + 아이템 복구)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 광고 부활 처리
|
||||||
|
///
|
||||||
|
/// 1. 상실한 아이템 복구 (있는 경우)
|
||||||
|
/// 2. HP/MP 100% 회복
|
||||||
|
/// 3. 사망 상태 해제
|
||||||
|
/// 4. 안전 지역으로 이동 태스크 설정
|
||||||
|
///
|
||||||
|
/// Note: 10분 자동부활 버프는 GameSessionController에서 처리
|
||||||
|
GameState processAdRevive(GameState state) {
|
||||||
|
if (!state.isDead) return state;
|
||||||
|
|
||||||
|
var nextState = state;
|
||||||
|
|
||||||
|
// 1. 상실한 아이템 복구 (있는 경우)
|
||||||
|
if (canRecoverLostItem(state)) {
|
||||||
|
nextState = processItemRecovery(nextState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 전체 HP/MP 계산 (장비 + 종족 + 클래스 보너스 포함)
|
||||||
|
final totalHpMax = _calculateTotalHpMax(nextState);
|
||||||
|
final totalMpMax = _calculateTotalMpMax(nextState);
|
||||||
|
|
||||||
|
// HP/MP 100% 회복 (장비 구매 없이)
|
||||||
|
nextState = nextState.copyWith(
|
||||||
|
stats: nextState.stats.copyWith(
|
||||||
|
hpCurrent: totalHpMax,
|
||||||
|
mpCurrent: totalMpMax,
|
||||||
|
),
|
||||||
|
clearDeathInfo: true, // 사망 상태 해제
|
||||||
|
);
|
||||||
|
|
||||||
|
// 스킬 쿨타임 초기화
|
||||||
|
nextState = nextState.copyWith(
|
||||||
|
skillSystem: SkillSystemState.empty().copyWith(
|
||||||
|
elapsedMs: nextState.skillSystem.elapsedMs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. 부활 후 태스크 시퀀스 설정
|
||||||
|
final resurrectionQueue = <QueueEntry>[
|
||||||
|
QueueEntry(
|
||||||
|
kind: QueueKind.task,
|
||||||
|
durationMillis: 3000,
|
||||||
|
caption: l10n.taskReturningToTown,
|
||||||
|
taskType: TaskType.neutral,
|
||||||
|
),
|
||||||
|
QueueEntry(
|
||||||
|
kind: QueueKind.task,
|
||||||
|
durationMillis: 3000,
|
||||||
|
caption: l10n.taskRestockingAtShop,
|
||||||
|
taskType: TaskType.market,
|
||||||
|
),
|
||||||
|
QueueEntry(
|
||||||
|
kind: QueueKind.task,
|
||||||
|
durationMillis: 2000,
|
||||||
|
caption: l10n.taskHeadingToHuntingGrounds,
|
||||||
|
taskType: TaskType.neutral,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final firstTask = resurrectionQueue.removeAt(0);
|
||||||
|
nextState = nextState.copyWith(
|
||||||
|
queue: QueueState(entries: resurrectionQueue),
|
||||||
|
progress: nextState.progress.copyWith(
|
||||||
|
currentTask: TaskInfo(
|
||||||
|
caption: firstTask.caption,
|
||||||
|
type: firstTask.taskType,
|
||||||
|
),
|
||||||
|
task: ProgressBarState(position: 0, max: firstTask.durationMillis),
|
||||||
|
currentCombat: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return nextState;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 아이템 복구 (Phase 5)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 상실한 아이템 복구 가능 여부 확인
|
||||||
|
///
|
||||||
|
/// 사망 상태이고 상실한 아이템이 있을 때만 true
|
||||||
|
bool canRecoverLostItem(GameState state) {
|
||||||
|
if (!state.isDead) return false;
|
||||||
|
if (state.deathInfo == null) return false;
|
||||||
|
return state.deathInfo!.lostItem != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 상실한 아이템 복구 처리
|
||||||
|
///
|
||||||
|
/// 광고 시청 후 호출되며 상실한 아이템을 장비에 복원합니다.
|
||||||
|
/// Returns: 복구된 상태 (복구 불가 시 원본 상태 반환)
|
||||||
|
GameState processItemRecovery(GameState state) {
|
||||||
|
if (!canRecoverLostItem(state)) return state;
|
||||||
|
|
||||||
|
final deathInfo = state.deathInfo!;
|
||||||
|
final lostItem = deathInfo.lostItem!;
|
||||||
|
final lostSlot = deathInfo.lostItemSlot!;
|
||||||
|
|
||||||
|
// 해당 슬롯에 아이템 복원
|
||||||
|
final slotIndex = lostSlot.index;
|
||||||
|
final updatedEquipment = state.equipment.setItemByIndex(slotIndex, lostItem);
|
||||||
|
|
||||||
|
// DeathInfo에서 상실 아이템 정보 제거 (복구 완료)
|
||||||
|
final updatedDeathInfo = deathInfo.copyWith(
|
||||||
|
lostEquipmentCount: 0,
|
||||||
|
lostItemName: null,
|
||||||
|
lostItemSlot: null,
|
||||||
|
lostItemRarity: null,
|
||||||
|
lostItem: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return state.copyWith(
|
||||||
|
equipment: updatedEquipment,
|
||||||
|
deathInfo: updatedDeathInfo,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 장비 보존 아이템 적용 (향후 확장용)
|
/// 장비 보존 아이템 적용 (향후 확장용)
|
||||||
///
|
///
|
||||||
/// [protectedSlots] 보존할 슬롯 인덱스 목록
|
/// [protectedSlots] 보존할 슬롯 인덱스 목록
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ class DeathInfo {
|
|||||||
this.lostItemName,
|
this.lostItemName,
|
||||||
this.lostItemSlot,
|
this.lostItemSlot,
|
||||||
this.lostItemRarity,
|
this.lostItemRarity,
|
||||||
|
this.lostItem,
|
||||||
this.lastCombatEvents = const [],
|
this.lastCombatEvents = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,6 +155,9 @@ class DeathInfo {
|
|||||||
/// 제물로 바친 아이템 희귀도 (null이면 없음)
|
/// 제물로 바친 아이템 희귀도 (null이면 없음)
|
||||||
final ItemRarity? lostItemRarity;
|
final ItemRarity? lostItemRarity;
|
||||||
|
|
||||||
|
/// 상실한 장비 전체 정보 (광고 부활 시 복구용)
|
||||||
|
final EquipmentItem? lostItem;
|
||||||
|
|
||||||
/// 사망 시점 골드
|
/// 사망 시점 골드
|
||||||
final int goldAtDeath;
|
final int goldAtDeath;
|
||||||
|
|
||||||
@@ -173,6 +177,7 @@ class DeathInfo {
|
|||||||
String? lostItemName,
|
String? lostItemName,
|
||||||
EquipmentSlot? lostItemSlot,
|
EquipmentSlot? lostItemSlot,
|
||||||
ItemRarity? lostItemRarity,
|
ItemRarity? lostItemRarity,
|
||||||
|
EquipmentItem? lostItem,
|
||||||
int? goldAtDeath,
|
int? goldAtDeath,
|
||||||
int? levelAtDeath,
|
int? levelAtDeath,
|
||||||
int? timestamp,
|
int? timestamp,
|
||||||
@@ -185,6 +190,7 @@ class DeathInfo {
|
|||||||
lostItemName: lostItemName ?? this.lostItemName,
|
lostItemName: lostItemName ?? this.lostItemName,
|
||||||
lostItemSlot: lostItemSlot ?? this.lostItemSlot,
|
lostItemSlot: lostItemSlot ?? this.lostItemSlot,
|
||||||
lostItemRarity: lostItemRarity ?? this.lostItemRarity,
|
lostItemRarity: lostItemRarity ?? this.lostItemRarity,
|
||||||
|
lostItem: lostItem ?? this.lostItem,
|
||||||
goldAtDeath: goldAtDeath ?? this.goldAtDeath,
|
goldAtDeath: goldAtDeath ?? this.goldAtDeath,
|
||||||
levelAtDeath: levelAtDeath ?? this.levelAtDeath,
|
levelAtDeath: levelAtDeath ?? this.levelAtDeath,
|
||||||
timestamp: timestamp ?? this.timestamp,
|
timestamp: timestamp ?? this.timestamp,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:collection';
|
|||||||
|
|
||||||
import 'package:asciineverdie/src/core/model/equipment_item.dart';
|
import 'package:asciineverdie/src/core/model/equipment_item.dart';
|
||||||
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/monetization_state.dart';
|
||||||
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||||
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
@@ -9,7 +10,8 @@ import 'package:asciineverdie/src/core/model/game_state.dart';
|
|||||||
/// 세이브 파일 버전
|
/// 세이브 파일 버전
|
||||||
/// - v2: 장비 이름만 저장 (레거시)
|
/// - v2: 장비 이름만 저장 (레거시)
|
||||||
/// - v3: 장비 전체 정보 저장 (level, rarity, stats 포함)
|
/// - v3: 장비 전체 정보 저장 (level, rarity, stats 포함)
|
||||||
const int kSaveVersion = 3;
|
/// - v4: MonetizationState 추가, DeathInfo.lostItem 추가
|
||||||
|
const int kSaveVersion = 4;
|
||||||
|
|
||||||
class GameSave {
|
class GameSave {
|
||||||
GameSave({
|
GameSave({
|
||||||
@@ -23,9 +25,14 @@ class GameSave {
|
|||||||
required this.progress,
|
required this.progress,
|
||||||
required this.queue,
|
required this.queue,
|
||||||
this.cheatsEnabled = false,
|
this.cheatsEnabled = false,
|
||||||
|
this.monetization,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory GameSave.fromState(GameState state, {bool cheatsEnabled = false}) {
|
factory GameSave.fromState(
|
||||||
|
GameState state, {
|
||||||
|
bool cheatsEnabled = false,
|
||||||
|
MonetizationState? monetization,
|
||||||
|
}) {
|
||||||
return GameSave(
|
return GameSave(
|
||||||
version: kSaveVersion,
|
version: kSaveVersion,
|
||||||
rngState: state.rng.state,
|
rngState: state.rng.state,
|
||||||
@@ -37,6 +44,7 @@ class GameSave {
|
|||||||
progress: state.progress,
|
progress: state.progress,
|
||||||
queue: state.queue,
|
queue: state.queue,
|
||||||
cheatsEnabled: cheatsEnabled,
|
cheatsEnabled: cheatsEnabled,
|
||||||
|
monetization: monetization,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +59,9 @@ class GameSave {
|
|||||||
final QueueState queue;
|
final QueueState queue;
|
||||||
final bool cheatsEnabled;
|
final bool cheatsEnabled;
|
||||||
|
|
||||||
|
/// 수익화 시스템 상태 (v4+)
|
||||||
|
final MonetizationState? monetization;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
'version': version,
|
'version': version,
|
||||||
@@ -132,6 +143,7 @@ class GameSave {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
if (monetization != null) 'monetization': monetization!.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +256,9 @@ class GameSave {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
monetization: _monetizationFromJson(
|
||||||
|
json['monetization'] as Map<String, dynamic>?,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,3 +370,12 @@ Equipment _equipmentFromJson(Map<String, dynamic> json, int version) {
|
|||||||
bestIndex: json['bestIndex'] as int? ?? 0,
|
bestIndex: json['bestIndex'] as int? ?? 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// MonetizationState 역직렬화 (v4+ 마이그레이션)
|
||||||
|
///
|
||||||
|
/// - v3 이하: null (기본값 사용)
|
||||||
|
/// - v4 이상: 저장된 상태 로드
|
||||||
|
MonetizationState? _monetizationFromJson(Map<String, dynamic>? json) {
|
||||||
|
if (json == null) return null;
|
||||||
|
return MonetizationState.fromJson(json);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/monetization_state.dart';
|
||||||
import 'package:asciineverdie/src/core/model/save_data.dart';
|
import 'package:asciineverdie/src/core/model/save_data.dart';
|
||||||
import 'package:asciineverdie/src/core/storage/save_repository.dart';
|
import 'package:asciineverdie/src/core/storage/save_repository.dart';
|
||||||
import 'package:asciineverdie/src/core/storage/save_service.dart'
|
import 'package:asciineverdie/src/core/storage/save_service.dart'
|
||||||
@@ -13,23 +14,36 @@ class SaveManager {
|
|||||||
|
|
||||||
/// Save current game state to disk. [fileName] may be absolute or relative.
|
/// Save current game state to disk. [fileName] may be absolute or relative.
|
||||||
/// Returns outcome with error on failure.
|
/// Returns outcome with error on failure.
|
||||||
|
///
|
||||||
|
/// [monetization] 저장 시 lastPlayTime을 현재 시간으로 자동 업데이트
|
||||||
Future<SaveOutcome> saveState(
|
Future<SaveOutcome> saveState(
|
||||||
GameState state, {
|
GameState state, {
|
||||||
String? fileName,
|
String? fileName,
|
||||||
bool cheatsEnabled = false,
|
bool cheatsEnabled = false,
|
||||||
|
MonetizationState? monetization,
|
||||||
}) {
|
}) {
|
||||||
final save = GameSave.fromState(state, cheatsEnabled: cheatsEnabled);
|
// lastPlayTime을 현재 시간으로 업데이트
|
||||||
|
final updatedMonetization = (monetization ?? MonetizationState.initial())
|
||||||
|
.copyWith(lastPlayTime: DateTime.now());
|
||||||
|
|
||||||
|
final save = GameSave.fromState(
|
||||||
|
state,
|
||||||
|
cheatsEnabled: cheatsEnabled,
|
||||||
|
monetization: updatedMonetization,
|
||||||
|
);
|
||||||
return _repo.save(save, fileName ?? defaultFileName);
|
return _repo.save(save, fileName ?? defaultFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load game state from disk. [fileName] may be absolute (e.g., file picker).
|
/// Load game state from disk. [fileName] may be absolute (e.g., file picker).
|
||||||
/// Returns outcome + optional state + cheatsEnabled flag.
|
/// Returns outcome + optional state + cheatsEnabled flag + monetization state.
|
||||||
Future<(SaveOutcome, GameState?, bool)> loadState({String? fileName}) async {
|
Future<(SaveOutcome, GameState?, bool, MonetizationState?)> loadState({
|
||||||
|
String? fileName,
|
||||||
|
}) async {
|
||||||
final (outcome, save) = await _repo.load(fileName ?? defaultFileName);
|
final (outcome, save) = await _repo.load(fileName ?? defaultFileName);
|
||||||
if (!outcome.success || save == null) {
|
if (!outcome.success || save == null) {
|
||||||
return (outcome, null, false);
|
return (outcome, null, false, null);
|
||||||
}
|
}
|
||||||
return (outcome, save.toState(), save.cheatsEnabled);
|
return (outcome, save.toState(), save.cheatsEnabled, save.monetization);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 저장 파일 목록 조회
|
/// 저장 파일 목록 조회
|
||||||
|
|||||||
@@ -8,17 +8,19 @@ import 'package:asciineverdie/src/core/model/game_state.dart';
|
|||||||
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
||||||
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
|
||||||
/// 사망 오버레이 위젯 (Phase 4)
|
/// 사망 오버레이 위젯
|
||||||
///
|
///
|
||||||
/// 플레이어 사망 시 표시되는 전체 화면 오버레이
|
/// 플레이어 사망 시 표시되는 전체 화면 오버레이
|
||||||
|
/// - 무료 부활: HP 50%, 아이템 희생
|
||||||
|
/// - 광고 부활: HP 100%, 아이템 복구, 10분 자동부활 버프
|
||||||
class DeathOverlay extends StatelessWidget {
|
class DeathOverlay extends StatelessWidget {
|
||||||
const DeathOverlay({
|
const DeathOverlay({
|
||||||
super.key,
|
super.key,
|
||||||
required this.deathInfo,
|
required this.deathInfo,
|
||||||
required this.traits,
|
required this.traits,
|
||||||
required this.onResurrect,
|
required this.onResurrect,
|
||||||
this.isAutoResurrectEnabled = false,
|
this.onAdRevive,
|
||||||
this.onToggleAutoResurrect,
|
this.isPaidUser = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 사망 정보
|
/// 사망 정보
|
||||||
@@ -27,14 +29,15 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
/// 캐릭터 특성 (이름, 직업 등)
|
/// 캐릭터 특성 (이름, 직업 등)
|
||||||
final Traits traits;
|
final Traits traits;
|
||||||
|
|
||||||
/// 부활 버튼 콜백
|
/// 무료 부활 버튼 콜백 (HP 50%, 아이템 희생)
|
||||||
final VoidCallback onResurrect;
|
final VoidCallback onResurrect;
|
||||||
|
|
||||||
/// 자동 부활 활성화 여부
|
/// 광고 부활 버튼 콜백 (HP 100% + 아이템 복구 + 10분 자동부활)
|
||||||
final bool isAutoResurrectEnabled;
|
/// null이면 광고 부활 버튼 숨김
|
||||||
|
final VoidCallback? onAdRevive;
|
||||||
|
|
||||||
/// 자동 부활 토글 콜백
|
/// 유료 유저 여부 (광고 아이콘 표시용)
|
||||||
final VoidCallback? onToggleAutoResurrect;
|
final bool isPaidUser;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -135,13 +138,13 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// 부활 버튼
|
// 일반 부활 버튼 (HP 50%, 아이템 희생)
|
||||||
_buildResurrectButton(context),
|
_buildResurrectButton(context),
|
||||||
|
|
||||||
// 자동 부활 버튼
|
// 광고 부활 버튼 (HP 100% + 아이템 복구 + 10분 자동부활)
|
||||||
if (onToggleAutoResurrect != null) ...[
|
if (onAdRevive != null) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildAutoResurrectButton(context),
|
_buildAdReviveButton(context),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -464,77 +467,149 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 자동 부활 토글 버튼
|
/// 광고 부활 버튼 (HP 100% + 아이템 복구 + 10분 자동부활)
|
||||||
Widget _buildAutoResurrectButton(BuildContext context) {
|
Widget _buildAdReviveButton(BuildContext context) {
|
||||||
final mpColor = RetroColors.mpOf(context);
|
final gold = RetroColors.goldOf(context);
|
||||||
final mpDark = RetroColors.mpDarkOf(context);
|
final goldDark = RetroColors.goldDarkOf(context);
|
||||||
final muted = RetroColors.textMutedOf(context);
|
final muted = RetroColors.textMutedOf(context);
|
||||||
|
final hasLostItem = deathInfo.lostItemName != null;
|
||||||
// 활성화 상태에 따른 색상
|
final itemRarityColor = _getRarityColor(deathInfo.lostItemRarity);
|
||||||
final buttonColor = isAutoResurrectEnabled ? mpColor : muted;
|
|
||||||
final buttonDark = isAutoResurrectEnabled
|
|
||||||
? mpDark
|
|
||||||
: muted.withValues(alpha: 0.5);
|
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onToggleAutoResurrect,
|
onTap: onAdRevive,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: buttonColor.withValues(alpha: 0.15),
|
color: gold.withValues(alpha: 0.2),
|
||||||
border: Border(
|
border: Border(
|
||||||
top: BorderSide(color: buttonColor, width: 2),
|
top: BorderSide(color: gold, width: 3),
|
||||||
left: BorderSide(color: buttonColor, width: 2),
|
left: BorderSide(color: gold, width: 3),
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(color: goldDark.withValues(alpha: 0.8), width: 3),
|
||||||
color: buttonDark.withValues(alpha: 0.8),
|
right: BorderSide(color: goldDark.withValues(alpha: 0.8), width: 3),
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
right: BorderSide(
|
|
||||||
color: buttonDark.withValues(alpha: 0.8),
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
// 메인 버튼 텍스트
|
||||||
isAutoResurrectEnabled ? '◉' : '○',
|
Row(
|
||||||
style: TextStyle(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
fontSize: 18,
|
children: [
|
||||||
color: buttonColor,
|
Text(
|
||||||
fontWeight: FontWeight.bold,
|
'✨',
|
||||||
),
|
style: TextStyle(fontSize: 20, color: gold),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
l10n.deathAdRevive.toUpperCase(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 14,
|
||||||
|
color: gold,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 광고 뱃지 (무료 유저만)
|
||||||
|
if (!isPaidUser) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withValues(alpha: 0.2),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'▶ AD',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
// 혜택 목록
|
||||||
l10n.deathAutoResurrect.toUpperCase(),
|
Column(
|
||||||
style: TextStyle(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
fontFamily: 'PressStart2P',
|
children: [
|
||||||
fontSize: 13,
|
// HP 100% 회복
|
||||||
color: buttonColor,
|
_buildBenefitRow(
|
||||||
letterSpacing: 1,
|
context,
|
||||||
),
|
icon: '♥',
|
||||||
|
text: l10n.deathAdReviveHp,
|
||||||
|
color: RetroColors.hpOf(context),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
// 아이템 복구 (잃은 아이템이 있을 때만)
|
||||||
|
if (hasLostItem) ...[
|
||||||
|
_buildBenefitRow(
|
||||||
|
context,
|
||||||
|
icon: '🔄',
|
||||||
|
text: '${l10n.deathAdReviveItem}: ${deathInfo.lostItemName}',
|
||||||
|
color: itemRarityColor,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
],
|
||||||
|
// 10분 자동부활
|
||||||
|
_buildBenefitRow(
|
||||||
|
context,
|
||||||
|
icon: '⏱',
|
||||||
|
text: l10n.deathAdReviveAuto,
|
||||||
|
color: RetroColors.mpOf(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
if (isAutoResurrectEnabled) ...[
|
const SizedBox(height: 6),
|
||||||
const SizedBox(width: 6),
|
// 유료 유저 설명
|
||||||
|
if (isPaidUser)
|
||||||
Text(
|
Text(
|
||||||
'ON',
|
l10n.deathAdRevivePaidDesc,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'PressStart2P',
|
fontFamily: 'PressStart2P',
|
||||||
fontSize: 13,
|
fontSize: 9,
|
||||||
color: mpColor,
|
color: muted,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 혜택 항목 행
|
||||||
|
Widget _buildBenefitRow(
|
||||||
|
BuildContext context, {
|
||||||
|
required String icon,
|
||||||
|
required String text,
|
||||||
|
required Color color,
|
||||||
|
}) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(icon, style: TextStyle(fontSize: 14, color: color)),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 10,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 사망 직전 전투 로그 표시
|
/// 사망 직전 전투 로그 표시
|
||||||
Widget _buildCombatLog(BuildContext context) {
|
Widget _buildCombatLog(BuildContext context) {
|
||||||
final events = deathInfo.lastCombatEvents;
|
final events = deathInfo.lastCombatEvents;
|
||||||
|
|||||||
Reference in New Issue
Block a user