feat(animation): 공격자별 위치 분리 및 이펙트 추가
- AttackerType enum 추가 (none, player, monster, both) - 플레이어 공격 시에만 캐릭터 이동 (Phase 7) - 몬스터 공격 시에만 몬스터 이동 (Phase 7) - 몬스터 공격 이펙트 추가 (← 방향, Phase 8) - AsciiAnimationCard에서 공격자 타입 전달
This commit is contained in:
@@ -41,17 +41,19 @@ class CanvasBattleComposer {
|
||||
int subFrame,
|
||||
String? monsterBaseName,
|
||||
EnvironmentType environment,
|
||||
int globalTick,
|
||||
) {
|
||||
int globalTick, {
|
||||
AttackerType attacker = AttackerType.none,
|
||||
}) {
|
||||
final layers = <AsciiLayer>[
|
||||
_createBackgroundLayer(environment, globalTick),
|
||||
_createCharacterLayer(phase, subFrame),
|
||||
_createMonsterLayer(phase, subFrame),
|
||||
_createCharacterLayer(phase, subFrame, attacker),
|
||||
_createMonsterLayer(phase, subFrame, attacker),
|
||||
];
|
||||
|
||||
// 이펙트 레이어 (공격/히트 페이즈에서만)
|
||||
if (phase == BattlePhase.attack || phase == BattlePhase.hit) {
|
||||
final effectLayer = _createEffectLayer(phase, subFrame);
|
||||
// 이펙트 레이어 (공격/히트 페이즈에서만, 공격자 있을 때)
|
||||
if ((phase == BattlePhase.attack || phase == BattlePhase.hit) &&
|
||||
attacker != AttackerType.none) {
|
||||
final effectLayer = _createEffectLayer(phase, subFrame, attacker);
|
||||
if (effectLayer != null) {
|
||||
layers.add(effectLayer);
|
||||
}
|
||||
@@ -101,7 +103,12 @@ class CanvasBattleComposer {
|
||||
/// 캐릭터 레이어 생성 (z=1)
|
||||
///
|
||||
/// Phase 4: 종족별 캐릭터 프레임 지원
|
||||
AsciiLayer _createCharacterLayer(BattlePhase phase, int subFrame) {
|
||||
/// Phase 7: 공격자별 위치 분리 (플레이어가 공격자일 때만 이동)
|
||||
AsciiLayer _createCharacterLayer(
|
||||
BattlePhase phase,
|
||||
int subFrame,
|
||||
AttackerType attacker,
|
||||
) {
|
||||
CharacterFrame charFrame;
|
||||
|
||||
// 종족 ID가 있으면 종족별 프레임 사용
|
||||
@@ -122,13 +129,18 @@ class CanvasBattleComposer {
|
||||
charFrame = charFrame.withShield();
|
||||
}
|
||||
|
||||
// 페이즈별 X 위치 (idle: 20%, attack: 30%)
|
||||
// 플레이어가 공격자인지 확인
|
||||
final isPlayerAttacking =
|
||||
attacker == AttackerType.player || attacker == AttackerType.both;
|
||||
|
||||
// 페이즈별 X 위치 (Phase 7: 공격자별 위치 분리)
|
||||
// 플레이어가 공격자일 때만 이동, 아니면 제자리(12)
|
||||
final charX = switch (phase) {
|
||||
BattlePhase.idle => 12, // 20%
|
||||
BattlePhase.prepare => 15, // 전환중
|
||||
BattlePhase.attack => 18, // 30%
|
||||
BattlePhase.hit => 18, // 30%
|
||||
BattlePhase.recover => 15, // 전환중
|
||||
BattlePhase.idle => 12,
|
||||
BattlePhase.prepare => isPlayerAttacking ? 15 : 12,
|
||||
BattlePhase.attack => isPlayerAttacking ? 18 : 12,
|
||||
BattlePhase.hit => attacker == AttackerType.both ? 18 : 12,
|
||||
BattlePhase.recover => isPlayerAttacking ? 15 : 12,
|
||||
};
|
||||
|
||||
final cells = _spriteToCells(charFrame.lines);
|
||||
@@ -144,7 +156,13 @@ class CanvasBattleComposer {
|
||||
}
|
||||
|
||||
/// 몬스터 레이어 생성 (z=1, 캐릭터보다 뒤)
|
||||
AsciiLayer _createMonsterLayer(BattlePhase phase, int subFrame) {
|
||||
///
|
||||
/// Phase 7: 공격자별 위치 분리 (몬스터가 공격자일 때만 이동)
|
||||
AsciiLayer _createMonsterLayer(
|
||||
BattlePhase phase,
|
||||
int subFrame,
|
||||
AttackerType attacker,
|
||||
) {
|
||||
final monsterFrames = _getAnimatedMonsterFrames(
|
||||
monsterCategory,
|
||||
monsterSize,
|
||||
@@ -155,14 +173,18 @@ class CanvasBattleComposer {
|
||||
// 몬스터 스프라이트를 오른쪽 정렬하여 셀로 변환
|
||||
final cells = _spriteToRightAlignedCells(monsterFrame, monsterWidth);
|
||||
|
||||
// 페이즈별 X 위치 (idle: 80%, attack: 70%)
|
||||
// 몬스터 오른쪽 끝 기준: idle=48, attack=42
|
||||
// 몬스터가 공격자인지 확인
|
||||
final isMonsterAttacking =
|
||||
attacker == AttackerType.monster || attacker == AttackerType.both;
|
||||
|
||||
// 페이즈별 X 위치 (Phase 7: 공격자별 위치 분리)
|
||||
// 몬스터가 공격자일 때만 이동, 아니면 제자리(48)
|
||||
final monsterRightEdge = switch (phase) {
|
||||
BattlePhase.idle => 48, // 80%
|
||||
BattlePhase.prepare => 45, // 전환중
|
||||
BattlePhase.attack => 42, // 70%
|
||||
BattlePhase.hit => 42, // 70%
|
||||
BattlePhase.recover => 45, // 전환중
|
||||
BattlePhase.idle => 48,
|
||||
BattlePhase.prepare => isMonsterAttacking ? 45 : 48,
|
||||
BattlePhase.attack => attacker == AttackerType.both ? 42 : 48,
|
||||
BattlePhase.hit => isMonsterAttacking ? 42 : 48,
|
||||
BattlePhase.recover => isMonsterAttacking ? 45 : 48,
|
||||
};
|
||||
final monsterX = monsterRightEdge - monsterWidth;
|
||||
|
||||
@@ -178,21 +200,39 @@ class CanvasBattleComposer {
|
||||
}
|
||||
|
||||
/// 이펙트 레이어 생성 (z=3, 캐릭터/몬스터 위에 표시)
|
||||
AsciiLayer? _createEffectLayer(BattlePhase phase, int subFrame) {
|
||||
final effect = getWeaponEffect(weaponCategory);
|
||||
final effectLines = _getEffectLines(effect, phase, subFrame);
|
||||
///
|
||||
/// Phase 8: 공격자에 따라 이펙트 위치/모양 분리
|
||||
/// - 플레이어 공격: 몬스터 왼쪽에 무기 이펙트 (→ 방향)
|
||||
/// - 몬스터 공격: 캐릭터 오른쪽에 공격 이펙트 (← 방향)
|
||||
AsciiLayer? _createEffectLayer(
|
||||
BattlePhase phase,
|
||||
int subFrame,
|
||||
AttackerType attacker,
|
||||
) {
|
||||
// 공격자에 따라 다른 이펙트 사용
|
||||
final List<String> effectLines;
|
||||
final int effectX;
|
||||
|
||||
if (attacker == AttackerType.player) {
|
||||
// 플레이어 공격: 무기 이펙트 → 몬스터 왼쪽
|
||||
final effect = getWeaponEffect(weaponCategory);
|
||||
effectLines = _getEffectLines(effect, phase, subFrame);
|
||||
effectX = 28;
|
||||
} else if (attacker == AttackerType.monster) {
|
||||
// 몬스터 공격: 왼쪽 방향 이펙트 → 캐릭터 오른쪽
|
||||
effectLines = _getMonsterAttackEffect(phase, subFrame);
|
||||
effectX = 18;
|
||||
} else {
|
||||
// 동시 공격(both): 무기 이펙트 중앙 표시
|
||||
final effect = getWeaponEffect(weaponCategory);
|
||||
effectLines = _getEffectLines(effect, phase, subFrame);
|
||||
effectX = 23;
|
||||
}
|
||||
|
||||
if (effectLines.isEmpty) return null;
|
||||
|
||||
final cells = _spriteToCells(effectLines);
|
||||
|
||||
// 이펙트 위치: 캐릭터 오른쪽 (30% 위치 + 캐릭터 너비)
|
||||
final charX = switch (phase) {
|
||||
BattlePhase.attack => 18, // 30%
|
||||
BattlePhase.hit => 18, // 30%
|
||||
_ => 12,
|
||||
};
|
||||
final effectX = charX + 6; // 캐릭터 너비만큼 오른쪽
|
||||
// 캐릭터 3줄 기준, 머리 위치
|
||||
final effectY = frameHeight - 3 - 1;
|
||||
|
||||
@@ -204,6 +244,16 @@ class CanvasBattleComposer {
|
||||
);
|
||||
}
|
||||
|
||||
/// 몬스터 공격 이펙트 (← 방향)
|
||||
List<String> _getMonsterAttackEffect(BattlePhase phase, int subFrame) {
|
||||
return switch (phase) {
|
||||
BattlePhase.prepare => _monsterPrepareFrames[subFrame % _monsterPrepareFrames.length],
|
||||
BattlePhase.attack => _monsterAttackFrames[subFrame % _monsterAttackFrames.length],
|
||||
BattlePhase.hit => _monsterHitFrames[subFrame % _monsterHitFrames.length],
|
||||
_ => <String>[],
|
||||
};
|
||||
}
|
||||
|
||||
/// 문자열 스프라이트를 AsciiCell 2D 배열로 변환
|
||||
List<List<AsciiCell>> _spriteToCells(List<String> lines) {
|
||||
return lines.map((line) {
|
||||
@@ -1024,3 +1074,26 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 몬스터 공격 이펙트 (← 방향, Phase 8)
|
||||
// ============================================================================
|
||||
|
||||
/// 몬스터 공격 준비 프레임
|
||||
const _monsterPrepareFrames = <List<String>>[
|
||||
[r' ', r' <', r' '],
|
||||
[r' ', r' <<', r' '],
|
||||
];
|
||||
|
||||
/// 몬스터 공격 프레임
|
||||
const _monsterAttackFrames = <List<String>>[
|
||||
[r' ', r' <-- ', r' '],
|
||||
[r' ', r' <--- ', r' '],
|
||||
[r' ', r' <-----', r' '],
|
||||
];
|
||||
|
||||
/// 몬스터 히트 프레임
|
||||
const _monsterHitFrames = <List<String>>[
|
||||
[r' *SLASH*', r' <-----', r' '],
|
||||
[r'*ATTACK*', r' <----', r' '],
|
||||
];
|
||||
|
||||
@@ -19,6 +19,21 @@ enum BattlePhase {
|
||||
recover,
|
||||
}
|
||||
|
||||
/// 공격자 타입 (위치 계산용)
|
||||
enum AttackerType {
|
||||
/// 공격 없음 (idle 상태)
|
||||
none,
|
||||
|
||||
/// 플레이어가 공격
|
||||
player,
|
||||
|
||||
/// 몬스터가 공격
|
||||
monster,
|
||||
|
||||
/// 동시 공격 (양쪽 모두 이동)
|
||||
both,
|
||||
}
|
||||
|
||||
/// 캐릭터 프레임 데이터
|
||||
class CharacterFrame {
|
||||
const CharacterFrame(this.lines);
|
||||
|
||||
Reference in New Issue
Block a user