feat(animation): Act 기반 몬스터 사이즈 시스템 추가

- Act 진행에 따른 몬스터 사이즈 확률 조정
- 보스: Act별 고정 사이즈 (소/중/대)
- 일반/엘리트: Act별 확률 랜덤
- TaskInfo에 monsterSize 필드 추가
- 애니메이션 패널에서 Act 기반 사이즈 사용
This commit is contained in:
JiWoong Sul
2026-01-15 18:01:31 +09:00
parent 23f15f41d3
commit ac76060222
10 changed files with 118 additions and 8 deletions

View File

@@ -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);
}

View File

@@ -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,
);

View File

@@ -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,
);

View File

@@ -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,
);
}
}