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,
|
int subFrame,
|
||||||
String? monsterBaseName,
|
String? monsterBaseName,
|
||||||
EnvironmentType environment,
|
EnvironmentType environment,
|
||||||
int globalTick,
|
int globalTick, {
|
||||||
) {
|
AttackerType attacker = AttackerType.none,
|
||||||
|
}) {
|
||||||
final layers = <AsciiLayer>[
|
final layers = <AsciiLayer>[
|
||||||
_createBackgroundLayer(environment, globalTick),
|
_createBackgroundLayer(environment, globalTick),
|
||||||
_createCharacterLayer(phase, subFrame),
|
_createCharacterLayer(phase, subFrame, attacker),
|
||||||
_createMonsterLayer(phase, subFrame),
|
_createMonsterLayer(phase, subFrame, attacker),
|
||||||
];
|
];
|
||||||
|
|
||||||
// 이펙트 레이어 (공격/히트 페이즈에서만)
|
// 이펙트 레이어 (공격/히트 페이즈에서만, 공격자 있을 때)
|
||||||
if (phase == BattlePhase.attack || phase == BattlePhase.hit) {
|
if ((phase == BattlePhase.attack || phase == BattlePhase.hit) &&
|
||||||
final effectLayer = _createEffectLayer(phase, subFrame);
|
attacker != AttackerType.none) {
|
||||||
|
final effectLayer = _createEffectLayer(phase, subFrame, attacker);
|
||||||
if (effectLayer != null) {
|
if (effectLayer != null) {
|
||||||
layers.add(effectLayer);
|
layers.add(effectLayer);
|
||||||
}
|
}
|
||||||
@@ -101,7 +103,12 @@ class CanvasBattleComposer {
|
|||||||
/// 캐릭터 레이어 생성 (z=1)
|
/// 캐릭터 레이어 생성 (z=1)
|
||||||
///
|
///
|
||||||
/// Phase 4: 종족별 캐릭터 프레임 지원
|
/// Phase 4: 종족별 캐릭터 프레임 지원
|
||||||
AsciiLayer _createCharacterLayer(BattlePhase phase, int subFrame) {
|
/// Phase 7: 공격자별 위치 분리 (플레이어가 공격자일 때만 이동)
|
||||||
|
AsciiLayer _createCharacterLayer(
|
||||||
|
BattlePhase phase,
|
||||||
|
int subFrame,
|
||||||
|
AttackerType attacker,
|
||||||
|
) {
|
||||||
CharacterFrame charFrame;
|
CharacterFrame charFrame;
|
||||||
|
|
||||||
// 종족 ID가 있으면 종족별 프레임 사용
|
// 종족 ID가 있으면 종족별 프레임 사용
|
||||||
@@ -122,13 +129,18 @@ class CanvasBattleComposer {
|
|||||||
charFrame = charFrame.withShield();
|
charFrame = charFrame.withShield();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 페이즈별 X 위치 (idle: 20%, attack: 30%)
|
// 플레이어가 공격자인지 확인
|
||||||
|
final isPlayerAttacking =
|
||||||
|
attacker == AttackerType.player || attacker == AttackerType.both;
|
||||||
|
|
||||||
|
// 페이즈별 X 위치 (Phase 7: 공격자별 위치 분리)
|
||||||
|
// 플레이어가 공격자일 때만 이동, 아니면 제자리(12)
|
||||||
final charX = switch (phase) {
|
final charX = switch (phase) {
|
||||||
BattlePhase.idle => 12, // 20%
|
BattlePhase.idle => 12,
|
||||||
BattlePhase.prepare => 15, // 전환중
|
BattlePhase.prepare => isPlayerAttacking ? 15 : 12,
|
||||||
BattlePhase.attack => 18, // 30%
|
BattlePhase.attack => isPlayerAttacking ? 18 : 12,
|
||||||
BattlePhase.hit => 18, // 30%
|
BattlePhase.hit => attacker == AttackerType.both ? 18 : 12,
|
||||||
BattlePhase.recover => 15, // 전환중
|
BattlePhase.recover => isPlayerAttacking ? 15 : 12,
|
||||||
};
|
};
|
||||||
|
|
||||||
final cells = _spriteToCells(charFrame.lines);
|
final cells = _spriteToCells(charFrame.lines);
|
||||||
@@ -144,7 +156,13 @@ class CanvasBattleComposer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 몬스터 레이어 생성 (z=1, 캐릭터보다 뒤)
|
/// 몬스터 레이어 생성 (z=1, 캐릭터보다 뒤)
|
||||||
AsciiLayer _createMonsterLayer(BattlePhase phase, int subFrame) {
|
///
|
||||||
|
/// Phase 7: 공격자별 위치 분리 (몬스터가 공격자일 때만 이동)
|
||||||
|
AsciiLayer _createMonsterLayer(
|
||||||
|
BattlePhase phase,
|
||||||
|
int subFrame,
|
||||||
|
AttackerType attacker,
|
||||||
|
) {
|
||||||
final monsterFrames = _getAnimatedMonsterFrames(
|
final monsterFrames = _getAnimatedMonsterFrames(
|
||||||
monsterCategory,
|
monsterCategory,
|
||||||
monsterSize,
|
monsterSize,
|
||||||
@@ -155,14 +173,18 @@ class CanvasBattleComposer {
|
|||||||
// 몬스터 스프라이트를 오른쪽 정렬하여 셀로 변환
|
// 몬스터 스프라이트를 오른쪽 정렬하여 셀로 변환
|
||||||
final cells = _spriteToRightAlignedCells(monsterFrame, monsterWidth);
|
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) {
|
final monsterRightEdge = switch (phase) {
|
||||||
BattlePhase.idle => 48, // 80%
|
BattlePhase.idle => 48,
|
||||||
BattlePhase.prepare => 45, // 전환중
|
BattlePhase.prepare => isMonsterAttacking ? 45 : 48,
|
||||||
BattlePhase.attack => 42, // 70%
|
BattlePhase.attack => attacker == AttackerType.both ? 42 : 48,
|
||||||
BattlePhase.hit => 42, // 70%
|
BattlePhase.hit => isMonsterAttacking ? 42 : 48,
|
||||||
BattlePhase.recover => 45, // 전환중
|
BattlePhase.recover => isMonsterAttacking ? 45 : 48,
|
||||||
};
|
};
|
||||||
final monsterX = monsterRightEdge - monsterWidth;
|
final monsterX = monsterRightEdge - monsterWidth;
|
||||||
|
|
||||||
@@ -178,21 +200,39 @@ class CanvasBattleComposer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 이펙트 레이어 생성 (z=3, 캐릭터/몬스터 위에 표시)
|
/// 이펙트 레이어 생성 (z=3, 캐릭터/몬스터 위에 표시)
|
||||||
AsciiLayer? _createEffectLayer(BattlePhase phase, int subFrame) {
|
///
|
||||||
final effect = getWeaponEffect(weaponCategory);
|
/// Phase 8: 공격자에 따라 이펙트 위치/모양 분리
|
||||||
final effectLines = _getEffectLines(effect, phase, subFrame);
|
/// - 플레이어 공격: 몬스터 왼쪽에 무기 이펙트 (→ 방향)
|
||||||
|
/// - 몬스터 공격: 캐릭터 오른쪽에 공격 이펙트 (← 방향)
|
||||||
|
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;
|
if (effectLines.isEmpty) return null;
|
||||||
|
|
||||||
final cells = _spriteToCells(effectLines);
|
final cells = _spriteToCells(effectLines);
|
||||||
|
|
||||||
// 이펙트 위치: 캐릭터 오른쪽 (30% 위치 + 캐릭터 너비)
|
|
||||||
final charX = switch (phase) {
|
|
||||||
BattlePhase.attack => 18, // 30%
|
|
||||||
BattlePhase.hit => 18, // 30%
|
|
||||||
_ => 12,
|
|
||||||
};
|
|
||||||
final effectX = charX + 6; // 캐릭터 너비만큼 오른쪽
|
|
||||||
// 캐릭터 3줄 기준, 머리 위치
|
// 캐릭터 3줄 기준, 머리 위치
|
||||||
final effectY = frameHeight - 3 - 1;
|
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 배열로 변환
|
/// 문자열 스프라이트를 AsciiCell 2D 배열로 변환
|
||||||
List<List<AsciiCell>> _spriteToCells(List<String> lines) {
|
List<List<AsciiCell>> _spriteToCells(List<String> lines) {
|
||||||
return lines.map((line) {
|
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,
|
recover,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 공격자 타입 (위치 계산용)
|
||||||
|
enum AttackerType {
|
||||||
|
/// 공격 없음 (idle 상태)
|
||||||
|
none,
|
||||||
|
|
||||||
|
/// 플레이어가 공격
|
||||||
|
player,
|
||||||
|
|
||||||
|
/// 몬스터가 공격
|
||||||
|
monster,
|
||||||
|
|
||||||
|
/// 동시 공격 (양쪽 모두 이동)
|
||||||
|
both,
|
||||||
|
}
|
||||||
|
|
||||||
/// 캐릭터 프레임 데이터
|
/// 캐릭터 프레임 데이터
|
||||||
class CharacterFrame {
|
class CharacterFrame {
|
||||||
const CharacterFrame(this.lines);
|
const CharacterFrame(this.lines);
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
int _eventDrivenPhaseFrames = 0;
|
int _eventDrivenPhaseFrames = 0;
|
||||||
bool _isEventDrivenPhase = false;
|
bool _isEventDrivenPhase = false;
|
||||||
|
|
||||||
|
// 공격자 타입 (Phase 7: 공격자별 위치 분리)
|
||||||
|
AttackerType _currentAttacker = AttackerType.none;
|
||||||
|
|
||||||
// 특수 애니메이션 프레임 수는 ascii_animation_type.dart의
|
// 특수 애니메이션 프레임 수는 ascii_animation_type.dart의
|
||||||
// specialAnimationFrameCounts 상수 사용
|
// specialAnimationFrameCounts 상수 사용
|
||||||
|
|
||||||
@@ -331,6 +334,14 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
} else {
|
} else {
|
||||||
_isEventDrivenPhase = false;
|
_isEventDrivenPhase = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 공격자 타입 결정 (Phase 7: 공격자별 위치 분리)
|
||||||
|
_currentAttacker = switch (event.type) {
|
||||||
|
CombatEventType.playerAttack ||
|
||||||
|
CombatEventType.playerSkill => AttackerType.player,
|
||||||
|
CombatEventType.monsterAttack => AttackerType.monster,
|
||||||
|
_ => AttackerType.none,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,6 +476,8 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
_showSkillEffect = false;
|
_showSkillEffect = false;
|
||||||
// 이벤트 기반 페이즈 종료
|
// 이벤트 기반 페이즈 종료
|
||||||
_isEventDrivenPhase = false;
|
_isEventDrivenPhase = false;
|
||||||
|
// 공격자 타입 리셋 (Phase 7)
|
||||||
|
_currentAttacker = AttackerType.none;
|
||||||
} else {
|
} else {
|
||||||
_battleSubFrame++;
|
_battleSubFrame++;
|
||||||
}
|
}
|
||||||
@@ -488,6 +501,7 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
widget.monsterBaseName,
|
widget.monsterBaseName,
|
||||||
_environment,
|
_environment,
|
||||||
_globalTick,
|
_globalTick,
|
||||||
|
attacker: _currentAttacker,
|
||||||
) ??
|
) ??
|
||||||
[AsciiLayer.empty()],
|
[AsciiLayer.empty()],
|
||||||
AnimationMode.walking =>
|
AnimationMode.walking =>
|
||||||
|
|||||||
Reference in New Issue
Block a user