diff --git a/lib/src/core/animation/canvas/ascii_canvas_painter.dart b/lib/src/core/animation/canvas/ascii_canvas_painter.dart index 80de44a..a8dde48 100644 --- a/lib/src/core/animation/canvas/ascii_canvas_painter.dart +++ b/lib/src/core/animation/canvas/ascii_canvas_painter.dart @@ -52,6 +52,10 @@ class AsciiCanvasPainter extends CustomPainter { this.objectColor = AsciiColors.object, this.positiveColor = AsciiColors.positive, this.negativeColor = AsciiColors.negative, + this.rarityUncommonColor = AsciiColors.rarityUncommon, + this.rarityRareColor = AsciiColors.rarityRare, + this.rarityEpicColor = AsciiColors.rarityEpic, + this.rarityLegendaryColor = AsciiColors.rarityLegendary, }); /// 렌더링할 레이어 목록 (z-order 정렬 필요) @@ -81,6 +85,22 @@ class AsciiCanvasPainter extends CustomPainter { /// 네거티브 이펙트 색상 (테마 인식) final Color negativeColor; + // ═══════════════════════════════════════════════════════════════════════ + // 무기 등급(ItemRarity) 색상 (Phase 9) + // ═══════════════════════════════════════════════════════════════════════ + + /// Uncommon 등급 색상 (초록) + final Color rarityUncommonColor; + + /// Rare 등급 색상 (파랑) + final Color rarityRareColor; + + /// Epic 등급 색상 (보라) + final Color rarityEpicColor; + + /// Legendary 등급 색상 (금색) + final Color rarityLegendaryColor; + /// Paragraph 캐시 (문자+색상+크기 조합별) static final Map<_ParagraphCacheKey, ui.Paragraph> _paragraphCache = {}; @@ -209,6 +229,11 @@ class AsciiCanvasPainter extends CustomPainter { AsciiCellColor.object => objectColor, AsciiCellColor.positive => positiveColor, AsciiCellColor.negative => negativeColor, + // 무기 등급 색상 (Phase 9) + AsciiCellColor.rarityUncommon => rarityUncommonColor, + AsciiCellColor.rarityRare => rarityRareColor, + AsciiCellColor.rarityEpic => rarityEpicColor, + AsciiCellColor.rarityLegendary => rarityLegendaryColor, }; } diff --git a/lib/src/core/animation/canvas/ascii_canvas_widget.dart b/lib/src/core/animation/canvas/ascii_canvas_widget.dart index 52ffdef..c17d1be 100644 --- a/lib/src/core/animation/canvas/ascii_canvas_widget.dart +++ b/lib/src/core/animation/canvas/ascii_canvas_widget.dart @@ -46,6 +46,12 @@ class AsciiCanvasWidget extends StatelessWidget { final posColor = AsciiColors.positiveOf(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( child: CustomPaint( painter: AsciiCanvasPainter( @@ -58,6 +64,10 @@ class AsciiCanvasWidget extends StatelessWidget { objectColor: objColor, positiveColor: posColor, negativeColor: negColor, + rarityUncommonColor: uncommonColor, + rarityRareColor: rareColor, + rarityEpicColor: epicColor, + rarityLegendaryColor: legendaryColor, ), size: Size.infinite, isComplex: true, diff --git a/lib/src/core/animation/canvas/ascii_cell.dart b/lib/src/core/animation/canvas/ascii_cell.dart index 0a977d2..c3d4e26 100644 --- a/lib/src/core/animation/canvas/ascii_cell.dart +++ b/lib/src/core/animation/canvas/ascii_cell.dart @@ -1,4 +1,4 @@ -/// ASCII 셀 색상 (4색 팔레트) +/// ASCII 셀 색상 (4색 팔레트 + 무기 등급 색상) enum AsciiCellColor { /// 배경색 (검정) background, @@ -6,11 +6,27 @@ enum AsciiCellColor { /// 오브젝트 (흰색) - 캐릭터, 몬스터, 지형 object, - /// 포지티브 이펙트 (시안) - !, +, =, >, < + /// 포지티브 이펙트 (시안) - !, +, =, >, < / common 무기 positive, /// 네거티브 이펙트 (마젠타) - *, ~ negative, + + // ═══════════════════════════════════════════════════════════════════════ + // 무기 등급(ItemRarity) 색상 (Phase 9) + // ═══════════════════════════════════════════════════════════════════════ + + /// Uncommon 등급 (초록) + rarityUncommon, + + /// Rare 등급 (파랑) + rarityRare, + + /// Epic 등급 (보라) + rarityEpic, + + /// Legendary 등급 (금색) + rarityLegendary, } /// 단일 ASCII 셀 데이터 diff --git a/lib/src/core/animation/canvas/canvas_battle_composer.dart b/lib/src/core/animation/canvas/canvas_battle_composer.dart index 4646602..93847cc 100644 --- a/lib/src/core/animation/canvas/canvas_battle_composer.dart +++ b/lib/src/core/animation/canvas/canvas_battle_composer.dart @@ -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/weapon_category.dart'; import 'package:asciineverdie/src/core/animation/weapon_effects.dart'; +import 'package:asciineverdie/src/core/model/item_stats.dart'; /// Canvas용 전투 프레임 합성기 /// @@ -20,6 +21,7 @@ class CanvasBattleComposer { required this.monsterCategory, required this.monsterSize, this.raceId, + this.weaponRarity, }); final WeaponCategory weaponCategory; @@ -30,6 +32,9 @@ class CanvasBattleComposer { /// 종족 ID (Phase 4: 종족별 캐릭터 애니메이션) final String? raceId; + /// 무기 희귀도 (Phase 9: 무기 등급별 이펙트 색상) + final ItemRarity? weaponRarity; + /// 프레임 상수 static const int frameWidth = 60; static const int frameHeight = 8; @@ -43,6 +48,13 @@ class CanvasBattleComposer { EnvironmentType environment, int globalTick, { 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 = [ _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 정렬 layers.sort((a, b) => a.zIndex.compareTo(b.zIndex)); @@ -262,7 +304,15 @@ class CanvasBattleComposer { if (effectLines.isEmpty) return null; - final cells = _spriteToCells(effectLines); + // Phase 9: 플레이어 공격 시 무기 등급 색상 적용 + final List> cells; + if (attacker == AttackerType.player && weaponRarity != null) { + // 무기 등급에 따른 이펙트 색상 + cells = _spriteToCellsWithColor(effectLines, weaponRarity!.effectCellColor); + } else { + // 기본 색상 (자동 색상 결정) + cells = _spriteToCells(effectLines); + } // 이펙트 높이에 따른 동적 Y 위치 (캔버스 하단 기준) 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> 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 _getMonsterAttackEffect(BattlePhase phase, int subFrame) { return switch (phase) { @@ -293,6 +456,21 @@ class CanvasBattleComposer { }).toList(); } + /// 문자열 스프라이트를 지정된 색상으로 AsciiCell 2D 배열로 변환 (Phase 9) + /// + /// 이펙트 문자(공백 아닌 문자)에 지정 색상 적용 + List> _spriteToCellsWithColor( + List 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 배열로 변환 List> _spriteToRightAlignedCells( List lines, @@ -1129,3 +1307,53 @@ const _monsterHitFrames = >[ [r' *SLASH!* ', r' **** ', r' <----- ', r' **** ', r' '], [r'*ATTACK!* ', r' **** ', r' <---- ', r' **** ', r' '], ]; + +// ============================================================================ +// 크리티컬 텍스트 프레임 (2줄, Phase 10) +// ============================================================================ + +/// 크리티컬 히트 텍스트 프레임 (반짝임 애니메이션) +const _critTextFrames = >[ + [r'*CRITICAL!*', r' ========='], + [r'=CRITICAL!=', r' *********'], +]; + +// ============================================================================ +// 전투 텍스트 이펙트 프레임 (Phase 11) +// ============================================================================ + +/// 회피 텍스트 프레임 (플레이어 회피 성공) +const _evadeTextFrames = >[ + [r'*EVADE!*', r'========'], + [r'=EVADE!=', r'********'], +]; + +/// 미스 텍스트 프레임 (플레이어 공격 빗나감) +const _missTextFrames = >[ + [r'*MISS!*', r'======='], + [r'=MISS!=', r'*******'], +]; + +/// 디버프 텍스트 프레임 (적에게 디버프 적용) +const _debuffTextFrames = >[ + [r'*DEBUFF!*', r'========='], + [r'=DEBUFF!=', r'*********'], +]; + +/// DOT 텍스트 프레임 (지속 피해) +const _dotTextFrames = >[ + [r'*DOT!*', r'======'], + [r'=DOT!=', r'******'], +]; + +/// 블록 텍스트 프레임 (방패 방어) +const _blockTextFrames = >[ + [r'*BLOCK!*', r'========'], + [r'=BLOCK!=', r'********'], +]; + +/// 패리 텍스트 프레임 (무기 쳐내기) +const _parryTextFrames = >[ + [r'*PARRY!*', r'========'], + [r'=PARRY!=', r'********'], +];