feat(animation): 전투 애니메이션 및 캔버스 개선
- CanvasBattleComposer 몬스터 등급별 색상 지원 - AsciiCanvasPainter/Widget 개선 - AsciiCell 스타일 확장
This commit is contained in:
@@ -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'********'],
|
||||||
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user