feat(animation): Act 기반 몬스터 사이즈 시스템 추가
- Act 진행에 따른 몬스터 사이즈 확률 조정 - 보스: Act별 고정 사이즈 (소/중/대) - 일반/엘리트: Act별 확률 랜덤 - TaskInfo에 monsterSize 필드 추가 - 애니메이션 패널에서 Act 기반 사이즈 사용
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
// 몬스터 크기 시스템
|
||||
// 몬스터 레벨에 따라 ASCII 아트 크기 결정
|
||||
// Act와 몬스터 등급에 따라 ASCII 아트 크기 결정
|
||||
|
||||
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||
|
||||
/// 몬스터 크기 enum
|
||||
/// 실제 프레임 줄 수와 일치하도록 설정
|
||||
@@ -48,3 +51,55 @@ MonsterSize getMonsterSize(int? level) {
|
||||
int getMonsterVerticalPadding(MonsterSize size) {
|
||||
return (7 - size.lines) ~/ 2;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Act 기반 몬스터 사이즈 결정 (Phase 13)
|
||||
// =============================================================================
|
||||
|
||||
/// 보스 몬스터 사이즈 결정 (Act별 고정)
|
||||
///
|
||||
/// - Prologue(1), Act I(2): small (4줄)
|
||||
/// - Act II(3), Act III(4): medium (6줄)
|
||||
/// - Act IV(5), Act V(6)+: large (8줄)
|
||||
MonsterSize getBossSizeForAct(int plotStageCount) {
|
||||
if (plotStageCount <= 2) return MonsterSize.small;
|
||||
if (plotStageCount <= 4) return MonsterSize.medium;
|
||||
return MonsterSize.large;
|
||||
}
|
||||
|
||||
/// 일반/엘리트 몬스터 사이즈 결정 (Act별 확률 랜덤)
|
||||
///
|
||||
/// Act가 진행될수록 큰 몬스터가 나올 확률 증가.
|
||||
/// 프롤로그에서는 대형 몬스터 0%.
|
||||
MonsterSize getRandomSizeForAct(int plotStageCount, DeterministicRandom rng) {
|
||||
final roll = rng.nextInt(100);
|
||||
|
||||
// Act별 확률 테이블 (small %, medium %, large = 나머지)
|
||||
final (smallChance, mediumChance) = switch (plotStageCount) {
|
||||
<= 1 => (70, 30), // Prologue: 70% small, 30% medium, 0% large
|
||||
2 => (50, 40), // Act I: 50% small, 40% medium, 10% large
|
||||
3 => (35, 40), // Act II: 35% small, 40% medium, 25% large
|
||||
4 => (25, 40), // Act III: 25% small, 40% medium, 35% large
|
||||
5 => (15, 35), // Act IV: 15% small, 35% medium, 50% large
|
||||
_ => (10, 25), // Act V+: 10% small, 25% medium, 65% large
|
||||
};
|
||||
|
||||
if (roll < smallChance) return MonsterSize.small;
|
||||
if (roll < smallChance + mediumChance) return MonsterSize.medium;
|
||||
return MonsterSize.large;
|
||||
}
|
||||
|
||||
/// 몬스터 등급과 Act에 따른 사이즈 결정 (통합 함수)
|
||||
///
|
||||
/// 보스: Act별 고정 사이즈
|
||||
/// 일반/엘리트: Act별 확률 랜덤
|
||||
MonsterSize getMonsterSizeForAct({
|
||||
required int plotStageCount,
|
||||
required MonsterGrade grade,
|
||||
required DeterministicRandom rng,
|
||||
}) {
|
||||
if (grade == MonsterGrade.boss) {
|
||||
return getBossSizeForAct(plotStageCount);
|
||||
}
|
||||
return getRandomSizeForAct(plotStageCount, rng);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||
import 'package:asciineverdie/src/core/animation/monster_size.dart';
|
||||
import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
|
||||
import 'package:asciineverdie/src/core/model/combat_state.dart';
|
||||
import 'package:asciineverdie/src/core/model/combat_stats.dart';
|
||||
@@ -253,6 +254,7 @@ class ActProgressionService {
|
||||
monsterPart: '*', // 특수 전리품
|
||||
monsterLevel: glitchGod.level,
|
||||
monsterGrade: MonsterGrade.boss, // 최종 보스는 항상 boss 등급
|
||||
monsterSize: MonsterSize.large, // 최종 보스는 항상 대형
|
||||
),
|
||||
currentCombat: combatState,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||
import 'package:asciineverdie/src/core/animation/monster_size.dart';
|
||||
import 'package:asciineverdie/src/core/engine/act_progression_service.dart';
|
||||
import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
|
||||
import 'package:asciineverdie/src/core/engine/combat_tick_service.dart';
|
||||
@@ -585,6 +586,7 @@ class ProgressService {
|
||||
monsterPart: '*', // Boss는 WinItem 드랍
|
||||
monsterLevel: actBoss.monsterStats.level,
|
||||
monsterGrade: MonsterGrade.boss,
|
||||
monsterSize: getBossSizeForAct(state.progress.plotStageCount),
|
||||
),
|
||||
currentCombat: actBoss,
|
||||
);
|
||||
@@ -673,6 +675,13 @@ class ProgressService {
|
||||
durationMillis,
|
||||
);
|
||||
|
||||
// 몬스터 사이즈 결정 (Act 기반, Phase 13)
|
||||
final monsterSize = getMonsterSizeForAct(
|
||||
plotStageCount: state.progress.plotStageCount,
|
||||
grade: monsterResult.grade,
|
||||
rng: state.rng,
|
||||
);
|
||||
|
||||
progress = taskResult.progress.copyWith(
|
||||
currentTask: TaskInfo(
|
||||
caption: taskResult.caption,
|
||||
@@ -681,6 +690,7 @@ class ProgressService {
|
||||
monsterPart: monsterResult.part,
|
||||
monsterLevel: effectiveMonsterLevel,
|
||||
monsterGrade: monsterResult.grade,
|
||||
monsterSize: monsterSize,
|
||||
),
|
||||
currentCombat: combatState,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:asciineverdie/src/core/animation/monster_size.dart';
|
||||
import 'package:asciineverdie/src/core/model/combat_event.dart';
|
||||
import 'package:asciineverdie/src/core/model/combat_state.dart';
|
||||
import 'package:asciineverdie/src/core/model/equipment_item.dart';
|
||||
@@ -315,6 +316,7 @@ class TaskInfo {
|
||||
this.monsterPart,
|
||||
this.monsterLevel,
|
||||
this.monsterGrade,
|
||||
this.monsterSize,
|
||||
});
|
||||
|
||||
final String caption;
|
||||
@@ -326,12 +328,15 @@ class TaskInfo {
|
||||
/// 킬 태스크의 전리품 부위 (예: "claw", "tail", "*"는 WinItem)
|
||||
final String? monsterPart;
|
||||
|
||||
/// 킬 태스크의 몬스터 레벨 (애니메이션 크기 결정용)
|
||||
/// 킬 태스크의 몬스터 레벨 (전투 스탯 계산용)
|
||||
final int? monsterLevel;
|
||||
|
||||
/// 킬 태스크의 몬스터 등급 (Normal/Elite/Boss)
|
||||
final MonsterGrade? monsterGrade;
|
||||
|
||||
/// 킬 태스크의 몬스터 사이즈 (애니메이션 크기 결정용, Act 기반)
|
||||
final MonsterSize? monsterSize;
|
||||
|
||||
factory TaskInfo.empty() =>
|
||||
const TaskInfo(caption: '', type: TaskType.neutral);
|
||||
|
||||
@@ -342,6 +347,7 @@ class TaskInfo {
|
||||
String? monsterPart,
|
||||
int? monsterLevel,
|
||||
MonsterGrade? monsterGrade,
|
||||
MonsterSize? monsterSize,
|
||||
}) {
|
||||
return TaskInfo(
|
||||
caption: caption ?? this.caption,
|
||||
@@ -350,6 +356,7 @@ class TaskInfo {
|
||||
monsterPart: monsterPart ?? this.monsterPart,
|
||||
monsterLevel: monsterLevel ?? this.monsterLevel,
|
||||
monsterGrade: monsterGrade ?? this.monsterGrade,
|
||||
monsterSize: monsterSize ?? this.monsterSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,6 +734,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
characterLevel: state.traits.level,
|
||||
monsterLevel: state.progress.currentTask.monsterLevel,
|
||||
monsterGrade: state.progress.currentTask.monsterGrade,
|
||||
monsterSize: state.progress.currentTask.monsterSize,
|
||||
latestCombatEvent:
|
||||
state.progress.currentCombat?.recentEvents.lastOrNull,
|
||||
raceId: state.traits.raceId,
|
||||
|
||||
@@ -48,6 +48,9 @@ class GameSessionController extends ChangeNotifier {
|
||||
GameState? _state;
|
||||
String? _error;
|
||||
|
||||
// 배속 저장 (pause/resume 시 유지)
|
||||
int _savedSpeedMultiplier = 1;
|
||||
|
||||
// 자동 부활 (Auto-Resurrection) 상태
|
||||
bool _autoResurrect = false;
|
||||
|
||||
@@ -89,7 +92,8 @@ class GameSessionController extends ChangeNotifier {
|
||||
bool isNewGame = true,
|
||||
}) async {
|
||||
// 기존 배속 보존 (부활/재개 시 유지)
|
||||
final previousSpeed = _loop?.speedMultiplier ?? 1;
|
||||
// _loop가 있으면 현재 배속 사용, 없으면 저장된 배속 사용
|
||||
final previousSpeed = _loop?.speedMultiplier ?? _savedSpeedMultiplier;
|
||||
|
||||
await _stopLoop(saveOnStop: false);
|
||||
|
||||
@@ -284,6 +288,12 @@ class GameSessionController extends ChangeNotifier {
|
||||
Future<void>? _stopLoop({required bool saveOnStop}) {
|
||||
final loop = _loop;
|
||||
final sub = _subscription;
|
||||
|
||||
// 배속 저장 (resume 시 복원용)
|
||||
if (loop != null) {
|
||||
_savedSpeedMultiplier = loop.speedMultiplier;
|
||||
}
|
||||
|
||||
_loop = null;
|
||||
_subscription = null;
|
||||
|
||||
|
||||
@@ -730,6 +730,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
||||
characterLevel: state.traits.level,
|
||||
monsterLevel: state.progress.currentTask.monsterLevel,
|
||||
monsterGrade: state.progress.currentTask.monsterGrade,
|
||||
monsterSize: state.progress.currentTask.monsterSize,
|
||||
latestCombatEvent:
|
||||
state.progress.currentCombat?.recentEvents.lastOrNull,
|
||||
raceId: state.traits.raceId,
|
||||
|
||||
@@ -47,6 +47,7 @@ class AsciiAnimationCard extends StatefulWidget {
|
||||
this.characterLevel,
|
||||
this.monsterLevel,
|
||||
this.monsterGrade,
|
||||
this.monsterSize,
|
||||
this.isPaused = false,
|
||||
this.isInCombat = true,
|
||||
this.monsterDied = false,
|
||||
@@ -85,12 +86,15 @@ class AsciiAnimationCard extends StatefulWidget {
|
||||
/// 캐릭터 레벨
|
||||
final int? characterLevel;
|
||||
|
||||
/// 몬스터 레벨 (몬스터 크기 결정용)
|
||||
/// 몬스터 레벨 (전투 스탯 계산용)
|
||||
final int? monsterLevel;
|
||||
|
||||
/// 몬스터 등급 (Normal/Elite/Boss) - 색상/접두사 표시용
|
||||
final MonsterGrade? monsterGrade;
|
||||
|
||||
/// 몬스터 사이즈 (애니메이션 크기 결정용, Act 기반)
|
||||
final MonsterSize? monsterSize;
|
||||
|
||||
/// 최근 전투 이벤트 (애니메이션 동기화용)
|
||||
final CombatEvent? latestCombatEvent;
|
||||
|
||||
@@ -175,7 +179,8 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
bool _showDeathAnimation = false;
|
||||
List<String>? _deathAnimationMonsterLines;
|
||||
String? _lastMonsterBaseName;
|
||||
int? _lastMonsterLevel; // 몬스터 레벨 캐시 (사망 시 크기 결정용)
|
||||
int? _lastMonsterLevel; // 몬스터 레벨 캐시 (사망 시 스탯용)
|
||||
MonsterSize? _lastMonsterSize; // 몬스터 사이즈 캐시 (사망 시 크기 결정용)
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -245,6 +250,9 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
if (widget.monsterLevel != null) {
|
||||
_lastMonsterLevel = widget.monsterLevel;
|
||||
}
|
||||
if (widget.monsterSize != null) {
|
||||
_lastMonsterSize = widget.monsterSize;
|
||||
}
|
||||
|
||||
// 새 몬스터 등장 시 사망 애니메이션 상태 리셋
|
||||
// (이전 몬스터 사망 애니메이션이 끝나기 전에 새 전투 시작 시 대응)
|
||||
@@ -591,7 +599,9 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
final hasShield =
|
||||
widget.shieldName != null && widget.shieldName!.isNotEmpty;
|
||||
final monsterCategory = getMonsterCategory(widget.monsterBaseName);
|
||||
final monsterSize = getMonsterSize(widget.monsterLevel);
|
||||
// Act 기반 사이즈 사용 (Phase 13), 없으면 레벨 기반 fallback
|
||||
final monsterSize =
|
||||
widget.monsterSize ?? getMonsterSize(widget.monsterLevel);
|
||||
|
||||
_battleComposer = CanvasBattleComposer(
|
||||
weaponCategory: weaponCategory,
|
||||
@@ -617,8 +627,10 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
if (monsterName == null) return null;
|
||||
|
||||
final monsterCategory = getMonsterCategory(monsterName);
|
||||
// 캐시된 레벨 사용 (사망 시점에 widget.monsterLevel이 null일 수 있음)
|
||||
final monsterSize = getMonsterSize(_lastMonsterLevel ?? widget.monsterLevel);
|
||||
// 캐시된 사이즈 사용 (사망 시점에 widget.monsterSize가 null일 수 있음)
|
||||
final monsterSize = _lastMonsterSize ??
|
||||
widget.monsterSize ??
|
||||
getMonsterSize(_lastMonsterLevel ?? widget.monsterLevel);
|
||||
|
||||
// 몬스터 Idle 프레임 가져오기
|
||||
final frames = getMonsterIdleFrames(monsterCategory, monsterSize);
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||
import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
|
||||
import 'package:asciineverdie/src/core/animation/monster_size.dart';
|
||||
import 'package:asciineverdie/src/core/model/combat_event.dart';
|
||||
import 'package:asciineverdie/src/core/model/combat_state.dart';
|
||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||
@@ -32,6 +33,7 @@ class EnhancedAnimationPanel extends StatefulWidget {
|
||||
this.characterLevel,
|
||||
this.monsterLevel,
|
||||
this.monsterGrade,
|
||||
this.monsterSize,
|
||||
this.latestCombatEvent,
|
||||
this.raceId,
|
||||
this.weaponRarity,
|
||||
@@ -52,6 +54,9 @@ class EnhancedAnimationPanel extends StatefulWidget {
|
||||
|
||||
/// 몬스터 등급 (Normal/Elite/Boss) - UI 색상/접두사 표시용
|
||||
final MonsterGrade? monsterGrade;
|
||||
|
||||
/// 몬스터 사이즈 (애니메이션 크기 결정용, Act 기반)
|
||||
final MonsterSize? monsterSize;
|
||||
final CombatEvent? latestCombatEvent;
|
||||
|
||||
/// 종족 ID (Phase 4: 종족별 캐릭터 애니메이션)
|
||||
@@ -225,6 +230,7 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
||||
characterLevel: widget.characterLevel,
|
||||
monsterLevel: widget.monsterLevel,
|
||||
monsterGrade: widget.monsterGrade,
|
||||
monsterSize: widget.monsterSize,
|
||||
isPaused: widget.isPaused,
|
||||
isInCombat: isInCombat,
|
||||
monsterDied: _monsterDied,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||
import 'package:asciineverdie/l10n/app_localizations.dart';
|
||||
import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
|
||||
import 'package:asciineverdie/src/core/animation/monster_size.dart';
|
||||
import 'package:asciineverdie/src/core/model/combat_event.dart';
|
||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||
@@ -25,6 +26,7 @@ class TaskProgressPanel extends StatelessWidget {
|
||||
this.characterLevel,
|
||||
this.monsterLevel,
|
||||
this.monsterGrade,
|
||||
this.monsterSize,
|
||||
this.latestCombatEvent,
|
||||
this.raceId,
|
||||
});
|
||||
@@ -49,6 +51,9 @@ class TaskProgressPanel extends StatelessWidget {
|
||||
/// 몬스터 등급 (Normal/Elite/Boss) - UI 색상/접두사 표시용
|
||||
final MonsterGrade? monsterGrade;
|
||||
|
||||
/// 몬스터 사이즈 (애니메이션 크기 결정용, Act 기반)
|
||||
final MonsterSize? monsterSize;
|
||||
|
||||
/// 최근 전투 이벤트 (애니메이션 동기화용, Phase 5)
|
||||
final CombatEvent? latestCombatEvent;
|
||||
|
||||
@@ -80,6 +85,7 @@ class TaskProgressPanel extends StatelessWidget {
|
||||
characterLevel: characterLevel,
|
||||
monsterLevel: monsterLevel,
|
||||
monsterGrade: monsterGrade,
|
||||
monsterSize: monsterSize,
|
||||
isPaused: isPaused,
|
||||
latestCombatEvent: latestCombatEvent,
|
||||
raceId: raceId,
|
||||
|
||||
Reference in New Issue
Block a user