Compare commits
4 Commits
76090a46b6
...
61edd87252
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61edd87252 | ||
|
|
c4d3565f62 | ||
|
|
5f9a063ae4 | ||
|
|
1eaff23001 |
@@ -300,6 +300,9 @@
|
|||||||
"endingHallOfFameLine2": "remembered in the Hall of Fame",
|
"endingHallOfFameLine2": "remembered in the Hall of Fame",
|
||||||
"@endingHallOfFameLine2": { "description": "Hall of fame message line 2" },
|
"@endingHallOfFameLine2": { "description": "Hall of fame message line 2" },
|
||||||
|
|
||||||
|
"endingHallOfFameButton": "HALL OF FAME",
|
||||||
|
"@endingHallOfFameButton": { "description": "Hall of fame button" },
|
||||||
|
|
||||||
"endingSkip": "SKIP",
|
"endingSkip": "SKIP",
|
||||||
"@endingSkip": { "description": "Skip button" },
|
"@endingSkip": { "description": "Skip button" },
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,7 @@
|
|||||||
"endingLegendLivesOn": "あなたの伝説は続く...",
|
"endingLegendLivesOn": "あなたの伝説は続く...",
|
||||||
"endingHallOfFameLine1": "あなたの英雄的な功績は",
|
"endingHallOfFameLine1": "あなたの英雄的な功績は",
|
||||||
"endingHallOfFameLine2": "殿堂に記録されます",
|
"endingHallOfFameLine2": "殿堂に記録されます",
|
||||||
|
"endingHallOfFameButton": "殿堂入り",
|
||||||
"endingSkip": "スキップ",
|
"endingSkip": "スキップ",
|
||||||
"endingTapToSkip": "タップでスキップ"
|
"endingTapToSkip": "タップでスキップ"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@
|
|||||||
"endingLegendLivesOn": "당신의 전설은 계속됩니다...",
|
"endingLegendLivesOn": "당신의 전설은 계속됩니다...",
|
||||||
"endingHallOfFameLine1": "당신의 영웅적인 업적이",
|
"endingHallOfFameLine1": "당신의 영웅적인 업적이",
|
||||||
"endingHallOfFameLine2": "명예의 전당에 기록됩니다",
|
"endingHallOfFameLine2": "명예의 전당에 기록됩니다",
|
||||||
|
"endingHallOfFameButton": "명예의 전당",
|
||||||
"endingSkip": "건너뛰기",
|
"endingSkip": "건너뛰기",
|
||||||
"endingTapToSkip": "탭하여 건너뛰기"
|
"endingTapToSkip": "탭하여 건너뛰기"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -629,6 +629,12 @@ abstract class L10n {
|
|||||||
/// **'remembered in the Hall of Fame'**
|
/// **'remembered in the Hall of Fame'**
|
||||||
String get endingHallOfFameLine2;
|
String get endingHallOfFameLine2;
|
||||||
|
|
||||||
|
/// Hall of fame button
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'HALL OF FAME'**
|
||||||
|
String get endingHallOfFameButton;
|
||||||
|
|
||||||
/// Skip button
|
/// Skip button
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -286,6 +286,9 @@ class L10nEn extends L10n {
|
|||||||
@override
|
@override
|
||||||
String get endingHallOfFameLine2 => 'remembered in the Hall of Fame';
|
String get endingHallOfFameLine2 => 'remembered in the Hall of Fame';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get endingHallOfFameButton => 'HALL OF FAME';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get endingSkip => 'SKIP';
|
String get endingSkip => 'SKIP';
|
||||||
|
|
||||||
|
|||||||
@@ -286,6 +286,9 @@ class L10nJa extends L10n {
|
|||||||
@override
|
@override
|
||||||
String get endingHallOfFameLine2 => '殿堂に記録されます';
|
String get endingHallOfFameLine2 => '殿堂に記録されます';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get endingHallOfFameButton => '殿堂入り';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get endingSkip => 'スキップ';
|
String get endingSkip => 'スキップ';
|
||||||
|
|
||||||
|
|||||||
@@ -286,6 +286,9 @@ class L10nKo extends L10n {
|
|||||||
@override
|
@override
|
||||||
String get endingHallOfFameLine2 => '명예의 전당에 기록됩니다';
|
String get endingHallOfFameLine2 => '명예의 전당에 기록됩니다';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get endingHallOfFameButton => '명예의 전당';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get endingSkip => '건너뛰기';
|
String get endingSkip => '건너뛰기';
|
||||||
|
|
||||||
|
|||||||
@@ -286,6 +286,9 @@ class L10nZh extends L10n {
|
|||||||
@override
|
@override
|
||||||
String get endingHallOfFameLine2 => '将被铭记于荣誉殿堂';
|
String get endingHallOfFameLine2 => '将被铭记于荣誉殿堂';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get endingHallOfFameButton => '荣誉殿堂';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get endingSkip => '跳过';
|
String get endingSkip => '跳过';
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,7 @@
|
|||||||
"endingLegendLivesOn": "您的传奇将永远流传...",
|
"endingLegendLivesOn": "您的传奇将永远流传...",
|
||||||
"endingHallOfFameLine1": "您的英雄事迹",
|
"endingHallOfFameLine1": "您的英雄事迹",
|
||||||
"endingHallOfFameLine2": "将被铭记于荣誉殿堂",
|
"endingHallOfFameLine2": "将被铭记于荣誉殿堂",
|
||||||
|
"endingHallOfFameButton": "荣誉殿堂",
|
||||||
"endingSkip": "跳过",
|
"endingSkip": "跳过",
|
||||||
"endingTapToSkip": "点击跳过"
|
"endingTapToSkip": "点击跳过"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -355,12 +355,14 @@ class ProgressService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Gain XP / level up.
|
// Gain XP / level up.
|
||||||
|
// 최대 레벨(100) 제한: 100레벨에서는 더 이상 레벨업하지 않음
|
||||||
if (gain) {
|
if (gain) {
|
||||||
if (progress.exp.position >= progress.exp.max) {
|
if (progress.exp.position >= progress.exp.max &&
|
||||||
|
nextState.traits.level < 100) {
|
||||||
nextState = _levelUp(nextState);
|
nextState = _levelUp(nextState);
|
||||||
leveledUp = true;
|
leveledUp = true;
|
||||||
progress = nextState.progress;
|
progress = nextState.progress;
|
||||||
} else {
|
} else if (nextState.traits.level < 100) {
|
||||||
final uncappedExp = progress.exp.position + incrementSeconds;
|
final uncappedExp = progress.exp.position + incrementSeconds;
|
||||||
final int newExpPos = uncappedExp > progress.exp.max
|
final int newExpPos = uncappedExp > progress.exp.max
|
||||||
? progress.exp.max
|
? progress.exp.max
|
||||||
@@ -863,8 +865,8 @@ class ProgressService {
|
|||||||
final targetLevel = ActMonsterLevel.forPlotStage(nextPlotStage);
|
final targetLevel = ActMonsterLevel.forPlotStage(nextPlotStage);
|
||||||
var nextState = state;
|
var nextState = state;
|
||||||
|
|
||||||
// 현재 레벨이 목표 레벨보다 낮으면 레벨업
|
// 현재 레벨이 목표 레벨보다 낮으면 레벨업 (최대 100레벨)
|
||||||
while (nextState.traits.level < targetLevel) {
|
while (nextState.traits.level < targetLevel && nextState.traits.level < 100) {
|
||||||
nextState = _levelUp(nextState);
|
nextState = _levelUp(nextState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -885,6 +887,11 @@ class ProgressService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GameState _levelUp(GameState state) {
|
GameState _levelUp(GameState state) {
|
||||||
|
// 최대 레벨(100) 안전장치: 이미 100레벨이면 레벨업하지 않음
|
||||||
|
if (state.traits.level >= 100) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
final nextLevel = state.traits.level + 1;
|
final nextLevel = state.traits.level + 1;
|
||||||
final rng = state.rng;
|
final rng = state.rng;
|
||||||
final hpGain = state.stats.con ~/ 3 + 1 + rng.nextInt(4);
|
final hpGain = state.stats.con ~/ 3 + 1 + rng.nextInt(4);
|
||||||
|
|||||||
@@ -159,14 +159,14 @@ class MonsterBaseStats {
|
|||||||
/// 레벨 기반 기본 스탯 생성
|
/// 레벨 기반 기본 스탯 생성
|
||||||
///
|
///
|
||||||
/// HP: 50 + level * 20 + (level^2 / 5)
|
/// HP: 50 + level * 20 + (level^2 / 5)
|
||||||
/// ATK: 3 + level * 2 (초반 생존율 개선을 위해 약화)
|
/// ATK: 5 + level * 4 (플레이어 DEF 스케일링에 맞춰 상향)
|
||||||
/// DEF: 2 + level * 2
|
/// DEF: 2 + level * 2
|
||||||
/// EXP: 10 + level * 5
|
/// EXP: 10 + level * 5
|
||||||
/// GOLD: 5 + level * 3
|
/// GOLD: 5 + level * 3
|
||||||
factory MonsterBaseStats.forLevel(int level) {
|
factory MonsterBaseStats.forLevel(int level) {
|
||||||
return MonsterBaseStats(
|
return MonsterBaseStats(
|
||||||
hp: 50 + level * 20 + (level * level ~/ 5),
|
hp: 50 + level * 20 + (level * level ~/ 5),
|
||||||
atk: 3 + level * 2,
|
atk: 5 + level * 4,
|
||||||
def: 2 + level * 2,
|
def: 2 + level * 2,
|
||||||
exp: 10 + level * 5,
|
exp: 10 + level * 5,
|
||||||
gold: 5 + level * 3,
|
gold: 5 + level * 3,
|
||||||
|
|||||||
@@ -71,10 +71,25 @@ class ItemResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int levelUpTimeSeconds(int level) {
|
int levelUpTimeSeconds(int level) {
|
||||||
// 10시간 내 레벨 100 도달 목표 (선형 성장)
|
// Act 진행과 레벨업 동기화 (10시간 완주 목표)
|
||||||
// 레벨 1: ~2분, 레벨 100: ~7분
|
// Act I 끝(2시간): 레벨 20, Act II 끝(5시간): 레벨 40, etc.
|
||||||
final seconds = 120 + (level * 3);
|
// 레벨 1: ~5분, 레벨 50: ~7분, 레벨 100: ~4분 (후반 가속)
|
||||||
return seconds;
|
if (level <= 20) {
|
||||||
|
// Act I: 레벨 1-20, 2시간 (7200초) → 평균 360초/레벨
|
||||||
|
return 300 + (level * 6);
|
||||||
|
} else if (level <= 40) {
|
||||||
|
// Act II: 레벨 21-40, 3시간 (10800초) → 평균 540초/레벨
|
||||||
|
return 400 + ((level - 20) * 10);
|
||||||
|
} else if (level <= 60) {
|
||||||
|
// Act III: 레벨 41-60, 3시간 (10800초) → 평균 540초/레벨
|
||||||
|
return 400 + ((level - 40) * 10);
|
||||||
|
} else if (level <= 80) {
|
||||||
|
// Act IV: 레벨 61-80, 1.5시간 (5400초) → 평균 270초/레벨
|
||||||
|
return 200 + ((level - 60) * 5);
|
||||||
|
} else {
|
||||||
|
// Act V: 레벨 81-100, 30분 (1800초) → 평균 90초/레벨
|
||||||
|
return 60 + ((level - 80) * 3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 초 단위 시간을 사람이 읽기 쉬운 형태로 변환 (원본 Main.pas:1265-1271)
|
/// 초 단위 시간을 사람이 읽기 쉬운 형태로 변환 (원본 Main.pas:1265-1271)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import 'package:asciineverdie/src/shared/retro_colors.dart';
|
|||||||
/// 게임 클리어 엔딩 오버레이 (Act V 완료 시)
|
/// 게임 클리어 엔딩 오버레이 (Act V 완료 시)
|
||||||
///
|
///
|
||||||
/// 영화 엔딩 크레딧 스타일로 텍스트가 아래에서 위로 스크롤됨
|
/// 영화 엔딩 크레딧 스타일로 텍스트가 아래에서 위로 스크롤됨
|
||||||
|
/// - 탭/클릭 시 스크롤 최하단으로 즉시 이동
|
||||||
|
/// - 최하단에 명예의 전당 버튼 표시
|
||||||
class VictoryOverlay extends StatefulWidget {
|
class VictoryOverlay extends StatefulWidget {
|
||||||
const VictoryOverlay({
|
const VictoryOverlay({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -35,6 +37,9 @@ class _VictoryOverlayState extends State<VictoryOverlay>
|
|||||||
late AnimationController _scrollController;
|
late AnimationController _scrollController;
|
||||||
late Animation<double> _scrollAnimation;
|
late Animation<double> _scrollAnimation;
|
||||||
|
|
||||||
|
// 스크롤이 완료(최하단 도달) 되었는지 여부
|
||||||
|
bool _isScrollComplete = false;
|
||||||
|
|
||||||
// 스크롤 지속 시간 (밀리초)
|
// 스크롤 지속 시간 (밀리초)
|
||||||
static const _scrollDurationMs = 25000; // 25초
|
static const _scrollDurationMs = 25000; // 25초
|
||||||
|
|
||||||
@@ -51,10 +56,12 @@ class _VictoryOverlayState extends State<VictoryOverlay>
|
|||||||
CurvedAnimation(parent: _scrollController, curve: Curves.linear),
|
CurvedAnimation(parent: _scrollController, curve: Curves.linear),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 스크롤 완료 시 자동 종료
|
// 스크롤 완료 시 버튼 표시 (자동 종료하지 않음)
|
||||||
_scrollController.addStatusListener((status) {
|
_scrollController.addStatusListener((status) {
|
||||||
if (status == AnimationStatus.completed) {
|
if (status == AnimationStatus.completed) {
|
||||||
widget.onComplete();
|
setState(() {
|
||||||
|
_isScrollComplete = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,12 +74,21 @@ class _VictoryOverlayState extends State<VictoryOverlay>
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 탭 시 스크롤 최하단으로 즉시 이동
|
||||||
|
void _skipToEnd() {
|
||||||
|
_scrollController.stop();
|
||||||
|
_scrollController.value = 1.0;
|
||||||
|
setState(() {
|
||||||
|
_isScrollComplete = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final gold = RetroColors.goldOf(context);
|
final gold = RetroColors.goldOf(context);
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: widget.onComplete, // 탭으로 스킵
|
onTap: _isScrollComplete ? null : _skipToEnd, // 스크롤 중에만 탭으로 스킵
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
@@ -86,38 +102,40 @@ class _VictoryOverlayState extends State<VictoryOverlay>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
// 스킵 버튼
|
// 스킵 버튼 (스크롤 중에만 표시)
|
||||||
Positioned(
|
if (!_isScrollComplete)
|
||||||
top: 16,
|
Positioned(
|
||||||
right: 16,
|
top: 16,
|
||||||
child: TextButton(
|
right: 16,
|
||||||
onPressed: widget.onComplete,
|
child: TextButton(
|
||||||
child: Text(
|
onPressed: _skipToEnd,
|
||||||
L10n.of(context).endingSkip,
|
child: Text(
|
||||||
style: TextStyle(
|
L10n.of(context).endingSkip,
|
||||||
fontFamily: 'PressStart2P',
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontFamily: 'PressStart2P',
|
||||||
color: gold.withValues(alpha: 0.5),
|
fontSize: 10,
|
||||||
|
color: gold.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
// 하단 탭 힌트
|
// 하단 탭 힌트 (스크롤 중에만 표시)
|
||||||
Positioned(
|
if (!_isScrollComplete)
|
||||||
bottom: 16,
|
Positioned(
|
||||||
left: 0,
|
bottom: 16,
|
||||||
right: 0,
|
left: 0,
|
||||||
child: Text(
|
right: 0,
|
||||||
L10n.of(context).endingTapToSkip,
|
child: Text(
|
||||||
style: TextStyle(
|
L10n.of(context).endingTapToSkip,
|
||||||
fontFamily: 'PressStart2P',
|
style: TextStyle(
|
||||||
fontSize: 8,
|
fontFamily: 'PressStart2P',
|
||||||
color: Colors.white.withValues(alpha: 0.2),
|
fontSize: 8,
|
||||||
|
color: Colors.white.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -145,7 +163,8 @@ class _VictoryOverlayState extends State<VictoryOverlay>
|
|||||||
|
|
||||||
double _estimateContentHeight() {
|
double _estimateContentHeight() {
|
||||||
// 대략적인 콘텐츠 높이 추정 (스크롤 계산용)
|
// 대략적인 콘텐츠 높이 추정 (스크롤 계산용)
|
||||||
return 1500.0;
|
// 명예의 전당 버튼 추가로 인해 높이 증가
|
||||||
|
return 1600.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCreditContent(BuildContext context) {
|
Widget _buildCreditContent(BuildContext context) {
|
||||||
@@ -231,7 +250,13 @@ class _VictoryOverlayState extends State<VictoryOverlay>
|
|||||||
// THE END
|
// THE END
|
||||||
// ═══════════════════════════════════
|
// ═══════════════════════════════════
|
||||||
_buildTheEnd(context, gold),
|
_buildTheEnd(context, gold),
|
||||||
const SizedBox(height: 200), // 여백 (스크롤 끝)
|
const SizedBox(height: 60),
|
||||||
|
|
||||||
|
// ═══════════════════════════════════
|
||||||
|
// HALL OF FAME BUTTON
|
||||||
|
// ═══════════════════════════════════
|
||||||
|
_buildHallOfFameButton(context, gold),
|
||||||
|
const SizedBox(height: 100), // 여백 (스크롤 끝)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -518,7 +543,6 @@ class _VictoryOverlayState extends State<VictoryOverlay>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTheEnd(BuildContext context, Color gold) {
|
Widget _buildTheEnd(BuildContext context, Color gold) {
|
||||||
final l10n = L10n.of(context);
|
|
||||||
const theEnd = '''
|
const theEnd = '''
|
||||||
████████╗██╗ ██╗███████╗ ███████╗███╗ ██╗██████╗
|
████████╗██╗ ██╗███████╗ ███████╗███╗ ██╗██████╗
|
||||||
╚══██╔══╝██║ ██║██╔════╝ ██╔════╝████╗ ██║██╔══██╗
|
╚══██╔══╝██║ ██║██╔════╝ ██╔════╝████╗ ██║██╔══██╗
|
||||||
@@ -539,7 +563,17 @@ class _VictoryOverlayState extends State<VictoryOverlay>
|
|||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 명예의 전당 버튼 (최하단)
|
||||||
|
Widget _buildHallOfFameButton(BuildContext context, Color gold) {
|
||||||
|
final l10n = L10n.of(context);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// 안내 텍스트
|
||||||
Text(
|
Text(
|
||||||
l10n.endingHallOfFameLine1,
|
l10n.endingHallOfFameLine1,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -557,6 +591,38 @@ class _VictoryOverlayState extends State<VictoryOverlay>
|
|||||||
color: gold.withValues(alpha: 0.7),
|
color: gold.withValues(alpha: 0.7),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 명예의 전당 버튼
|
||||||
|
SizedBox(
|
||||||
|
width: 280,
|
||||||
|
height: 56,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: widget.onComplete,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: gold,
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
elevation: 8,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.emoji_events, size: 24),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
l10n.endingHallOfFameButton,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,16 @@ 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', () {
|
||||||
// 새 공식: 120 + (level * 3) - 10시간 내 레벨 100 도달 목표
|
// Act 진행과 동기화된 레벨업 시간
|
||||||
expect(pq_logic.levelUpTime(1), 123); // 120 + 3 = 123초 (~2분)
|
// Act I (레벨 1-20): 300 + level * 6
|
||||||
expect(pq_logic.levelUpTime(10), 150); // 120 + 30 = 150초 (~2.5분)
|
expect(pq_logic.levelUpTime(1), 306); // 300 + 6 = 306초 (~5분)
|
||||||
expect(pq_logic.levelUpTime(100), 420); // 120 + 300 = 420초 (~7분)
|
expect(pq_logic.levelUpTime(10), 360); // 300 + 60 = 360초 (6분)
|
||||||
|
expect(pq_logic.levelUpTime(20), 420); // 300 + 120 = 420초 (7분)
|
||||||
|
// Act II/III (레벨 21-60): 400 + (level-X) * 10
|
||||||
|
expect(pq_logic.levelUpTime(30), 500); // 400 + 100 = 500초
|
||||||
|
expect(pq_logic.levelUpTime(50), 500); // 400 + 100 = 500초
|
||||||
|
// Act V (레벨 81-100): 60 + (level-80) * 3 (후반 가속)
|
||||||
|
expect(pq_logic.levelUpTime(100), 120); // 60 + 60 = 120초 (2분)
|
||||||
});
|
});
|
||||||
|
|
||||||
test('roughTime formats seconds into human-readable strings', () {
|
test('roughTime formats seconds into human-readable strings', () {
|
||||||
|
|||||||
Reference in New Issue
Block a user