feat(animation): 레이어 투명도 및 이펙트 5줄 확장

- AsciiLayer에 opacity 필드 추가
- AsciiCanvasPainter에서 레이어 투명도 렌더링 지원
- 배경 레이어 50% 투명으로 캐릭터 부각
- 모든 무기 이펙트 3줄→5줄로 확장
- 몬스터 공격 이펙트 5줄로 확장
This commit is contained in:
JiWoong Sul
2025-12-27 21:58:45 +09:00
parent 5fa58695ec
commit bfeb58ff29
5 changed files with 83 additions and 72 deletions

View File

@@ -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(

View File

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

View File

@@ -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<List<String>> _largeAlertFrames(MonsterCategory category) {
}
// ============================================================================
// 몬스터 공격 이펙트 (← 방향, Phase 8)
// 몬스터 공격 이펙트 (← 방향, Phase 8) - 5줄
// ============================================================================
/// 몬스터 공격 준비 프레임
/// 몬스터 공격 준비 프레임 (5줄)
const _monsterPrepareFrames = <List<String>>[
[r' ', r' <', r' '],
[r' ', r' <<', r' '],
[r' ', r' ', r' < ', r' ', r' '],
[r' ', r' _ ', r' << ', r' - ', r' '],
];
/// 몬스터 공격 프레임
/// 몬스터 공격 프레임 (5줄)
const _monsterAttackFrames = <List<String>>[
[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 = <List<String>>[
[r' *SLASH*', r' <-----', r' '],
[r'*ATTACK*', r' <----', r' '],
[r' *SLASH!* ', r' **** ', r' <----- ', r' **** ', r' '],
[r'*ATTACK!* ', r' **** ', r' <---- ', r' **** ', r' '],
];

View File

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