From bfeb58ff295b4d67218f0e3a1636d0285a3a8eef Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Sat, 27 Dec 2025 21:58:45 +0900 Subject: [PATCH] =?UTF-8?q?feat(animation):=20=EB=A0=88=EC=9D=B4=EC=96=B4?= =?UTF-8?q?=20=ED=88=AC=EB=AA=85=EB=8F=84=20=EB=B0=8F=20=EC=9D=B4=ED=8E=99?= =?UTF-8?q?=ED=8A=B8=205=EC=A4=84=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AsciiLayer에 opacity 필드 추가 - AsciiCanvasPainter에서 레이어 투명도 렌더링 지원 - 배경 레이어 50% 투명으로 캐릭터 부각 - 모든 무기 이펙트 3줄→5줄로 확장 - 몬스터 공격 이펙트 5줄로 확장 --- .../canvas/ascii_canvas_painter.dart | 16 ++- .../core/animation/canvas/ascii_layer.dart | 4 + .../canvas/canvas_battle_composer.dart | 25 ++-- lib/src/core/animation/weapon_effects.dart | 108 +++++++++--------- .../game/widgets/ascii_animation_card.dart | 2 +- 5 files changed, 83 insertions(+), 72 deletions(-) diff --git a/lib/src/core/animation/canvas/ascii_canvas_painter.dart b/lib/src/core/animation/canvas/ascii_canvas_painter.dart index 37dd4a5..c4398a4 100644 --- a/lib/src/core/animation/canvas/ascii_canvas_painter.dart +++ b/lib/src/core/animation/canvas/ascii_canvas_painter.dart @@ -12,12 +12,14 @@ class _ParagraphCacheKey { required this.color, required this.fontSize, required this.cellWidth, + required this.opacity, }); final String char; final AsciiCellColor color; final double fontSize; final double cellWidth; + final double opacity; @override bool operator ==(Object other) => @@ -26,10 +28,11 @@ class _ParagraphCacheKey { char == other.char && color == other.color && fontSize == other.fontSize && - cellWidth == other.cellWidth; + cellWidth == other.cellWidth && + opacity == other.opacity; @override - int get hashCode => Object.hash(char, color, fontSize, cellWidth); + int get hashCode => Object.hash(char, color, fontSize, cellWidth, opacity); } /// ASCII Canvas 페인터 (CustomPainter 구현) @@ -124,7 +127,8 @@ class AsciiCanvasPainter extends CustomPainter { final x = gridX * cellWidth; final y = gridY * cellHeight; - _drawCell(canvas, cell, x, y, cellWidth, cellHeight, fontSize); + // 레이어 투명도를 셀 렌더링에 전달 + _drawCell(canvas, cell, x, y, cellWidth, cellHeight, fontSize, layer.opacity); } } } @@ -138,20 +142,22 @@ class AsciiCanvasPainter extends CustomPainter { double cellWidth, double cellHeight, double fontSize, + double opacity, ) { final cacheKey = _ParagraphCacheKey( char: cell.char, color: cell.color, fontSize: fontSize, cellWidth: cellWidth, + opacity: opacity, ); // 캐시 히트 확인 var paragraph = _paragraphCache[cacheKey]; if (paragraph == null) { - // 캐시 미스: 새 Paragraph 생성 - final color = _getColor(cell.color); + // 캐시 미스: 새 Paragraph 생성 (opacity 적용) + final color = _getColor(cell.color).withValues(alpha: opacity); final paragraphBuilder = ui.ParagraphBuilder( ui.ParagraphStyle( diff --git a/lib/src/core/animation/canvas/ascii_layer.dart b/lib/src/core/animation/canvas/ascii_layer.dart index 22b08c6..296c940 100644 --- a/lib/src/core/animation/canvas/ascii_layer.dart +++ b/lib/src/core/animation/canvas/ascii_layer.dart @@ -9,6 +9,7 @@ class AsciiLayer { this.zIndex = 0, this.offsetX = 0, this.offsetY = 0, + this.opacity = 1.0, }); /// 2D 셀 배열 [row][column] @@ -23,6 +24,9 @@ class AsciiLayer { /// Y 오프셋 final int offsetY; + /// 레이어 투명도 (0.0 ~ 1.0, 배경 레이어 등에서 사용) + final double opacity; + /// 레이어 높이 (줄 수) int get height => cells.length; diff --git a/lib/src/core/animation/canvas/canvas_battle_composer.dart b/lib/src/core/animation/canvas/canvas_battle_composer.dart index 3b01b51..6e3425e 100644 --- a/lib/src/core/animation/canvas/canvas_battle_composer.dart +++ b/lib/src/core/animation/canvas/canvas_battle_composer.dart @@ -99,7 +99,8 @@ class CanvasBattleComposer { } } - return AsciiLayer(cells: cells, zIndex: 0); + // 배경 레이어는 50% 투명으로 캐릭터 부각 + return AsciiLayer(cells: cells, zIndex: 0, opacity: 0.5); } /// 캐릭터 레이어 생성 (z=1) @@ -1107,24 +1108,24 @@ List> _largeAlertFrames(MonsterCategory category) { } // ============================================================================ -// 몬스터 공격 이펙트 (← 방향, Phase 8) +// 몬스터 공격 이펙트 (← 방향, Phase 8) - 5줄 // ============================================================================ -/// 몬스터 공격 준비 프레임 +/// 몬스터 공격 준비 프레임 (5줄) const _monsterPrepareFrames = >[ - [r' ', r' <', r' '], - [r' ', r' <<', r' '], + [r' ', r' ', r' < ', r' ', r' '], + [r' ', r' _ ', r' << ', r' - ', r' '], ]; -/// 몬스터 공격 프레임 +/// 몬스터 공격 프레임 (5줄) const _monsterAttackFrames = >[ - [r' ', r' <-- ', r' '], - [r' ', r' <--- ', r' '], - [r' ', r' <-----', r' '], + [r' ', r' __ ', r' <-- ', r' -- ', r' '], + [r' ', r' ___ ', r' <--- ', r' --- ', r' '], + [r' ', r' ____ ', r' <----- ', r' ---- ', r' '], ]; -/// 몬스터 히트 프레임 +/// 몬스터 히트 프레임 (5줄) const _monsterHitFrames = >[ - [r' *SLASH*', r' <-----', r' '], - [r'*ATTACK*', r' <----', r' '], + [r' *SLASH!* ', r' **** ', r' <----- ', r' **** ', r' '], + [r'*ATTACK!* ', r' **** ', r' <---- ', r' **** ', r' '], ]; diff --git a/lib/src/core/animation/weapon_effects.dart b/lib/src/core/animation/weapon_effects.dart index ad9578d..3788157 100644 --- a/lib/src/core/animation/weapon_effects.dart +++ b/lib/src/core/animation/weapon_effects.dart @@ -47,95 +47,95 @@ WeaponEffect getWeaponEffect(WeaponCategory category) { } // ============================================================================ -// 둔기류 - 휘두르기 (3줄) +// 둔기류 - 휘두르기 (5줄) // ============================================================================ const _bluntEffect = WeaponEffect( prepareFrames: [ - [r' _', r' /', r' /'], - [r' _/', r' / ', r' / '], + [r' ', r' _ ', r' / ', r' / ', r' / '], + [r' ', r' _/ ', r' / ', r' / ', r' / '], ], attackFrames: [ - [r' _/ ', r' / ', r'/ '], - [r' /__ ', r'/ ', r' '], - [r'/__ ', r' ', r' '], - [r'/__=>', r' ', r' '], + [r' ', r' _/ ', r' / ', r' / ', r' / '], + [r' ', r' _/ ', r' / ', r' /________', r'/ '], + [r' ', r' _/ ', r' /________', r'/ ', r' '], + [r' ', r' _/____=> ', r'/ ', r' ', r' '], ], hitFrames: [ - [r' *BASH* ', r'/__=> ', r' '], - [r'*SMASH!*', r' /__ ', r' '], + [r' *BASH* ', r' _/____=> ', r'/ ** ', r' ** ', r' '], + [r' *SMASH!* ', r' _/____ ', r' / ** ', r' ** ', r' '], ], hitSound: '*BASH*', - effectHeight: 3, - effectYStart: 2, + effectHeight: 5, + effectYStart: 1, ); // ============================================================================ -// 케이블류 - 채찍질 (3줄) +// 케이블류 - 채찍질 (5줄) // ============================================================================ const _cableEffect = WeaponEffect( prepareFrames: [ - [r' ', r'~ ', r' ~ '], - [r' ', r'~~ ', r' ~ '], + [r' ', r'~ ', r' ~ ', r' ~ ', r' ~ '], + [r' ', r'~~ ', r' ~~ ', r' ~ ', r' ~ '], ], attackFrames: [ - [r' ', r'~~~ ', r' ~~ '], - [r' ', r'~~~~ ', r' ~~ '], - [r' ', r'~~~~~> ', r' ~~ '], - [r' ', r'~~~~~~> ', r' ~~'], + [r' ', r'~~~ ', r' ~~~ ', r' ~~ ', r' ~ '], + [r' ', r'~~~~ ', r' ~~~~ ', r' ~~', r' '], + [r' ', r'~~~~~> ', r' ~~~~~', r' ', r' '], + [r' ', r'~~~~~~> ', r' ~~~~', r' ', r' '], ], hitFrames: [ - [r' *WHIP*', r'~~~~~~> ', r' ~~'], - [r' *CRACK*', r'~~~~~> ', r' ~~ '], + [r' *WHIP* ', r'~~~~~~> ', r' ~~~~', r' **', r' '], + [r' *CRACK!* ', r'~~~~~> ', r' ~~~~~', r' **', r' '], ], hitSound: '*WHIP*', - effectHeight: 3, - effectYStart: 2, + effectHeight: 5, + effectYStart: 1, ); // ============================================================================ -// 투척류 - 발사 (3줄) +// 투척류 - 발사 (5줄) // ============================================================================ const _projectileEffect = WeaponEffect( prepareFrames: [ - [r' ', r'[=] ', r' '], - [r' ', r'[==] ', r' '], + [r' ', r' ', r' [=] ', r' ', r' '], + [r' ', r' _ ', r' [==] ', r' - ', r' '], ], attackFrames: [ - [r' ', r' [> ', r' '], - [r' ', r' [>', r' '], - [r' ', r' [>', r' '], - [r' ', r' [>', r' '], + [r' ', r' . ', r' [> ', r" ' ", r' '], + [r' ', r' . ', r' [> ', r" ' ", r' '], + [r' ', r' . ', r' [>', r" ' ", r' '], + [r' ', r' ', r' [>', r' ', r' '], ], hitFrames: [ - [r' *CLANG*', r' [>', r' '], - [r' *CRASH* ', r' [> ', r' '], + [r' *CLANG!* ', r' ***', r' [>', r' ***', r' '], + [r' *CRASH!* ', r' *** ', r' [> ', r' *** ', r' '], ], hitSound: '*CLANG*', - effectHeight: 3, - effectYStart: 2, + effectHeight: 5, + effectYStart: 1, ); // ============================================================================ -// 에너지류 - 빔 발사 (3줄) +// 에너지류 - 빔 발사 (5줄) // ============================================================================ const _energyEffect = WeaponEffect( prepareFrames: [ - [r' ', r' <*> ', r' '], - [r' == ', r' <**> ', r' == '], + [r' ', r' == ', r' <**> ', r' == ', r' '], + [r' ==== ', r' ====== ', r' <****> ', r' ====== ', r' ==== '], ], attackFrames: [ - [r' ==== ', r'==<*>== ', r' ==== '], - [r' ====== ', r'===<*>==', r' ====== '], - [r'========', r'===<*>==', r'========'], - [r'========', r'====<*>=', r'========'], + [r' ====== ', r' ======== ', r'===<**>===', r' ======== ', r' ====== '], + [r' ======== ', r'==========', r'===<**>===', r'==========', r' ======== '], + [r'==========', r'==========', r'====<**>==', r'==========', r'=========='], + [r'==========', r'==========', r'=====<**>=', r'==========', r'=========='], ], hitFrames: [ - [r'==*ZAP*=', r'===<*>==', r'========'], - [r'*BZZT!*=', r'====<*>=', r'========'], + [r'===*ZAP*==', r'==========', r'====<**>==', r'==========', r'===*ZAP*=='], + [r'==*BZZT!*=', r'==========', r'=====<**>=', r'==========', r'==*BZZT!*='], ], hitSound: '*ZAP*', - effectHeight: 3, - effectYStart: 2, + effectHeight: 5, + effectYStart: 1, ); // ============================================================================ @@ -162,23 +162,23 @@ const _cosmicEffect = WeaponEffect( ); // ============================================================================ -// 맨손 - 기본 펀치 (3줄) +// 맨손 - 기본 펀치 (5줄) // ============================================================================ const _unarmedEffect = WeaponEffect( prepareFrames: [ - [r' ', r' ', r' '], - [r' ', r' > ', r' '], + [r' ', r' ', r' > ', r' ', r' '], + [r' ', r' _ ', r' -> ', r' - ', r' '], ], attackFrames: [ - [r' ', r'-> ', r' '], - [r' ', r'---> ', r' '], - [r' ', r'-----> ', r' '], + [r' ', r' __ ', r' ---> ', r' -- ', r' '], + [r' ', r' ___ ', r' -----> ', r' --- ', r' '], + [r' ', r' ____', r' ======>', r' ----', r' '], ], hitFrames: [ - [r' *POW* ', r'-----> ', r' '], - [r'*PUNCH*', r'----> ', r' '], + [r' *POW!* ', r' ****', r' ======>', r' ****', r' '], + [r' *PUNCH!* ', r' **** ', r' =====> ', r' **** ', r' '], ], hitSound: '*POW*', - effectHeight: 3, - effectYStart: 2, + effectHeight: 5, + effectYStart: 1, ); diff --git a/lib/src/features/game/widgets/ascii_animation_card.dart b/lib/src/features/game/widgets/ascii_animation_card.dart index a8812ac..5f00481 100644 --- a/lib/src/features/game/widgets/ascii_animation_card.dart +++ b/lib/src/features/game/widgets/ascii_animation_card.dart @@ -520,7 +520,7 @@ class _AsciiAnimationCardState extends State { @override Widget build(BuildContext context) { - // Phase 7: 고정 4색 팔레트 사용 (colorTheme 무시) + // 검정 배경 위에 배경 레이어(50% 투명)가 그려짐 const bgColor = AsciiColors.background; // 테두리 효과 결정 (전투 이벤트 또는 특수 애니메이션)