Compare commits
5 Commits
9ecf9d1692
...
20421dafd7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20421dafd7 | ||
|
|
7570a4205c | ||
|
|
4688aff56b | ||
|
|
5c8ab0d3f4 | ||
|
|
e112378ad2 |
BIN
assets/audio/sfx/block.mp3
Normal file
BIN
assets/audio/sfx/block.mp3
Normal file
Binary file not shown.
BIN
assets/audio/sfx/evade.mp3
Normal file
BIN
assets/audio/sfx/evade.mp3
Normal file
Binary file not shown.
BIN
assets/audio/sfx/parry.mp3
Normal file
BIN
assets/audio/sfx/parry.mp3
Normal file
Binary file not shown.
@@ -52,6 +52,10 @@ class AsciiCanvasPainter extends CustomPainter {
|
|||||||
this.objectColor = AsciiColors.object,
|
this.objectColor = AsciiColors.object,
|
||||||
this.positiveColor = AsciiColors.positive,
|
this.positiveColor = AsciiColors.positive,
|
||||||
this.negativeColor = AsciiColors.negative,
|
this.negativeColor = AsciiColors.negative,
|
||||||
|
this.rarityUncommonColor = AsciiColors.rarityUncommon,
|
||||||
|
this.rarityRareColor = AsciiColors.rarityRare,
|
||||||
|
this.rarityEpicColor = AsciiColors.rarityEpic,
|
||||||
|
this.rarityLegendaryColor = AsciiColors.rarityLegendary,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 렌더링할 레이어 목록 (z-order 정렬 필요)
|
/// 렌더링할 레이어 목록 (z-order 정렬 필요)
|
||||||
@@ -81,6 +85,22 @@ class AsciiCanvasPainter extends CustomPainter {
|
|||||||
/// 네거티브 이펙트 색상 (테마 인식)
|
/// 네거티브 이펙트 색상 (테마 인식)
|
||||||
final Color negativeColor;
|
final Color negativeColor;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// 무기 등급(ItemRarity) 색상 (Phase 9)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Uncommon 등급 색상 (초록)
|
||||||
|
final Color rarityUncommonColor;
|
||||||
|
|
||||||
|
/// Rare 등급 색상 (파랑)
|
||||||
|
final Color rarityRareColor;
|
||||||
|
|
||||||
|
/// Epic 등급 색상 (보라)
|
||||||
|
final Color rarityEpicColor;
|
||||||
|
|
||||||
|
/// Legendary 등급 색상 (금색)
|
||||||
|
final Color rarityLegendaryColor;
|
||||||
|
|
||||||
/// Paragraph 캐시 (문자+색상+크기 조합별)
|
/// Paragraph 캐시 (문자+색상+크기 조합별)
|
||||||
static final Map<_ParagraphCacheKey, ui.Paragraph> _paragraphCache = {};
|
static final Map<_ParagraphCacheKey, ui.Paragraph> _paragraphCache = {};
|
||||||
|
|
||||||
@@ -209,6 +229,11 @@ class AsciiCanvasPainter extends CustomPainter {
|
|||||||
AsciiCellColor.object => objectColor,
|
AsciiCellColor.object => objectColor,
|
||||||
AsciiCellColor.positive => positiveColor,
|
AsciiCellColor.positive => positiveColor,
|
||||||
AsciiCellColor.negative => negativeColor,
|
AsciiCellColor.negative => negativeColor,
|
||||||
|
// 무기 등급 색상 (Phase 9)
|
||||||
|
AsciiCellColor.rarityUncommon => rarityUncommonColor,
|
||||||
|
AsciiCellColor.rarityRare => rarityRareColor,
|
||||||
|
AsciiCellColor.rarityEpic => rarityEpicColor,
|
||||||
|
AsciiCellColor.rarityLegendary => rarityLegendaryColor,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ class AsciiCanvasWidget extends StatelessWidget {
|
|||||||
final posColor = AsciiColors.positiveOf(context);
|
final posColor = AsciiColors.positiveOf(context);
|
||||||
final negColor = AsciiColors.negativeOf(context);
|
final negColor = AsciiColors.negativeOf(context);
|
||||||
|
|
||||||
|
// 무기 등급 색상 (Phase 9)
|
||||||
|
final uncommonColor = AsciiColors.rarityUncommonOf(context);
|
||||||
|
final rareColor = AsciiColors.rarityRareOf(context);
|
||||||
|
final epicColor = AsciiColors.rarityEpicOf(context);
|
||||||
|
final legendaryColor = AsciiColors.rarityLegendaryOf(context);
|
||||||
|
|
||||||
return RepaintBoundary(
|
return RepaintBoundary(
|
||||||
child: CustomPaint(
|
child: CustomPaint(
|
||||||
painter: AsciiCanvasPainter(
|
painter: AsciiCanvasPainter(
|
||||||
@@ -58,6 +64,10 @@ class AsciiCanvasWidget extends StatelessWidget {
|
|||||||
objectColor: objColor,
|
objectColor: objColor,
|
||||||
positiveColor: posColor,
|
positiveColor: posColor,
|
||||||
negativeColor: negColor,
|
negativeColor: negColor,
|
||||||
|
rarityUncommonColor: uncommonColor,
|
||||||
|
rarityRareColor: rareColor,
|
||||||
|
rarityEpicColor: epicColor,
|
||||||
|
rarityLegendaryColor: legendaryColor,
|
||||||
),
|
),
|
||||||
size: Size.infinite,
|
size: Size.infinite,
|
||||||
isComplex: true,
|
isComplex: true,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/// ASCII 셀 색상 (4색 팔레트)
|
/// ASCII 셀 색상 (4색 팔레트 + 무기 등급 색상)
|
||||||
enum AsciiCellColor {
|
enum AsciiCellColor {
|
||||||
/// 배경색 (검정)
|
/// 배경색 (검정)
|
||||||
background,
|
background,
|
||||||
@@ -6,11 +6,27 @@ enum AsciiCellColor {
|
|||||||
/// 오브젝트 (흰색) - 캐릭터, 몬스터, 지형
|
/// 오브젝트 (흰색) - 캐릭터, 몬스터, 지형
|
||||||
object,
|
object,
|
||||||
|
|
||||||
/// 포지티브 이펙트 (시안) - !, +, =, >, <
|
/// 포지티브 이펙트 (시안) - !, +, =, >, < / common 무기
|
||||||
positive,
|
positive,
|
||||||
|
|
||||||
/// 네거티브 이펙트 (마젠타) - *, ~
|
/// 네거티브 이펙트 (마젠타) - *, ~
|
||||||
negative,
|
negative,
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// 무기 등급(ItemRarity) 색상 (Phase 9)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Uncommon 등급 (초록)
|
||||||
|
rarityUncommon,
|
||||||
|
|
||||||
|
/// Rare 등급 (파랑)
|
||||||
|
rarityRare,
|
||||||
|
|
||||||
|
/// Epic 등급 (보라)
|
||||||
|
rarityEpic,
|
||||||
|
|
||||||
|
/// Legendary 등급 (금색)
|
||||||
|
rarityLegendary,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 단일 ASCII 셀 데이터
|
/// 단일 ASCII 셀 데이터
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:asciineverdie/src/core/animation/monster_size.dart';
|
|||||||
import 'package:asciineverdie/src/core/animation/race_character_frames.dart';
|
import 'package:asciineverdie/src/core/animation/race_character_frames.dart';
|
||||||
import 'package:asciineverdie/src/core/animation/weapon_category.dart';
|
import 'package:asciineverdie/src/core/animation/weapon_category.dart';
|
||||||
import 'package:asciineverdie/src/core/animation/weapon_effects.dart';
|
import 'package:asciineverdie/src/core/animation/weapon_effects.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
||||||
|
|
||||||
/// Canvas용 전투 프레임 합성기
|
/// Canvas용 전투 프레임 합성기
|
||||||
///
|
///
|
||||||
@@ -20,6 +21,7 @@ class CanvasBattleComposer {
|
|||||||
required this.monsterCategory,
|
required this.monsterCategory,
|
||||||
required this.monsterSize,
|
required this.monsterSize,
|
||||||
this.raceId,
|
this.raceId,
|
||||||
|
this.weaponRarity,
|
||||||
});
|
});
|
||||||
|
|
||||||
final WeaponCategory weaponCategory;
|
final WeaponCategory weaponCategory;
|
||||||
@@ -30,6 +32,9 @@ class CanvasBattleComposer {
|
|||||||
/// 종족 ID (Phase 4: 종족별 캐릭터 애니메이션)
|
/// 종족 ID (Phase 4: 종족별 캐릭터 애니메이션)
|
||||||
final String? raceId;
|
final String? raceId;
|
||||||
|
|
||||||
|
/// 무기 희귀도 (Phase 9: 무기 등급별 이펙트 색상)
|
||||||
|
final ItemRarity? weaponRarity;
|
||||||
|
|
||||||
/// 프레임 상수
|
/// 프레임 상수
|
||||||
static const int frameWidth = 60;
|
static const int frameWidth = 60;
|
||||||
static const int frameHeight = 8;
|
static const int frameHeight = 8;
|
||||||
@@ -43,6 +48,13 @@ class CanvasBattleComposer {
|
|||||||
EnvironmentType environment,
|
EnvironmentType environment,
|
||||||
int globalTick, {
|
int globalTick, {
|
||||||
AttackerType attacker = AttackerType.none,
|
AttackerType attacker = AttackerType.none,
|
||||||
|
bool isCritical = false,
|
||||||
|
bool isEvade = false,
|
||||||
|
bool isMiss = false,
|
||||||
|
bool isDebuff = false,
|
||||||
|
bool isDot = false,
|
||||||
|
bool isBlock = false,
|
||||||
|
bool isParry = false,
|
||||||
}) {
|
}) {
|
||||||
final layers = <AsciiLayer>[
|
final layers = <AsciiLayer>[
|
||||||
_createBackgroundLayer(environment, globalTick),
|
_createBackgroundLayer(environment, globalTick),
|
||||||
@@ -61,6 +73,36 @@ class CanvasBattleComposer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 텍스트 이펙트 레이어 (Phase 10~11)
|
||||||
|
// 크리티컬 텍스트 (상단 중앙)
|
||||||
|
if (isCritical && (phase == BattlePhase.attack || phase == BattlePhase.hit)) {
|
||||||
|
layers.add(_createCriticalTextLayer(subFrame));
|
||||||
|
}
|
||||||
|
// 회피 텍스트 (캐릭터 위)
|
||||||
|
if (isEvade) {
|
||||||
|
layers.add(_createEvadeTextLayer(subFrame));
|
||||||
|
}
|
||||||
|
// 미스 텍스트 (몬스터 위)
|
||||||
|
if (isMiss) {
|
||||||
|
layers.add(_createMissTextLayer(subFrame));
|
||||||
|
}
|
||||||
|
// 디버프 텍스트 (몬스터 위)
|
||||||
|
if (isDebuff) {
|
||||||
|
layers.add(_createDebuffTextLayer(subFrame));
|
||||||
|
}
|
||||||
|
// DOT 텍스트 (몬스터 위)
|
||||||
|
if (isDot) {
|
||||||
|
layers.add(_createDotTextLayer(subFrame));
|
||||||
|
}
|
||||||
|
// 블록 텍스트 (캐릭터 위)
|
||||||
|
if (isBlock) {
|
||||||
|
layers.add(_createBlockTextLayer(subFrame));
|
||||||
|
}
|
||||||
|
// 패리 텍스트 (캐릭터 위)
|
||||||
|
if (isParry) {
|
||||||
|
layers.add(_createParryTextLayer(subFrame));
|
||||||
|
}
|
||||||
|
|
||||||
// z-order 정렬
|
// z-order 정렬
|
||||||
layers.sort((a, b) => a.zIndex.compareTo(b.zIndex));
|
layers.sort((a, b) => a.zIndex.compareTo(b.zIndex));
|
||||||
|
|
||||||
@@ -262,7 +304,15 @@ class CanvasBattleComposer {
|
|||||||
|
|
||||||
if (effectLines.isEmpty) return null;
|
if (effectLines.isEmpty) return null;
|
||||||
|
|
||||||
final cells = _spriteToCells(effectLines);
|
// Phase 9: 플레이어 공격 시 무기 등급 색상 적용
|
||||||
|
final List<List<AsciiCell>> cells;
|
||||||
|
if (attacker == AttackerType.player && weaponRarity != null) {
|
||||||
|
// 무기 등급에 따른 이펙트 색상
|
||||||
|
cells = _spriteToCellsWithColor(effectLines, weaponRarity!.effectCellColor);
|
||||||
|
} else {
|
||||||
|
// 기본 색상 (자동 색상 결정)
|
||||||
|
cells = _spriteToCells(effectLines);
|
||||||
|
}
|
||||||
|
|
||||||
// 이펙트 높이에 따른 동적 Y 위치 (캔버스 하단 기준)
|
// 이펙트 높이에 따른 동적 Y 위치 (캔버스 하단 기준)
|
||||||
final effectHeight = effectLines.length;
|
final effectHeight = effectLines.length;
|
||||||
@@ -276,6 +326,119 @@ class CanvasBattleComposer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 크리티컬 텍스트 레이어 생성 (z=4, 최상위)
|
||||||
|
///
|
||||||
|
/// 크리티컬 히트 시 "*CRIT!*" 텍스트를 화면 상단 중앙에 표시.
|
||||||
|
/// 노란색(positive) 색상으로 강조.
|
||||||
|
AsciiLayer _createCriticalTextLayer(int subFrame) {
|
||||||
|
// 프레임별 텍스트 애니메이션 (반짝임 효과)
|
||||||
|
final textLines = subFrame % 2 == 0
|
||||||
|
? _critTextFrames[0]
|
||||||
|
: _critTextFrames[1];
|
||||||
|
|
||||||
|
final cells = textLines.map((String line) {
|
||||||
|
return line.split('').map((String char) {
|
||||||
|
if (char == ' ') return AsciiCell.empty;
|
||||||
|
return AsciiCell(char: char, color: AsciiCellColor.positive);
|
||||||
|
}).toList();
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// 화면 상단 중앙 배치 (Y=0~1, X=중앙)
|
||||||
|
final textWidth = textLines.isNotEmpty ? textLines[0].length : 0;
|
||||||
|
final offsetX = (frameWidth - textWidth) ~/ 2;
|
||||||
|
|
||||||
|
return AsciiLayer(
|
||||||
|
cells: cells,
|
||||||
|
zIndex: 4, // 이펙트(z=3) 위에 표시
|
||||||
|
offsetX: offsetX,
|
||||||
|
offsetY: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 회피 텍스트 레이어 생성 (캐릭터 위, positive)
|
||||||
|
AsciiLayer _createEvadeTextLayer(int subFrame) {
|
||||||
|
return _createTextLayer(
|
||||||
|
frames: _evadeTextFrames,
|
||||||
|
subFrame: subFrame,
|
||||||
|
color: AsciiCellColor.positive,
|
||||||
|
offsetX: 15, // 캐릭터 위
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 미스 텍스트 레이어 생성 (몬스터 위, negative)
|
||||||
|
AsciiLayer _createMissTextLayer(int subFrame) {
|
||||||
|
return _createTextLayer(
|
||||||
|
frames: _missTextFrames,
|
||||||
|
subFrame: subFrame,
|
||||||
|
color: AsciiCellColor.negative,
|
||||||
|
offsetX: 35, // 몬스터 위
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 디버프 텍스트 레이어 생성 (몬스터 위, negative)
|
||||||
|
AsciiLayer _createDebuffTextLayer(int subFrame) {
|
||||||
|
return _createTextLayer(
|
||||||
|
frames: _debuffTextFrames,
|
||||||
|
subFrame: subFrame,
|
||||||
|
color: AsciiCellColor.negative,
|
||||||
|
offsetX: 35, // 몬스터 위
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DOT 텍스트 레이어 생성 (몬스터 위, negative)
|
||||||
|
AsciiLayer _createDotTextLayer(int subFrame) {
|
||||||
|
return _createTextLayer(
|
||||||
|
frames: _dotTextFrames,
|
||||||
|
subFrame: subFrame,
|
||||||
|
color: AsciiCellColor.negative,
|
||||||
|
offsetX: 35, // 몬스터 위
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 블록 텍스트 레이어 생성 (캐릭터 위, positive)
|
||||||
|
AsciiLayer _createBlockTextLayer(int subFrame) {
|
||||||
|
return _createTextLayer(
|
||||||
|
frames: _blockTextFrames,
|
||||||
|
subFrame: subFrame,
|
||||||
|
color: AsciiCellColor.positive,
|
||||||
|
offsetX: 15, // 캐릭터 위
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 패리 텍스트 레이어 생성 (캐릭터 위, positive)
|
||||||
|
AsciiLayer _createParryTextLayer(int subFrame) {
|
||||||
|
return _createTextLayer(
|
||||||
|
frames: _parryTextFrames,
|
||||||
|
subFrame: subFrame,
|
||||||
|
color: AsciiCellColor.positive,
|
||||||
|
offsetX: 15, // 캐릭터 위
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 공통 텍스트 레이어 생성 헬퍼
|
||||||
|
AsciiLayer _createTextLayer({
|
||||||
|
required List<List<String>> frames,
|
||||||
|
required int subFrame,
|
||||||
|
required AsciiCellColor color,
|
||||||
|
required int offsetX,
|
||||||
|
}) {
|
||||||
|
final textLines = frames[subFrame % 2];
|
||||||
|
|
||||||
|
final cells = textLines.map((String line) {
|
||||||
|
return line.split('').map((String char) {
|
||||||
|
if (char == ' ') return AsciiCell.empty;
|
||||||
|
return AsciiCell(char: char, color: color);
|
||||||
|
}).toList();
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return AsciiLayer(
|
||||||
|
cells: cells,
|
||||||
|
zIndex: 4,
|
||||||
|
offsetX: offsetX,
|
||||||
|
offsetY: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 몬스터 공격 이펙트 (← 방향)
|
/// 몬스터 공격 이펙트 (← 방향)
|
||||||
List<String> _getMonsterAttackEffect(BattlePhase phase, int subFrame) {
|
List<String> _getMonsterAttackEffect(BattlePhase phase, int subFrame) {
|
||||||
return switch (phase) {
|
return switch (phase) {
|
||||||
@@ -293,6 +456,21 @@ class CanvasBattleComposer {
|
|||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 문자열 스프라이트를 지정된 색상으로 AsciiCell 2D 배열로 변환 (Phase 9)
|
||||||
|
///
|
||||||
|
/// 이펙트 문자(공백 아닌 문자)에 지정 색상 적용
|
||||||
|
List<List<AsciiCell>> _spriteToCellsWithColor(
|
||||||
|
List<String> lines,
|
||||||
|
AsciiCellColor effectColor,
|
||||||
|
) {
|
||||||
|
return lines.map((line) {
|
||||||
|
return line.split('').map((char) {
|
||||||
|
if (char == ' ' || char.isEmpty) return AsciiCell.empty;
|
||||||
|
return AsciiCell(char: char, color: effectColor);
|
||||||
|
}).toList();
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
/// 문자열 스프라이트를 오른쪽 정렬된 AsciiCell 2D 배열로 변환
|
/// 문자열 스프라이트를 오른쪽 정렬된 AsciiCell 2D 배열로 변환
|
||||||
List<List<AsciiCell>> _spriteToRightAlignedCells(
|
List<List<AsciiCell>> _spriteToRightAlignedCells(
|
||||||
List<String> lines,
|
List<String> lines,
|
||||||
@@ -1129,3 +1307,53 @@ const _monsterHitFrames = <List<String>>[
|
|||||||
[r' *SLASH!* ', r' **** ', r' <----- ', r' **** ', r' '],
|
[r' *SLASH!* ', r' **** ', r' <----- ', r' **** ', r' '],
|
||||||
[r'*ATTACK!* ', r' **** ', r' <---- ', r' **** ', r' '],
|
[r'*ATTACK!* ', r' **** ', r' <---- ', r' **** ', r' '],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 크리티컬 텍스트 프레임 (2줄, Phase 10)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 크리티컬 히트 텍스트 프레임 (반짝임 애니메이션)
|
||||||
|
const _critTextFrames = <List<String>>[
|
||||||
|
[r'*CRITICAL!*', r' ========='],
|
||||||
|
[r'=CRITICAL!=', r' *********'],
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 전투 텍스트 이펙트 프레임 (Phase 11)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 회피 텍스트 프레임 (플레이어 회피 성공)
|
||||||
|
const _evadeTextFrames = <List<String>>[
|
||||||
|
[r'*EVADE!*', r'========'],
|
||||||
|
[r'=EVADE!=', r'********'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 미스 텍스트 프레임 (플레이어 공격 빗나감)
|
||||||
|
const _missTextFrames = <List<String>>[
|
||||||
|
[r'*MISS!*', r'======='],
|
||||||
|
[r'=MISS!=', r'*******'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 디버프 텍스트 프레임 (적에게 디버프 적용)
|
||||||
|
const _debuffTextFrames = <List<String>>[
|
||||||
|
[r'*DEBUFF!*', r'========='],
|
||||||
|
[r'=DEBUFF!=', r'*********'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/// DOT 텍스트 프레임 (지속 피해)
|
||||||
|
const _dotTextFrames = <List<String>>[
|
||||||
|
[r'*DOT!*', r'======'],
|
||||||
|
[r'=DOT!=', r'******'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 블록 텍스트 프레임 (방패 방어)
|
||||||
|
const _blockTextFrames = <List<String>>[
|
||||||
|
[r'*BLOCK!*', r'========'],
|
||||||
|
[r'=BLOCK!=', r'********'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 패리 텍스트 프레임 (무기 쳐내기)
|
||||||
|
const _parryTextFrames = <List<String>>[
|
||||||
|
[r'*PARRY!*', r'========'],
|
||||||
|
[r'=PARRY!=', r'********'],
|
||||||
|
];
|
||||||
|
|||||||
@@ -323,6 +323,15 @@ enum SfxType {
|
|||||||
|
|
||||||
/// 퀘스트 완료
|
/// 퀘스트 완료
|
||||||
questComplete,
|
questComplete,
|
||||||
|
|
||||||
|
/// 회피 (Phase 11)
|
||||||
|
evade,
|
||||||
|
|
||||||
|
/// 방패 방어 (Phase 11)
|
||||||
|
block,
|
||||||
|
|
||||||
|
/// 무기 쳐내기 (Phase 11)
|
||||||
|
parry,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// BgmType을 파일명으로 변환
|
/// BgmType을 파일명으로 변환
|
||||||
@@ -340,5 +349,8 @@ extension SfxTypeExtension on SfxType {
|
|||||||
SfxType.click => 'click',
|
SfxType.click => 'click',
|
||||||
SfxType.levelUp => 'level_up',
|
SfxType.levelUp => 'level_up',
|
||||||
SfxType.questComplete => 'quest_complete',
|
SfxType.questComplete => 'quest_complete',
|
||||||
|
SfxType.evade => 'evade',
|
||||||
|
SfxType.block => 'block',
|
||||||
|
SfxType.parry => 'parry',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,26 @@ class AsciiColors {
|
|||||||
static Color backgroundOf(BuildContext context) =>
|
static Color backgroundOf(BuildContext context) =>
|
||||||
RetroColors.isDarkMode(context) ? background : _lightBackground;
|
RetroColors.isDarkMode(context) ? background : _lightBackground;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// 무기 등급(ItemRarity) 색상 Getter (테마 인식, Phase 9)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Uncommon 등급 색상 - 테마 인식
|
||||||
|
static Color rarityUncommonOf(BuildContext context) =>
|
||||||
|
RetroColors.isDarkMode(context) ? rarityUncommon : _lightRarityUncommon;
|
||||||
|
|
||||||
|
/// Rare 등급 색상 - 테마 인식
|
||||||
|
static Color rarityRareOf(BuildContext context) =>
|
||||||
|
RetroColors.isDarkMode(context) ? rarityRare : _lightRarityRare;
|
||||||
|
|
||||||
|
/// Epic 등급 색상 - 테마 인식
|
||||||
|
static Color rarityEpicOf(BuildContext context) =>
|
||||||
|
RetroColors.isDarkMode(context) ? rarityEpic : _lightRarityEpic;
|
||||||
|
|
||||||
|
/// Legendary 등급 색상 - 테마 인식
|
||||||
|
static Color rarityLegendaryOf(BuildContext context) =>
|
||||||
|
RetroColors.isDarkMode(context) ? rarityLegendary : _lightRarityLegendary;
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// 라이트 모드 색상 (양피지/크림 기반)
|
// 라이트 모드 색상 (양피지/크림 기반)
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
@@ -48,6 +68,18 @@ class AsciiColors {
|
|||||||
/// 라이트 모드 배경 (양피지 크림)
|
/// 라이트 모드 배경 (양피지 크림)
|
||||||
static const Color _lightBackground = Color(0xFFF5E6C8);
|
static const Color _lightBackground = Color(0xFFF5E6C8);
|
||||||
|
|
||||||
|
/// 라이트 모드 Uncommon 등급 (진한 초록)
|
||||||
|
static const Color _lightRarityUncommon = Color(0xFF008800);
|
||||||
|
|
||||||
|
/// 라이트 모드 Rare 등급 (진한 파랑)
|
||||||
|
static const Color _lightRarityRare = Color(0xFF0055AA);
|
||||||
|
|
||||||
|
/// 라이트 모드 Epic 등급 (진한 보라)
|
||||||
|
static const Color _lightRarityEpic = Color(0xFF660099);
|
||||||
|
|
||||||
|
/// 라이트 모드 Legendary 등급 (진한 금색)
|
||||||
|
static const Color _lightRarityLegendary = Color(0xFFCC7700);
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// 레거시 정적 색상 (다크 모드 기본값 / context 없는 곳에서 사용)
|
// 레거시 정적 색상 (다크 모드 기본값 / context 없는 곳에서 사용)
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
@@ -64,6 +96,22 @@ class AsciiColors {
|
|||||||
/// 배경 색상
|
/// 배경 색상
|
||||||
static const Color background = Colors.black;
|
static const Color background = Colors.black;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// 무기 등급(ItemRarity) 정적 색상 (다크 모드 기본값, Phase 9)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Uncommon 등급 (밝은 초록)
|
||||||
|
static const Color rarityUncommon = Color(0xFF00FF00);
|
||||||
|
|
||||||
|
/// Rare 등급 (밝은 파랑)
|
||||||
|
static const Color rarityRare = Color(0xFF0088FF);
|
||||||
|
|
||||||
|
/// Epic 등급 (밝은 보라)
|
||||||
|
static const Color rarityEpic = Color(0xFF9900FF);
|
||||||
|
|
||||||
|
/// Legendary 등급 (밝은 금색)
|
||||||
|
static const Color rarityLegendary = Color(0xFFFFAA00);
|
||||||
|
|
||||||
/// 상황에 따른 색상 반환
|
/// 상황에 따른 색상 반환
|
||||||
static Color forContext(AsciiColorContext context) {
|
static Color forContext(AsciiColorContext context) {
|
||||||
return switch (context) {
|
return switch (context) {
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:asciineverdie/data/potion_data.dart';
|
import 'package:asciineverdie/data/potion_data.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||||
import 'package:asciineverdie/src/core/model/potion.dart';
|
import 'package:asciineverdie/src/core/model/potion.dart';
|
||||||
|
|
||||||
/// 물약 서비스
|
/// 물약 서비스
|
||||||
@@ -357,19 +360,35 @@ class PotionService {
|
|||||||
///
|
///
|
||||||
/// 전투 승리 시 물약 드랍 여부 결정 및 물약 획득
|
/// 전투 승리 시 물약 드랍 여부 결정 및 물약 획득
|
||||||
/// [playerLevel] 플레이어 레벨 (드랍 확률 및 티어 결정)
|
/// [playerLevel] 플레이어 레벨 (드랍 확률 및 티어 결정)
|
||||||
|
/// [monsterLevel] 몬스터 레벨 (티어 결정에 영향)
|
||||||
|
/// [monsterGrade] 몬스터 등급 (드랍 확률 보너스)
|
||||||
/// [inventory] 현재 물약 인벤토리
|
/// [inventory] 현재 물약 인벤토리
|
||||||
/// [roll] 0~99 범위의 난수 (드랍 확률 판정)
|
/// [roll] 0~99 범위의 난수 (드랍 확률 판정)
|
||||||
/// [typeRoll] 0~99 범위의 난수 (HP/MP 결정)
|
/// [typeRoll] 0~99 범위의 난수 (HP/MP 결정)
|
||||||
/// Returns: (업데이트된 인벤토리, 드랍된 물약 또는 null)
|
/// Returns: (업데이트된 인벤토리, 드랍된 물약 또는 null)
|
||||||
(PotionInventory, Potion?) tryPotionDrop({
|
(PotionInventory, Potion?) tryPotionDrop({
|
||||||
required int playerLevel,
|
required int playerLevel,
|
||||||
|
required int monsterLevel,
|
||||||
|
required MonsterGrade monsterGrade,
|
||||||
required PotionInventory inventory,
|
required PotionInventory inventory,
|
||||||
required int roll,
|
required int roll,
|
||||||
required int typeRoll,
|
required int typeRoll,
|
||||||
}) {
|
}) {
|
||||||
// 드랍 확률 계산
|
// 기본 드랍 확률 계산
|
||||||
final dropChance = (baseDropChance + playerLevel * dropChancePerLevel)
|
var dropChance = (baseDropChance + playerLevel * dropChancePerLevel)
|
||||||
.clamp(baseDropChance, maxDropChance);
|
.clamp(baseDropChance, maxDropChance);
|
||||||
|
|
||||||
|
// 몬스터 등급 보너스 (Elite +5%, Boss +15%)
|
||||||
|
dropChance += monsterGrade.potionDropBonus;
|
||||||
|
|
||||||
|
// 몬스터 레벨 > 플레이어 레벨이면 추가 확률 (+1%/레벨 차이)
|
||||||
|
if (monsterLevel > playerLevel) {
|
||||||
|
dropChance += (monsterLevel - playerLevel) * 0.01;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 최대 확률 제한 (50%)
|
||||||
|
dropChance = dropChance.clamp(0.0, 0.5);
|
||||||
|
|
||||||
final dropThreshold = (dropChance * 100).round();
|
final dropThreshold = (dropChance * 100).round();
|
||||||
|
|
||||||
// 드랍 실패
|
// 드랍 실패
|
||||||
@@ -380,8 +399,9 @@ class PotionService {
|
|||||||
// 물약 타입 결정 (60% HP, 40% MP)
|
// 물약 타입 결정 (60% HP, 40% MP)
|
||||||
final isHpPotion = typeRoll < 60;
|
final isHpPotion = typeRoll < 60;
|
||||||
|
|
||||||
// 레벨 기반 티어 결정
|
// 티어 결정: max(플레이어 레벨, 몬스터 레벨) 기반
|
||||||
final tier = PotionData.tierForLevel(playerLevel);
|
final effectiveLevel = math.max(playerLevel, monsterLevel);
|
||||||
|
final tier = PotionData.tierForLevel(effectiveLevel);
|
||||||
|
|
||||||
// 물약 선택
|
// 물약 선택
|
||||||
final Potion? potion;
|
final Potion? potion;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ 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/game_state.dart';
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
import 'package:asciineverdie/src/core/model/monster_combat_stats.dart';
|
import 'package:asciineverdie/src/core/model/monster_combat_stats.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||||
import 'package:asciineverdie/src/core/model/potion.dart';
|
import 'package:asciineverdie/src/core/model/potion.dart';
|
||||||
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
||||||
import 'package:asciineverdie/src/core/model/skill.dart';
|
import 'package:asciineverdie/src/core/model/skill.dart';
|
||||||
@@ -591,6 +592,7 @@ class ProgressService {
|
|||||||
monsterBaseName: monsterResult.baseName,
|
monsterBaseName: monsterResult.baseName,
|
||||||
monsterPart: monsterResult.part,
|
monsterPart: monsterResult.part,
|
||||||
monsterLevel: monsterResult.level,
|
monsterLevel: monsterResult.level,
|
||||||
|
monsterGrade: monsterResult.grade,
|
||||||
),
|
),
|
||||||
currentCombat: combatState,
|
currentCombat: combatState,
|
||||||
);
|
);
|
||||||
@@ -646,6 +648,7 @@ class ProgressService {
|
|||||||
monsterBaseName: 'Glitch God',
|
monsterBaseName: 'Glitch God',
|
||||||
monsterPart: '*', // 특수 전리품
|
monsterPart: '*', // 특수 전리품
|
||||||
monsterLevel: glitchGod.level,
|
monsterLevel: glitchGod.level,
|
||||||
|
monsterGrade: MonsterGrade.boss, // 최종 보스는 항상 boss 등급
|
||||||
),
|
),
|
||||||
currentCombat: combatState,
|
currentCombat: combatState,
|
||||||
);
|
);
|
||||||
@@ -963,8 +966,12 @@ class ProgressService {
|
|||||||
// 물약 드랍 시도
|
// 물약 드랍 시도
|
||||||
final potionService = const PotionService();
|
final potionService = const PotionService();
|
||||||
final rng = resultState.rng;
|
final rng = resultState.rng;
|
||||||
|
final monsterLevel = taskInfo.monsterLevel ?? resultState.traits.level;
|
||||||
|
final monsterGrade = taskInfo.monsterGrade ?? MonsterGrade.normal;
|
||||||
final (updatedPotionInventory, droppedPotion) = potionService.tryPotionDrop(
|
final (updatedPotionInventory, droppedPotion) = potionService.tryPotionDrop(
|
||||||
playerLevel: resultState.traits.level,
|
playerLevel: resultState.traits.level,
|
||||||
|
monsterLevel: monsterLevel,
|
||||||
|
monsterGrade: monsterGrade,
|
||||||
inventory: resultState.potionInventory,
|
inventory: resultState.potionInventory,
|
||||||
roll: rng.nextInt(100),
|
roll: rng.nextInt(100),
|
||||||
typeRoll: rng.nextInt(100),
|
typeRoll: rng.nextInt(100),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:asciineverdie/src/core/model/combat_state.dart';
|
|||||||
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/item_stats.dart';
|
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||||
import 'package:asciineverdie/src/core/model/potion.dart';
|
import 'package:asciineverdie/src/core/model/potion.dart';
|
||||||
import 'package:asciineverdie/src/core/model/skill.dart';
|
import 'package:asciineverdie/src/core/model/skill.dart';
|
||||||
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||||
@@ -280,6 +281,7 @@ class TaskInfo {
|
|||||||
this.monsterBaseName,
|
this.monsterBaseName,
|
||||||
this.monsterPart,
|
this.monsterPart,
|
||||||
this.monsterLevel,
|
this.monsterLevel,
|
||||||
|
this.monsterGrade,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String caption;
|
final String caption;
|
||||||
@@ -294,6 +296,9 @@ class TaskInfo {
|
|||||||
/// 킬 태스크의 몬스터 레벨 (애니메이션 크기 결정용)
|
/// 킬 태스크의 몬스터 레벨 (애니메이션 크기 결정용)
|
||||||
final int? monsterLevel;
|
final int? monsterLevel;
|
||||||
|
|
||||||
|
/// 킬 태스크의 몬스터 등급 (Normal/Elite/Boss)
|
||||||
|
final MonsterGrade? monsterGrade;
|
||||||
|
|
||||||
factory TaskInfo.empty() =>
|
factory TaskInfo.empty() =>
|
||||||
const TaskInfo(caption: '', type: TaskType.neutral);
|
const TaskInfo(caption: '', type: TaskType.neutral);
|
||||||
|
|
||||||
@@ -303,6 +308,7 @@ class TaskInfo {
|
|||||||
String? monsterBaseName,
|
String? monsterBaseName,
|
||||||
String? monsterPart,
|
String? monsterPart,
|
||||||
int? monsterLevel,
|
int? monsterLevel,
|
||||||
|
MonsterGrade? monsterGrade,
|
||||||
}) {
|
}) {
|
||||||
return TaskInfo(
|
return TaskInfo(
|
||||||
caption: caption ?? this.caption,
|
caption: caption ?? this.caption,
|
||||||
@@ -310,6 +316,7 @@ class TaskInfo {
|
|||||||
monsterBaseName: monsterBaseName ?? this.monsterBaseName,
|
monsterBaseName: monsterBaseName ?? this.monsterBaseName,
|
||||||
monsterPart: monsterPart ?? this.monsterPart,
|
monsterPart: monsterPart ?? this.monsterPart,
|
||||||
monsterLevel: monsterLevel ?? this.monsterLevel,
|
monsterLevel: monsterLevel ?? this.monsterLevel,
|
||||||
|
monsterGrade: monsterGrade ?? this.monsterGrade,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart';
|
||||||
|
|
||||||
/// 아이템 희귀도
|
/// 아이템 희귀도
|
||||||
enum ItemRarity {
|
enum ItemRarity {
|
||||||
common,
|
common,
|
||||||
@@ -23,6 +25,17 @@ enum ItemRarity {
|
|||||||
epic => 400,
|
epic => 400,
|
||||||
legendary => 1000,
|
legendary => 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// 공격 이펙트 셀 색상 (Phase 9: 무기 등급별 이펙트)
|
||||||
|
///
|
||||||
|
/// common은 기본 positive(시안), 나머지는 등급별 고유 색상
|
||||||
|
AsciiCellColor get effectCellColor => switch (this) {
|
||||||
|
ItemRarity.common => AsciiCellColor.positive,
|
||||||
|
ItemRarity.uncommon => AsciiCellColor.rarityUncommon,
|
||||||
|
ItemRarity.rare => AsciiCellColor.rarityRare,
|
||||||
|
ItemRarity.epic => AsciiCellColor.rarityEpic,
|
||||||
|
ItemRarity.legendary => AsciiCellColor.rarityLegendary,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 아이템 스탯 보정치
|
/// 아이템 스탯 보정치
|
||||||
|
|||||||
60
lib/src/core/model/monster_grade.dart
Normal file
60
lib/src/core/model/monster_grade.dart
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// 몬스터 등급 (드랍 품질 및 UI 표시에 영향)
|
||||||
|
enum MonsterGrade {
|
||||||
|
/// 일반 몬스터 (85% 확률)
|
||||||
|
/// - 기본 드랍 확률
|
||||||
|
/// - UI: 기본 색상, 접두사 없음
|
||||||
|
normal,
|
||||||
|
|
||||||
|
/// 정예 몬스터 (12% 확률)
|
||||||
|
/// - 물약 드랍 +5%, 아이템 품질 향상
|
||||||
|
/// - UI: 파란색, ★ 접두사
|
||||||
|
elite,
|
||||||
|
|
||||||
|
/// 보스 몬스터 (3% 확률)
|
||||||
|
/// - 물약 드랍 +15%, 최고 아이템 품질
|
||||||
|
/// - UI: 금색, ★★★ 접두사
|
||||||
|
boss,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MonsterGrade 확장 메서드
|
||||||
|
extension MonsterGradeExtension on MonsterGrade {
|
||||||
|
/// 물약 드랍 확률 보너스 (0.0 ~ 1.0)
|
||||||
|
double get potionDropBonus => switch (this) {
|
||||||
|
MonsterGrade.normal => 0.0,
|
||||||
|
MonsterGrade.elite => 0.05, // +5%
|
||||||
|
MonsterGrade.boss => 0.15, // +15%
|
||||||
|
};
|
||||||
|
|
||||||
|
/// UI 표시용 접두사 (몬스터 이름 앞에 붙음)
|
||||||
|
String get displayPrefix => switch (this) {
|
||||||
|
MonsterGrade.normal => '',
|
||||||
|
MonsterGrade.elite => '★ ',
|
||||||
|
MonsterGrade.boss => '★★★ ',
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 스탯 배율 (전투력 강화)
|
||||||
|
double get statMultiplier => switch (this) {
|
||||||
|
MonsterGrade.normal => 1.0,
|
||||||
|
MonsterGrade.elite => 1.3, // +30% 스탯
|
||||||
|
MonsterGrade.boss => 1.8, // +80% 스탯
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 경험치 배율
|
||||||
|
double get expMultiplier => switch (this) {
|
||||||
|
MonsterGrade.normal => 1.0,
|
||||||
|
MonsterGrade.elite => 1.5, // +50% 경험치
|
||||||
|
MonsterGrade.boss => 2.5, // +150% 경험치
|
||||||
|
};
|
||||||
|
|
||||||
|
/// UI 표시용 색상
|
||||||
|
/// - Normal: 기본 텍스트 색상 (null 반환 → 기본 스타일 사용)
|
||||||
|
/// - Elite: 파란색 (#7AA2F7)
|
||||||
|
/// - Boss: 금색 (#E0AF68)
|
||||||
|
Color? get displayColor => switch (this) {
|
||||||
|
MonsterGrade.normal => null,
|
||||||
|
MonsterGrade.elite => const Color(0xFF7AA2F7), // MP 파랑
|
||||||
|
MonsterGrade.boss => const Color(0xFFE0AF68), // 골드
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'dart:math' as math;
|
|||||||
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
||||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||||
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
||||||
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||||
import 'package:asciineverdie/src/core/util/roman.dart';
|
import 'package:asciineverdie/src/core/util/roman.dart';
|
||||||
@@ -483,6 +484,25 @@ Stats winStat(Stats stats, DeterministicRandom rng) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 몬스터 등급 결정 (Normal 85%, Elite 12%, Boss 3%)
|
||||||
|
/// 몬스터 레벨이 플레이어 레벨보다 높으면 상위 등급 확률 증가
|
||||||
|
MonsterGrade _determineGrade(
|
||||||
|
int monsterLevel,
|
||||||
|
int playerLevel,
|
||||||
|
DeterministicRandom rng,
|
||||||
|
) {
|
||||||
|
// 기본 확률: Normal 85%, Elite 12%, Boss 3%
|
||||||
|
// 레벨 차이에 따른 보정
|
||||||
|
final levelDiff = monsterLevel - playerLevel;
|
||||||
|
final eliteBonus = (levelDiff * 2).clamp(0, 10); // 최대 +10%
|
||||||
|
final bossBonus = (levelDiff * 0.5).clamp(0, 3).toInt(); // 최대 +3%
|
||||||
|
|
||||||
|
final roll = rng.nextInt(100);
|
||||||
|
if (roll < 3 + bossBonus) return MonsterGrade.boss;
|
||||||
|
if (roll < 15 + eliteBonus) return MonsterGrade.elite;
|
||||||
|
return MonsterGrade.normal;
|
||||||
|
}
|
||||||
|
|
||||||
MonsterTaskResult monsterTask(
|
MonsterTaskResult monsterTask(
|
||||||
PqConfig config,
|
PqConfig config,
|
||||||
DeterministicRandom rng,
|
DeterministicRandom rng,
|
||||||
@@ -592,11 +612,15 @@ MonsterTaskResult monsterTask(
|
|||||||
name = l10n.indefiniteL10n(name, qty);
|
name = l10n.indefiniteL10n(name, qty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 몬스터 등급 결정 (level = 플레이어 레벨)
|
||||||
|
final grade = _determineGrade(monsterLevel, level, rng);
|
||||||
|
|
||||||
return MonsterTaskResult(
|
return MonsterTaskResult(
|
||||||
displayName: name,
|
displayName: name,
|
||||||
baseName: baseName,
|
baseName: baseName,
|
||||||
level: monsterLevel * qty,
|
level: monsterLevel * qty,
|
||||||
part: part,
|
part: part,
|
||||||
|
grade: grade,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,6 +631,7 @@ class MonsterTaskResult {
|
|||||||
required this.baseName,
|
required this.baseName,
|
||||||
required this.level,
|
required this.level,
|
||||||
required this.part,
|
required this.part,
|
||||||
|
required this.grade,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 화면에 표시할 몬스터 이름 (형용사 포함, 예: "a sick Goblin")
|
/// 화면에 표시할 몬스터 이름 (형용사 포함, 예: "a sick Goblin")
|
||||||
@@ -620,6 +645,9 @@ class MonsterTaskResult {
|
|||||||
|
|
||||||
/// 전리품 부위 (예: "claw", "tail", "*"는 WinItem 호출)
|
/// 전리품 부위 (예: "claw", "tail", "*"는 WinItem 호출)
|
||||||
final String part;
|
final String part;
|
||||||
|
|
||||||
|
/// 몬스터 등급 (Normal/Elite/Boss)
|
||||||
|
final MonsterGrade grade;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RewardKind { spell, equip, stat, item }
|
enum RewardKind { spell, equip, stat, item }
|
||||||
|
|||||||
@@ -302,16 +302,21 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
case CombatEventType.monsterAttack:
|
case CombatEventType.monsterAttack:
|
||||||
audio.playMonsterSfx('hit');
|
audio.playMonsterSfx('hit');
|
||||||
|
|
||||||
|
// 회피/방어 SFX (Phase 11)
|
||||||
|
case CombatEventType.playerEvade:
|
||||||
|
audio.playPlayerSfx('evade');
|
||||||
|
case CombatEventType.monsterEvade:
|
||||||
|
// 몬스터 회피 = 플레이어 공격 빗나감 (evade SFX)
|
||||||
|
audio.playPlayerSfx('evade');
|
||||||
|
case CombatEventType.playerBlock:
|
||||||
|
audio.playPlayerSfx('block');
|
||||||
|
case CombatEventType.playerParry:
|
||||||
|
audio.playPlayerSfx('parry');
|
||||||
|
|
||||||
// SFX 없음
|
// SFX 없음
|
||||||
case CombatEventType.dotTick:
|
case CombatEventType.dotTick:
|
||||||
// DOT 틱은 SFX 없음 (너무 자주 발생)
|
// DOT 틱은 SFX 없음 (너무 자주 발생)
|
||||||
break;
|
break;
|
||||||
case CombatEventType.playerEvade:
|
|
||||||
case CombatEventType.monsterEvade:
|
|
||||||
case CombatEventType.playerBlock:
|
|
||||||
case CombatEventType.playerParry:
|
|
||||||
// 회피/방어는 별도 SFX 없음
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -925,6 +930,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
shieldName: state.equipment.shield,
|
shieldName: state.equipment.shield,
|
||||||
characterLevel: state.traits.level,
|
characterLevel: state.traits.level,
|
||||||
monsterLevel: state.progress.currentTask.monsterLevel,
|
monsterLevel: state.progress.currentTask.monsterLevel,
|
||||||
|
monsterGrade: state.progress.currentTask.monsterGrade,
|
||||||
latestCombatEvent:
|
latestCombatEvent:
|
||||||
state.progress.currentCombat?.recentEvents.lastOrNull,
|
state.progress.currentCombat?.recentEvents.lastOrNull,
|
||||||
raceId: state.traits.raceId,
|
raceId: state.traits.raceId,
|
||||||
|
|||||||
@@ -663,9 +663,11 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
shieldName: state.equipment.shield,
|
shieldName: state.equipment.shield,
|
||||||
characterLevel: state.traits.level,
|
characterLevel: state.traits.level,
|
||||||
monsterLevel: state.progress.currentTask.monsterLevel,
|
monsterLevel: state.progress.currentTask.monsterLevel,
|
||||||
|
monsterGrade: state.progress.currentTask.monsterGrade,
|
||||||
latestCombatEvent:
|
latestCombatEvent:
|
||||||
state.progress.currentCombat?.recentEvents.lastOrNull,
|
state.progress.currentCombat?.recentEvents.lastOrNull,
|
||||||
raceId: state.traits.raceId,
|
raceId: state.traits.raceId,
|
||||||
|
weaponRarity: state.equipment.weaponItem.rarity,
|
||||||
),
|
),
|
||||||
|
|
||||||
// 중앙: 캐로셀 (PageView)
|
// 중앙: 캐로셀 (PageView)
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import 'package:asciineverdie/src/core/animation/weapon_category.dart';
|
|||||||
import 'package:asciineverdie/src/core/constants/ascii_colors.dart';
|
import 'package:asciineverdie/src/core/constants/ascii_colors.dart';
|
||||||
import 'package:asciineverdie/src/core/model/combat_event.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/game_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||||
|
|
||||||
/// 애니메이션 모드
|
/// 애니메이션 모드
|
||||||
enum AnimationMode {
|
enum AnimationMode {
|
||||||
@@ -43,9 +45,11 @@ class AsciiAnimationCard extends StatefulWidget {
|
|||||||
this.shieldName,
|
this.shieldName,
|
||||||
this.characterLevel,
|
this.characterLevel,
|
||||||
this.monsterLevel,
|
this.monsterLevel,
|
||||||
|
this.monsterGrade,
|
||||||
this.isPaused = false,
|
this.isPaused = false,
|
||||||
this.latestCombatEvent,
|
this.latestCombatEvent,
|
||||||
this.raceId,
|
this.raceId,
|
||||||
|
this.weaponRarity,
|
||||||
});
|
});
|
||||||
|
|
||||||
final TaskType taskType;
|
final TaskType taskType;
|
||||||
@@ -73,12 +77,18 @@ class AsciiAnimationCard extends StatefulWidget {
|
|||||||
/// 몬스터 레벨 (몬스터 크기 결정용)
|
/// 몬스터 레벨 (몬스터 크기 결정용)
|
||||||
final int? monsterLevel;
|
final int? monsterLevel;
|
||||||
|
|
||||||
|
/// 몬스터 등급 (Normal/Elite/Boss) - 색상/접두사 표시용
|
||||||
|
final MonsterGrade? monsterGrade;
|
||||||
|
|
||||||
/// 최근 전투 이벤트 (애니메이션 동기화용)
|
/// 최근 전투 이벤트 (애니메이션 동기화용)
|
||||||
final CombatEvent? latestCombatEvent;
|
final CombatEvent? latestCombatEvent;
|
||||||
|
|
||||||
/// 종족 ID (Phase 4: 종족별 캐릭터 애니메이션)
|
/// 종족 ID (Phase 4: 종족별 캐릭터 애니메이션)
|
||||||
final String? raceId;
|
final String? raceId;
|
||||||
|
|
||||||
|
/// 무기 희귀도 (Phase 9: 무기 등급별 이펙트 색상)
|
||||||
|
final ItemRarity? weaponRarity;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AsciiAnimationCard> createState() => _AsciiAnimationCardState();
|
State<AsciiAnimationCard> createState() => _AsciiAnimationCardState();
|
||||||
}
|
}
|
||||||
@@ -128,6 +138,12 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
bool _showParryEffect = false;
|
bool _showParryEffect = false;
|
||||||
bool _showSkillEffect = false;
|
bool _showSkillEffect = false;
|
||||||
|
|
||||||
|
// 추가 전투 이펙트 (Phase 11)
|
||||||
|
bool _showEvadeEffect = false;
|
||||||
|
bool _showMissEffect = false;
|
||||||
|
bool _showDebuffEffect = false;
|
||||||
|
bool _showDotEffect = false;
|
||||||
|
|
||||||
// 공격 속도 기반 동적 페이즈 프레임 수 (Phase 6)
|
// 공격 속도 기반 동적 페이즈 프레임 수 (Phase 6)
|
||||||
int _eventDrivenPhaseFrames = 0;
|
int _eventDrivenPhaseFrames = 0;
|
||||||
bool _isEventDrivenPhase = false;
|
bool _isEventDrivenPhase = false;
|
||||||
@@ -191,12 +207,13 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
oldWidget.weaponName != widget.weaponName ||
|
oldWidget.weaponName != widget.weaponName ||
|
||||||
oldWidget.shieldName != widget.shieldName ||
|
oldWidget.shieldName != widget.shieldName ||
|
||||||
oldWidget.monsterLevel != widget.monsterLevel ||
|
oldWidget.monsterLevel != widget.monsterLevel ||
|
||||||
oldWidget.raceId != widget.raceId) {
|
oldWidget.raceId != widget.raceId ||
|
||||||
|
oldWidget.weaponRarity != widget.weaponRarity) {
|
||||||
_updateAnimation();
|
_updateAnimation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 전투 이벤트에 따라 애니메이션 페이즈 강제 전환 (Phase 5)
|
/// 전투 이벤트에 따라 애니메이션 페이즈 강제 전환 (Phase 5, 11)
|
||||||
void _handleCombatEvent(CombatEvent event) {
|
void _handleCombatEvent(CombatEvent event) {
|
||||||
_lastEventTimestamp = event.timestamp;
|
_lastEventTimestamp = event.timestamp;
|
||||||
|
|
||||||
@@ -204,121 +221,90 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
if (_animationMode != AnimationMode.battle) return;
|
if (_animationMode != AnimationMode.battle) return;
|
||||||
|
|
||||||
// 이벤트 타입에 따라 페이즈 및 효과 결정
|
// 이벤트 타입에 따라 페이즈 및 효과 결정
|
||||||
|
// (targetPhase, isCritical, isBlock, isParry, isSkill, isEvade, isMiss, isDebuff, isDot)
|
||||||
final (
|
final (
|
||||||
targetPhase,
|
targetPhase,
|
||||||
isCritical,
|
isCritical,
|
||||||
isBlock,
|
isBlock,
|
||||||
isParry,
|
isParry,
|
||||||
isSkill,
|
isSkill,
|
||||||
|
isEvade,
|
||||||
|
isMiss,
|
||||||
|
isDebuff,
|
||||||
|
isDot,
|
||||||
) = switch (event.type) {
|
) = switch (event.type) {
|
||||||
// 플레이어 공격 → prepare 페이즈부터 시작 (준비 동작 표시)
|
// 플레이어 공격 → prepare 페이즈부터 시작 (준비 동작 표시)
|
||||||
CombatEventType.playerAttack => (
|
CombatEventType.playerAttack => (
|
||||||
BattlePhase.prepare,
|
BattlePhase.prepare,
|
||||||
event.isCritical,
|
event.isCritical,
|
||||||
false,
|
false, false, false, false, false, false, false,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
// 스킬 사용 → prepare 페이즈부터 시작 + 스킬 이펙트
|
// 스킬 사용 → prepare 페이즈부터 시작 + 스킬 이펙트
|
||||||
CombatEventType.playerSkill => (
|
CombatEventType.playerSkill => (
|
||||||
BattlePhase.prepare,
|
BattlePhase.prepare,
|
||||||
event.isCritical,
|
event.isCritical,
|
||||||
false,
|
false, false, true, false, false, false, false,
|
||||||
false,
|
|
||||||
true,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// 몬스터 공격 → prepare 페이즈부터 시작
|
// 몬스터 공격 → prepare 페이즈부터 시작
|
||||||
CombatEventType.monsterAttack => (
|
CombatEventType.monsterAttack => (
|
||||||
BattlePhase.prepare,
|
BattlePhase.prepare,
|
||||||
false,
|
false, false, false, false, false, false, false, false,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
// 블록 → hit 페이즈 + 블록 이펙트
|
// 블록 → hit 페이즈 + 블록 이펙트 + 텍스트
|
||||||
CombatEventType.playerBlock => (
|
CombatEventType.playerBlock => (
|
||||||
BattlePhase.hit,
|
BattlePhase.hit,
|
||||||
false,
|
false, true, false, false, false, false, false, false,
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
// 패리 → hit 페이즈 + 패리 이펙트
|
// 패리 → hit 페이즈 + 패리 이펙트 + 텍스트
|
||||||
CombatEventType.playerParry => (
|
CombatEventType.playerParry => (
|
||||||
BattlePhase.hit,
|
BattlePhase.hit,
|
||||||
false,
|
false, false, true, false, false, false, false, false,
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// 회피 → recover 페이즈 (빠른 회피 동작)
|
// 플레이어 회피 → recover 페이즈 + 회피 텍스트
|
||||||
CombatEventType.playerEvade => (
|
CombatEventType.playerEvade => (
|
||||||
BattlePhase.recover,
|
BattlePhase.recover,
|
||||||
false,
|
false, false, false, false, true, false, false, false,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
|
// 몬스터 회피 → idle 페이즈 + 미스 텍스트
|
||||||
CombatEventType.monsterEvade => (
|
CombatEventType.monsterEvade => (
|
||||||
BattlePhase.idle,
|
BattlePhase.idle,
|
||||||
false,
|
false, false, false, false, false, true, false, false,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// 회복/버프 → idle 페이즈 유지
|
// 회복/버프 → idle 페이즈 유지
|
||||||
CombatEventType.playerHeal => (
|
CombatEventType.playerHeal => (
|
||||||
BattlePhase.idle,
|
BattlePhase.idle,
|
||||||
false,
|
false, false, false, false, false, false, false, false,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
CombatEventType.playerBuff => (
|
CombatEventType.playerBuff => (
|
||||||
BattlePhase.idle,
|
BattlePhase.idle,
|
||||||
false,
|
false, false, false, false, false, false, false, false,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// 디버프 적용 → idle 페이즈 유지
|
// 디버프 적용 → idle 페이즈 + 디버프 텍스트
|
||||||
CombatEventType.playerDebuff => (
|
CombatEventType.playerDebuff => (
|
||||||
BattlePhase.idle,
|
BattlePhase.idle,
|
||||||
false,
|
false, false, false, false, false, false, true, false,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// DOT 틱 → attack 페이즈 (지속 피해)
|
// DOT 틱 → attack 페이즈 + DOT 텍스트
|
||||||
CombatEventType.dotTick => (
|
CombatEventType.dotTick => (
|
||||||
BattlePhase.attack,
|
BattlePhase.attack,
|
||||||
false,
|
false, false, false, false, false, false, false, true,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// 물약 사용 → idle 페이즈 유지
|
// 물약 사용 → idle 페이즈 유지
|
||||||
CombatEventType.playerPotion => (
|
CombatEventType.playerPotion => (
|
||||||
BattlePhase.idle,
|
BattlePhase.idle,
|
||||||
false,
|
false, false, false, false, false, false, false, false,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// 물약 드랍 → idle 페이즈 유지
|
// 물약 드랍 → idle 페이즈 유지
|
||||||
CombatEventType.potionDrop => (
|
CombatEventType.potionDrop => (
|
||||||
BattlePhase.idle,
|
BattlePhase.idle,
|
||||||
false,
|
false, false, false, false, false, false, false, false,
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -330,6 +316,10 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
_showBlockEffect = isBlock;
|
_showBlockEffect = isBlock;
|
||||||
_showParryEffect = isParry;
|
_showParryEffect = isParry;
|
||||||
_showSkillEffect = isSkill;
|
_showSkillEffect = isSkill;
|
||||||
|
_showEvadeEffect = isEvade;
|
||||||
|
_showMissEffect = isMiss;
|
||||||
|
_showDebuffEffect = isDebuff;
|
||||||
|
_showDotEffect = isDot;
|
||||||
|
|
||||||
// 페이즈 인덱스 동기화
|
// 페이즈 인덱스 동기화
|
||||||
_phaseIndex = _battlePhaseSequence.indexWhere((p) => p.$1 == targetPhase);
|
_phaseIndex = _battlePhaseSequence.indexWhere((p) => p.$1 == targetPhase);
|
||||||
@@ -454,6 +444,7 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
monsterCategory: monsterCategory,
|
monsterCategory: monsterCategory,
|
||||||
monsterSize: monsterSize,
|
monsterSize: monsterSize,
|
||||||
raceId: widget.raceId,
|
raceId: widget.raceId,
|
||||||
|
weaponRarity: widget.weaponRarity,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 환경 타입 추론
|
// 환경 타입 추론
|
||||||
@@ -483,6 +474,10 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
_showBlockEffect = false;
|
_showBlockEffect = false;
|
||||||
_showParryEffect = false;
|
_showParryEffect = false;
|
||||||
_showSkillEffect = false;
|
_showSkillEffect = false;
|
||||||
|
_showEvadeEffect = false;
|
||||||
|
_showMissEffect = false;
|
||||||
|
_showDebuffEffect = false;
|
||||||
|
_showDotEffect = false;
|
||||||
// 공격자 타입 및 이벤트 기반 페이즈 리셋 (idle 페이즈 진입 시에만)
|
// 공격자 타입 및 이벤트 기반 페이즈 리셋 (idle 페이즈 진입 시에만)
|
||||||
// 공격 사이클(prepare→attack→hit→recover) 동안 유지 (Bug fix)
|
// 공격 사이클(prepare→attack→hit→recover) 동안 유지 (Bug fix)
|
||||||
if (_battlePhaseSequence[_phaseIndex].$1 == BattlePhase.idle) {
|
if (_battlePhaseSequence[_phaseIndex].$1 == BattlePhase.idle) {
|
||||||
@@ -513,6 +508,13 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
_environment,
|
_environment,
|
||||||
_globalTick,
|
_globalTick,
|
||||||
attacker: _currentAttacker,
|
attacker: _currentAttacker,
|
||||||
|
isCritical: _showCriticalEffect,
|
||||||
|
isEvade: _showEvadeEffect,
|
||||||
|
isMiss: _showMissEffect,
|
||||||
|
isDebuff: _showDebuffEffect,
|
||||||
|
isDot: _showDotEffect,
|
||||||
|
isBlock: _showBlockEffect,
|
||||||
|
isParry: _showParryEffect,
|
||||||
) ??
|
) ??
|
||||||
[AsciiLayer.empty()],
|
[AsciiLayer.empty()],
|
||||||
AnimationMode.walking =>
|
AnimationMode.walking =>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
|
|||||||
import 'package:asciineverdie/src/core/model/combat_event.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/combat_state.dart';
|
||||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||||
import 'package:asciineverdie/src/features/game/widgets/ascii_animation_card.dart';
|
import 'package:asciineverdie/src/features/game/widgets/ascii_animation_card.dart';
|
||||||
|
|
||||||
/// 모바일용 확장 애니메이션 패널
|
/// 모바일용 확장 애니메이션 패널
|
||||||
@@ -29,8 +31,10 @@ class EnhancedAnimationPanel extends StatefulWidget {
|
|||||||
this.shieldName,
|
this.shieldName,
|
||||||
this.characterLevel,
|
this.characterLevel,
|
||||||
this.monsterLevel,
|
this.monsterLevel,
|
||||||
|
this.monsterGrade,
|
||||||
this.latestCombatEvent,
|
this.latestCombatEvent,
|
||||||
this.raceId,
|
this.raceId,
|
||||||
|
this.weaponRarity,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ProgressState progress;
|
final ProgressState progress;
|
||||||
@@ -45,11 +49,17 @@ class EnhancedAnimationPanel extends StatefulWidget {
|
|||||||
final String? shieldName;
|
final String? shieldName;
|
||||||
final int? characterLevel;
|
final int? characterLevel;
|
||||||
final int? monsterLevel;
|
final int? monsterLevel;
|
||||||
|
|
||||||
|
/// 몬스터 등급 (Normal/Elite/Boss) - UI 색상/접두사 표시용
|
||||||
|
final MonsterGrade? monsterGrade;
|
||||||
final CombatEvent? latestCombatEvent;
|
final CombatEvent? latestCombatEvent;
|
||||||
|
|
||||||
/// 종족 ID (Phase 4: 종족별 캐릭터 애니메이션)
|
/// 종족 ID (Phase 4: 종족별 캐릭터 애니메이션)
|
||||||
final String? raceId;
|
final String? raceId;
|
||||||
|
|
||||||
|
/// 무기 희귀도 (Phase 9: 무기 등급별 이펙트 색상)
|
||||||
|
final ItemRarity? weaponRarity;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<EnhancedAnimationPanel> createState() => _EnhancedAnimationPanelState();
|
State<EnhancedAnimationPanel> createState() => _EnhancedAnimationPanelState();
|
||||||
}
|
}
|
||||||
@@ -185,9 +195,11 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
shieldName: widget.shieldName,
|
shieldName: widget.shieldName,
|
||||||
characterLevel: widget.characterLevel,
|
characterLevel: widget.characterLevel,
|
||||||
monsterLevel: widget.monsterLevel,
|
monsterLevel: widget.monsterLevel,
|
||||||
|
monsterGrade: widget.monsterGrade,
|
||||||
isPaused: widget.isPaused,
|
isPaused: widget.isPaused,
|
||||||
latestCombatEvent: widget.latestCombatEvent,
|
latestCombatEvent: widget.latestCombatEvent,
|
||||||
raceId: widget.raceId,
|
raceId: widget.raceId,
|
||||||
|
weaponRarity: widget.weaponRarity,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -630,11 +642,34 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
? (task.position / task.max).clamp(0.0, 1.0)
|
? (task.position / task.max).clamp(0.0, 1.0)
|
||||||
: 0.0;
|
: 0.0;
|
||||||
|
|
||||||
|
// 몬스터 등급에 따른 접두사와 색상
|
||||||
|
final grade = widget.monsterGrade;
|
||||||
|
final isKillTask = widget.progress.currentTask.type == TaskType.kill;
|
||||||
|
final gradePrefix =
|
||||||
|
(isKillTask && grade != null) ? grade.displayPrefix : '';
|
||||||
|
final gradeColor =
|
||||||
|
(isKillTask && grade != null) ? grade.displayColor : null;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// 캡션
|
// 캡션 (등급에 따른 접두사 및 색상)
|
||||||
Text(
|
Text.rich(
|
||||||
_getStatusMessage(),
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
if (gradePrefix.isNotEmpty)
|
||||||
|
TextSpan(
|
||||||
|
text: gradePrefix,
|
||||||
|
style: TextStyle(
|
||||||
|
color: gradeColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: _getStatusMessage(),
|
||||||
|
style: gradeColor != null ? TextStyle(color: gradeColor) : null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:asciineverdie/l10n/app_localizations.dart';
|
|||||||
import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
|
import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
|
||||||
import 'package:asciineverdie/src/core/model/combat_event.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/game_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||||
import 'package:asciineverdie/src/features/game/widgets/ascii_animation_card.dart';
|
import 'package:asciineverdie/src/features/game/widgets/ascii_animation_card.dart';
|
||||||
|
|
||||||
/// 상단 패널: ASCII 애니메이션 + Task Progress 바
|
/// 상단 패널: ASCII 애니메이션 + Task Progress 바
|
||||||
@@ -23,6 +24,7 @@ class TaskProgressPanel extends StatelessWidget {
|
|||||||
this.shieldName,
|
this.shieldName,
|
||||||
this.characterLevel,
|
this.characterLevel,
|
||||||
this.monsterLevel,
|
this.monsterLevel,
|
||||||
|
this.monsterGrade,
|
||||||
this.latestCombatEvent,
|
this.latestCombatEvent,
|
||||||
this.raceId,
|
this.raceId,
|
||||||
});
|
});
|
||||||
@@ -44,6 +46,9 @@ class TaskProgressPanel extends StatelessWidget {
|
|||||||
final int? characterLevel;
|
final int? characterLevel;
|
||||||
final int? monsterLevel;
|
final int? monsterLevel;
|
||||||
|
|
||||||
|
/// 몬스터 등급 (Normal/Elite/Boss) - UI 색상/접두사 표시용
|
||||||
|
final MonsterGrade? monsterGrade;
|
||||||
|
|
||||||
/// 최근 전투 이벤트 (애니메이션 동기화용, Phase 5)
|
/// 최근 전투 이벤트 (애니메이션 동기화용, Phase 5)
|
||||||
final CombatEvent? latestCombatEvent;
|
final CombatEvent? latestCombatEvent;
|
||||||
|
|
||||||
@@ -74,6 +79,7 @@ class TaskProgressPanel extends StatelessWidget {
|
|||||||
shieldName: shieldName,
|
shieldName: shieldName,
|
||||||
characterLevel: characterLevel,
|
characterLevel: characterLevel,
|
||||||
monsterLevel: monsterLevel,
|
monsterLevel: monsterLevel,
|
||||||
|
monsterGrade: monsterGrade,
|
||||||
isPaused: isPaused,
|
isPaused: isPaused,
|
||||||
latestCombatEvent: latestCombatEvent,
|
latestCombatEvent: latestCombatEvent,
|
||||||
raceId: raceId,
|
raceId: raceId,
|
||||||
@@ -87,11 +93,7 @@ class TaskProgressPanel extends StatelessWidget {
|
|||||||
_buildPauseButton(context),
|
_buildPauseButton(context),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: _buildStatusMessage(context),
|
||||||
_getStatusMessage(context),
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_buildSpeedButton(context),
|
_buildSpeedButton(context),
|
||||||
@@ -153,6 +155,42 @@ class TaskProgressPanel extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 상태 메시지 위젯 (등급에 따른 접두사 및 색상 적용)
|
||||||
|
Widget _buildStatusMessage(BuildContext context) {
|
||||||
|
final message = _getStatusMessage(context);
|
||||||
|
|
||||||
|
// 몬스터 등급에 따른 접두사와 색상
|
||||||
|
final grade = monsterGrade;
|
||||||
|
final isKillTask = progress.currentTask.type == TaskType.kill;
|
||||||
|
final gradePrefix =
|
||||||
|
(isKillTask && grade != null) ? grade.displayPrefix : '';
|
||||||
|
final gradeColor =
|
||||||
|
(isKillTask && grade != null) ? grade.displayColor : null;
|
||||||
|
|
||||||
|
return Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
if (gradePrefix.isNotEmpty)
|
||||||
|
TextSpan(
|
||||||
|
text: gradePrefix,
|
||||||
|
style: TextStyle(
|
||||||
|
color: gradeColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: message,
|
||||||
|
style: gradeColor != null ? TextStyle(color: gradeColor) : null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 현재 상태에 맞는 메시지 반환
|
/// 현재 상태에 맞는 메시지 반환
|
||||||
///
|
///
|
||||||
/// 특수 애니메이션(부활 등) 중에는 해당 메시지 표시
|
/// 특수 애니메이션(부활 등) 중에는 해당 메시지 표시
|
||||||
|
|||||||
Reference in New Issue
Block a user