feat(animation): 아레나 전투 애니메이션 지원
- CanvasBattleComposer: 아레나 모드 지원 추가 - AsciiAnimationCard: 아레나 전투 애니메이션 렌더링
This commit is contained in:
@@ -14,6 +14,8 @@ import 'package:asciineverdie/src/core/model/item_stats.dart';
|
||||
///
|
||||
/// 기존 BattleComposer의 로직을 레이어 기반으로 변환.
|
||||
/// 출력: `List<AsciiLayer>` (z-order 정렬됨)
|
||||
///
|
||||
/// PvP 모드: [opponentRaceId]가 설정되면 몬스터 대신 상대 캐릭터(좌우 반전) 표시
|
||||
class CanvasBattleComposer {
|
||||
const CanvasBattleComposer({
|
||||
required this.weaponCategory,
|
||||
@@ -22,6 +24,8 @@ class CanvasBattleComposer {
|
||||
required this.monsterSize,
|
||||
this.raceId,
|
||||
this.weaponRarity,
|
||||
this.opponentRaceId,
|
||||
this.opponentHasShield = false,
|
||||
});
|
||||
|
||||
final WeaponCategory weaponCategory;
|
||||
@@ -35,6 +39,15 @@ class CanvasBattleComposer {
|
||||
/// 무기 희귀도 (Phase 9: 무기 등급별 이펙트 색상)
|
||||
final ItemRarity? weaponRarity;
|
||||
|
||||
/// 상대 종족 ID (PvP 모드: 설정 시 몬스터 대신 캐릭터 표시)
|
||||
final String? opponentRaceId;
|
||||
|
||||
/// 상대 방패 장착 여부 (PvP 모드)
|
||||
final bool opponentHasShield;
|
||||
|
||||
/// PvP 모드 여부
|
||||
bool get isPvP => opponentRaceId != null;
|
||||
|
||||
/// 프레임 상수
|
||||
static const int frameWidth = 60;
|
||||
static const int frameHeight = 8;
|
||||
@@ -59,7 +72,11 @@ class CanvasBattleComposer {
|
||||
final layers = <AsciiLayer>[
|
||||
_createBackgroundLayer(environment, globalTick),
|
||||
_createCharacterLayer(phase, subFrame, attacker),
|
||||
_createMonsterLayer(phase, subFrame, attacker),
|
||||
// PvP 모드: 몬스터 대신 상대 캐릭터(좌우 반전) 표시
|
||||
if (isPvP)
|
||||
_createOpponentCharacterLayer(phase, subFrame, attacker)
|
||||
else
|
||||
_createMonsterLayer(phase, subFrame, attacker),
|
||||
];
|
||||
|
||||
// 이펙트 레이어 (준비/공격/히트 페이즈에서, 공격자 있을 때)
|
||||
@@ -246,6 +263,91 @@ class CanvasBattleComposer {
|
||||
);
|
||||
}
|
||||
|
||||
/// 상대 캐릭터 레이어 생성 (PvP 모드, z=1)
|
||||
///
|
||||
/// 몬스터 대신 상대 캐릭터를 좌우 반전하여 표시
|
||||
AsciiLayer _createOpponentCharacterLayer(
|
||||
BattlePhase phase,
|
||||
int subFrame,
|
||||
AttackerType attacker,
|
||||
) {
|
||||
// 상대 종족별 프레임 조회
|
||||
CharacterFrame opponentFrame;
|
||||
if (opponentRaceId != null && opponentRaceId!.isNotEmpty) {
|
||||
final raceData = RaceCharacterFrames.get(opponentRaceId!);
|
||||
if (raceData != null) {
|
||||
final frames = raceData.getFrames(phase);
|
||||
opponentFrame = frames[subFrame % frames.length];
|
||||
} else {
|
||||
opponentFrame = getCharacterFrame(phase, subFrame);
|
||||
}
|
||||
} else {
|
||||
opponentFrame = getCharacterFrame(phase, subFrame);
|
||||
}
|
||||
|
||||
if (opponentHasShield) {
|
||||
opponentFrame = opponentFrame.withShield();
|
||||
}
|
||||
|
||||
// 좌우 반전
|
||||
final mirroredLines = _mirrorLines(opponentFrame.lines);
|
||||
|
||||
// 상대가 공격자인지 확인 (몬스터 역할)
|
||||
final isOpponentAttacking =
|
||||
attacker == AttackerType.monster || attacker == AttackerType.both;
|
||||
|
||||
// 페이즈별 X 위치 (몬스터와 동일하지만 캐릭터 너비 기준)
|
||||
const opponentWidth = 6;
|
||||
final opponentRightEdge = switch (phase) {
|
||||
BattlePhase.idle => 48,
|
||||
BattlePhase.prepare => isOpponentAttacking ? 45 : 48,
|
||||
BattlePhase.attack => isOpponentAttacking ? 42 : 48,
|
||||
BattlePhase.hit => isOpponentAttacking ? 42 : 48,
|
||||
BattlePhase.recover => isOpponentAttacking ? 45 : 48,
|
||||
};
|
||||
final opponentX = opponentRightEdge - opponentWidth;
|
||||
|
||||
final cells = _spriteToCells(mirroredLines);
|
||||
final opponentY = frameHeight - cells.length - 1;
|
||||
|
||||
return AsciiLayer(
|
||||
cells: cells,
|
||||
zIndex: 1,
|
||||
offsetX: opponentX,
|
||||
offsetY: opponentY,
|
||||
);
|
||||
}
|
||||
|
||||
/// 문자열 좌우 반전 (PvP 모드용)
|
||||
List<String> _mirrorLines(List<String> lines) {
|
||||
return lines.map((line) {
|
||||
final chars = line.split('');
|
||||
final mirrored = chars.reversed.map(_mirrorChar).toList();
|
||||
return mirrored.join();
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// 개별 문자 미러링
|
||||
String _mirrorChar(String char) {
|
||||
return switch (char) {
|
||||
'/' => r'\',
|
||||
r'\' => '/',
|
||||
'(' => ')',
|
||||
')' => '(',
|
||||
'[' => ']',
|
||||
']' => '[',
|
||||
'{' => '}',
|
||||
'}' => '{',
|
||||
'<' => '>',
|
||||
'>' => '<',
|
||||
'┘' => '└',
|
||||
'└' => '┘',
|
||||
'┐' => '┌',
|
||||
'┌' => '┐',
|
||||
_ => char,
|
||||
};
|
||||
}
|
||||
|
||||
/// 이펙트 레이어 생성 (z=3, 캐릭터/몬스터 위에 표시)
|
||||
///
|
||||
/// Phase 8: 공격자에 따라 이펙트 위치/모양 분리
|
||||
|
||||
Reference in New Issue
Block a user