feat(hall-of-fame): 명예의 전당 상세 보기 및 스펠북 기록 추가
- HallOfFameEntry에 finalSpells 필드 추가 (스펠 이름 + 랭크) - 명예의 전당 카드 클릭 시 상세 정보 다이얼로그 표시 - 디버그 모드에서 샘플 엔트리 자동 생성 (테스트용) - pq_logic 및 progress 관련 minor 수정
This commit is contained in:
@@ -65,14 +65,14 @@ class ProgressLoop {
|
|||||||
Stream<GameState> get stream => _stateController.stream;
|
Stream<GameState> get stream => _stateController.stream;
|
||||||
GameState _state;
|
GameState _state;
|
||||||
|
|
||||||
/// 현재 배속 (1x, 2x, 5x)
|
/// 현재 배속 (1x, 3x, 10x)
|
||||||
int get speedMultiplier => _speedMultiplier;
|
int get speedMultiplier => _speedMultiplier;
|
||||||
|
|
||||||
/// 배속 순환: 1 -> 2 -> 5 -> 1
|
/// 배속 순환: 1 -> 3 -> 10 -> 1
|
||||||
void cycleSpeed() {
|
void cycleSpeed() {
|
||||||
_speedMultiplier = switch (_speedMultiplier) {
|
_speedMultiplier = switch (_speedMultiplier) {
|
||||||
1 => 2,
|
1 => 3,
|
||||||
2 => 5,
|
3 => 10,
|
||||||
_ => 1,
|
_ => 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ class ProgressService {
|
|||||||
// ExpBar 초기화 (원본 743-746줄)
|
// ExpBar 초기화 (원본 743-746줄)
|
||||||
final expBar = ProgressBarState(position: 0, max: pq_logic.levelUpTime(1));
|
final expBar = ProgressBarState(position: 0, max: pq_logic.levelUpTime(1));
|
||||||
|
|
||||||
// PlotBar 초기화 (원본 759줄)
|
// PlotBar 초기화 - Prologue 5분 (300초)
|
||||||
final plotBar = const ProgressBarState(position: 0, max: 26 * 1000);
|
final plotBar = const ProgressBarState(position: 0, max: 300);
|
||||||
|
|
||||||
final progress = taskResult.progress.copyWith(
|
final progress = taskResult.progress.copyWith(
|
||||||
exp: expBar,
|
exp: expBar,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class HallOfFameEntry {
|
|||||||
required this.clearedAt,
|
required this.clearedAt,
|
||||||
this.finalStats,
|
this.finalStats,
|
||||||
this.finalEquipment,
|
this.finalEquipment,
|
||||||
|
this.finalSpells,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 고유 ID (UUID)
|
/// 고유 ID (UUID)
|
||||||
@@ -56,6 +57,9 @@ class HallOfFameEntry {
|
|||||||
/// 최종 장비 목록 (향후 아스키 아레나용)
|
/// 최종 장비 목록 (향후 아스키 아레나용)
|
||||||
final Map<String, String>? finalEquipment;
|
final Map<String, String>? finalEquipment;
|
||||||
|
|
||||||
|
/// 최종 스펠북 (스펠 이름 + 랭크)
|
||||||
|
final List<Map<String, String>>? finalSpells;
|
||||||
|
|
||||||
/// 플레이 시간을 Duration으로 변환
|
/// 플레이 시간을 Duration으로 변환
|
||||||
Duration get totalPlayTime => Duration(milliseconds: totalPlayTimeMs);
|
Duration get totalPlayTime => Duration(milliseconds: totalPlayTimeMs);
|
||||||
|
|
||||||
@@ -107,6 +111,9 @@ class HallOfFameEntry {
|
|||||||
'greaves': state.equipment.greaves,
|
'greaves': state.equipment.greaves,
|
||||||
'sollerets': state.equipment.sollerets,
|
'sollerets': state.equipment.sollerets,
|
||||||
},
|
},
|
||||||
|
finalSpells: state.spellBook.spells
|
||||||
|
.map((s) => {'name': s.name, 'rank': s.rank})
|
||||||
|
.toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +131,7 @@ class HallOfFameEntry {
|
|||||||
'questsCompleted': questsCompleted,
|
'questsCompleted': questsCompleted,
|
||||||
'clearedAt': clearedAt.toIso8601String(),
|
'clearedAt': clearedAt.toIso8601String(),
|
||||||
'finalEquipment': finalEquipment,
|
'finalEquipment': finalEquipment,
|
||||||
|
'finalSpells': finalSpells,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +151,11 @@ class HallOfFameEntry {
|
|||||||
finalEquipment: json['finalEquipment'] != null
|
finalEquipment: json['finalEquipment'] != null
|
||||||
? Map<String, String>.from(json['finalEquipment'] as Map)
|
? Map<String, String>.from(json['finalEquipment'] as Map)
|
||||||
: null,
|
: null,
|
||||||
|
finalSpells: json['finalSpells'] != null
|
||||||
|
? (json['finalSpells'] as List<dynamic>)
|
||||||
|
.map((s) => Map<String, String>.from(s as Map))
|
||||||
|
.toList()
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,9 +70,10 @@ class ItemResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int levelUpTimeSeconds(int level) {
|
int levelUpTimeSeconds(int level) {
|
||||||
// ~20 minutes for level 1, then exponential growth (same as LevelUpTime in Main.pas).
|
// 10시간 내 레벨 100 도달 목표 (선형 성장)
|
||||||
final seconds = (20.0 + math.pow(1.15, level)) * 60.0;
|
// 레벨 1: ~2분, 레벨 100: ~7분
|
||||||
return seconds.round();
|
final seconds = 120 + (level * 3);
|
||||||
|
return seconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 초 단위 시간을 사람이 읽기 쉬운 형태로 변환 (원본 Main.pas:1265-1271)
|
/// 초 단위 시간을 사람이 읽기 쉬운 형태로 변환 (원본 Main.pas:1265-1271)
|
||||||
@@ -736,10 +737,22 @@ class ActResult {
|
|||||||
final List<RewardKind> rewards;
|
final List<RewardKind> rewards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Act별 Plot Bar 최대값 (초) - 10시간 완주 목표
|
||||||
|
const _actPlotBarSeconds = [
|
||||||
|
300, // Prologue: 5분
|
||||||
|
7200, // Act I: 2시간
|
||||||
|
10800, // Act II: 3시간
|
||||||
|
10800, // Act III: 3시간
|
||||||
|
5400, // Act IV: 1.5시간
|
||||||
|
1800, // Act V: 30분
|
||||||
|
];
|
||||||
|
|
||||||
ActResult completeAct(int existingActCount) {
|
ActResult completeAct(int existingActCount) {
|
||||||
final nextActIndex = existingActCount;
|
final nextActIndex = existingActCount;
|
||||||
final title = l10n.actTitle(intToRoman(nextActIndex));
|
final title = l10n.actTitle(intToRoman(nextActIndex));
|
||||||
final plotBarMax = 60 * 60 * (1 + 5 * existingActCount);
|
final plotBarMax = existingActCount < _actPlotBarSeconds.length
|
||||||
|
? _actPlotBarSeconds[existingActCount]
|
||||||
|
: 3600;
|
||||||
|
|
||||||
final rewards = <RewardKind>[];
|
final rewards = <RewardKind>[];
|
||||||
if (existingActCount > 1) {
|
if (existingActCount > 1) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
@@ -25,7 +26,13 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadHallOfFame() async {
|
Future<void> _loadHallOfFame() async {
|
||||||
final hallOfFame = await _storage.load();
|
var hallOfFame = await _storage.load();
|
||||||
|
|
||||||
|
// 디버그 모드일 때 샘플 엔트리 추가 (빈 경우에만)
|
||||||
|
if (kDebugMode && hallOfFame.isEmpty) {
|
||||||
|
hallOfFame = hallOfFame.addEntry(_createDebugSampleEntry());
|
||||||
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_hallOfFame = hallOfFame;
|
_hallOfFame = hallOfFame;
|
||||||
@@ -134,6 +141,43 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 디버그 모드 샘플 엔트리 생성 (kDebugMode에서만 사용)
|
||||||
|
HallOfFameEntry _createDebugSampleEntry() {
|
||||||
|
return HallOfFameEntry(
|
||||||
|
id: 'debug_sample_001',
|
||||||
|
characterName: 'Debug Hero',
|
||||||
|
race: 'byte_human',
|
||||||
|
klass: 'loop_wizard',
|
||||||
|
level: 100,
|
||||||
|
totalPlayTimeMs: 10 * 60 * 60 * 1000, // 10시간
|
||||||
|
totalDeaths: 3,
|
||||||
|
monstersKilled: 1234,
|
||||||
|
questsCompleted: 42,
|
||||||
|
clearedAt: DateTime.now(),
|
||||||
|
finalEquipment: {
|
||||||
|
'weapon': '+15 Legendary Debugger',
|
||||||
|
'shield': '+10 Exception Shield',
|
||||||
|
'helm': '+8 Null Pointer Helm',
|
||||||
|
'hauberk': '+12 Thread-Safe Armor',
|
||||||
|
'brassairts': '+6 Memory Guard',
|
||||||
|
'vambraces': '+5 Stack Overflow Band',
|
||||||
|
'gauntlets': '+7 Syntax Checker Gloves',
|
||||||
|
'gambeson': '+9 Buffer Padding',
|
||||||
|
'cuisses': '+4 Runtime Protector',
|
||||||
|
'greaves': '+6 Compile Time Shin',
|
||||||
|
'sollerets': '+5 Binary Boots',
|
||||||
|
},
|
||||||
|
finalSpells: [
|
||||||
|
{'name': 'Recursive Thunder', 'rank': 'XII'},
|
||||||
|
{'name': 'Async Heal', 'rank': 'VIII'},
|
||||||
|
{'name': 'Memory Leak Curse', 'rank': 'X'},
|
||||||
|
{'name': 'Stack Overflow', 'rank': 'VI'},
|
||||||
|
{'name': 'Null Pointer Strike', 'rank': 'IX'},
|
||||||
|
{'name': 'Thread Lock', 'rank': 'VII'},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 명예의 전당 엔트리 카드
|
/// 명예의 전당 엔트리 카드
|
||||||
class _HallOfFameEntryCard extends StatelessWidget {
|
class _HallOfFameEntryCard extends StatelessWidget {
|
||||||
const _HallOfFameEntryCard({required this.entry, required this.rank});
|
const _HallOfFameEntryCard({required this.entry, required this.rank});
|
||||||
@@ -141,6 +185,13 @@ class _HallOfFameEntryCard extends StatelessWidget {
|
|||||||
final HallOfFameEntry entry;
|
final HallOfFameEntry entry;
|
||||||
final int rank;
|
final int rank;
|
||||||
|
|
||||||
|
void _showDetailDialog(BuildContext context) {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _HallOfFameDetailDialog(entry: entry),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final rankColor = _getRankColor(rank);
|
final rankColor = _getRankColor(rank);
|
||||||
@@ -148,10 +199,13 @@ class _HallOfFameEntryCard extends StatelessWidget {
|
|||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child: Padding(
|
child: InkWell(
|
||||||
padding: const EdgeInsets.all(12),
|
onTap: () => _showDetailDialog(context),
|
||||||
child: Row(
|
borderRadius: BorderRadius.circular(12),
|
||||||
children: [
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
// 순위 표시
|
// 순위 표시
|
||||||
Container(
|
Container(
|
||||||
width: 40,
|
width: 40,
|
||||||
@@ -265,7 +319,8 @@ class _HallOfFameEntryCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatChip(IconData icon, String value, Color color) {
|
Widget _buildStatChip(IconData icon, String value, Color color) {
|
||||||
@@ -433,3 +488,233 @@ class _GameClearDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 명예의 전당 상세 정보 다이얼로그
|
||||||
|
class _HallOfFameDetailDialog extends StatelessWidget {
|
||||||
|
const _HallOfFameDetailDialog({required this.entry});
|
||||||
|
|
||||||
|
final HallOfFameEntry entry;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'"${entry.characterName}"',
|
||||||
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${GameDataL10n.getRaceName(context, entry.race)} '
|
||||||
|
'${GameDataL10n.getKlassName(context, entry.klass)} - '
|
||||||
|
'${l10n.uiLevel(entry.level)}',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// 통계 섹션
|
||||||
|
_buildSection(
|
||||||
|
icon: Icons.analytics,
|
||||||
|
title: 'Statistics',
|
||||||
|
child: _buildStatsGrid(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// 장비 섹션
|
||||||
|
if (entry.finalEquipment != null) ...[
|
||||||
|
_buildSection(
|
||||||
|
icon: Icons.shield,
|
||||||
|
title: 'Equipment',
|
||||||
|
child: _buildEquipmentList(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
// 스펠 섹션
|
||||||
|
if (entry.finalSpells != null && entry.finalSpells!.isNotEmpty)
|
||||||
|
_buildSection(
|
||||||
|
icon: Icons.auto_fix_high,
|
||||||
|
title: 'Spells',
|
||||||
|
child: _buildSpellList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Close'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSection({
|
||||||
|
required IconData icon,
|
||||||
|
required String title,
|
||||||
|
required Widget child,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 18, color: Colors.amber.shade700),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.amber.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatsGrid() {
|
||||||
|
return Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
_buildStatItem(Icons.timer, l10n.hofTime, entry.formattedPlayTime),
|
||||||
|
_buildStatItem(
|
||||||
|
Icons.pest_control,
|
||||||
|
'Monsters',
|
||||||
|
'${entry.monstersKilled}',
|
||||||
|
),
|
||||||
|
_buildStatItem(
|
||||||
|
Icons.heart_broken,
|
||||||
|
l10n.hofDeaths,
|
||||||
|
'${entry.totalDeaths}',
|
||||||
|
),
|
||||||
|
_buildStatItem(
|
||||||
|
Icons.check_circle,
|
||||||
|
l10n.hofQuests,
|
||||||
|
'${entry.questsCompleted}',
|
||||||
|
),
|
||||||
|
_buildStatItem(
|
||||||
|
Icons.calendar_today,
|
||||||
|
'Cleared',
|
||||||
|
entry.formattedClearedDate,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatItem(IconData icon, String label, String value) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 14, color: Colors.grey.shade600),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(fontSize: 10, color: Colors.grey.shade600),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEquipmentList() {
|
||||||
|
final equipment = entry.finalEquipment!;
|
||||||
|
final slots = [
|
||||||
|
('weapon', Icons.gavel, 'Weapon'),
|
||||||
|
('shield', Icons.shield, 'Shield'),
|
||||||
|
('helm', Icons.sports_mma, 'Helm'),
|
||||||
|
('hauberk', Icons.checkroom, 'Hauberk'),
|
||||||
|
('brassairts', Icons.front_hand, 'Brassairts'),
|
||||||
|
('vambraces', Icons.back_hand, 'Vambraces'),
|
||||||
|
('gauntlets', Icons.sports_handball, 'Gauntlets'),
|
||||||
|
('gambeson', Icons.dry_cleaning, 'Gambeson'),
|
||||||
|
('cuisses', Icons.airline_seat_legroom_normal, 'Cuisses'),
|
||||||
|
('greaves', Icons.snowshoeing, 'Greaves'),
|
||||||
|
('sollerets', Icons.do_not_step, 'Sollerets'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: slots.map((slot) {
|
||||||
|
final (key, icon, label) = slot;
|
||||||
|
final value = equipment[key] ?? '-';
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 16, color: Colors.grey.shade500),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
SizedBox(
|
||||||
|
width: 80,
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSpellList() {
|
||||||
|
final spells = entry.finalSpells!;
|
||||||
|
return Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: spells.map((spell) {
|
||||||
|
final name = spell['name'] ?? '';
|
||||||
|
final rank = spell['rank'] ?? '';
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.purple.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.purple.shade200),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$name $rank',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.purple.shade700),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ void main() {
|
|||||||
const config = PqConfig();
|
const config = PqConfig();
|
||||||
|
|
||||||
test('levelUpTime grows with level and matches expected seconds', () {
|
test('levelUpTime grows with level and matches expected seconds', () {
|
||||||
expect(pq_logic.levelUpTime(1), 1269);
|
// 새 공식: 120 + (level * 3) - 10시간 내 레벨 100 도달 목표
|
||||||
expect(pq_logic.levelUpTime(10), 1443);
|
expect(pq_logic.levelUpTime(1), 123); // 120 + 3 = 123초 (~2분)
|
||||||
|
expect(pq_logic.levelUpTime(10), 150); // 120 + 30 = 150초 (~2.5분)
|
||||||
|
expect(pq_logic.levelUpTime(100), 420); // 120 + 300 = 420초 (~7분)
|
||||||
});
|
});
|
||||||
|
|
||||||
test('roughTime formats seconds into human-readable strings', () {
|
test('roughTime formats seconds into human-readable strings', () {
|
||||||
@@ -119,7 +121,8 @@ void main() {
|
|||||||
|
|
||||||
final act2 = pq_logic.completeAct(2);
|
final act2 = pq_logic.completeAct(2);
|
||||||
expect(act2.actTitle, 'Act II');
|
expect(act2.actTitle, 'Act II');
|
||||||
expect(act2.plotBarMaxSeconds, 39600);
|
// 새 배열 기반: Act II = 10800초 (3시간) - 10시간 완주 목표
|
||||||
|
expect(act2.plotBarMaxSeconds, 10800);
|
||||||
expect(act2.rewards, contains(pq_logic.RewardKind.item));
|
expect(act2.rewards, contains(pq_logic.RewardKind.item));
|
||||||
expect(act2.rewards, isNot(contains(pq_logic.RewardKind.equip)));
|
expect(act2.rewards, isNot(contains(pq_logic.RewardKind.equip)));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user