feat(ui): HP/MP 바 개선 및 전투 시스템 UI 업데이트
- HP/MP 변화 시 플래시 효과 및 변화량 표시 추가 - 전투 중 몬스터 HP 바 표시 기능 추가 - 몬스터 HP 바 Row 오버플로우 버그 수정 (Flexible 적용) - 전투 상태 및 이벤트 모델 개선 - 캐릭터 애니메이션 및 전투 컴포저 업데이트
This commit is contained in:
@@ -93,22 +93,29 @@ class BattleComposer {
|
||||
_overlaySpriteWithSpaces(canvas, normalizedChar, charX, charY);
|
||||
|
||||
// 4. 몬스터 프레임 (정규화하여 오른쪽 정렬)
|
||||
// idle 프레임 기준 너비로 정렬하여 hit/alert 시 위치 이동 방지
|
||||
final monsterRefWidth = _getMonsterReferenceWidth(monsterCategory, monsterSize);
|
||||
final monsterFrames =
|
||||
_getAnimatedMonsterFrames(monsterCategory, monsterSize, phase);
|
||||
final monsterFrame = monsterFrames[subFrame % monsterFrames.length];
|
||||
final normalizedMonster = _normalizeSpriteRight(monsterFrame, monsterWidth);
|
||||
final normalizedMonster = _normalizeSpriteRight(
|
||||
monsterFrame,
|
||||
monsterWidth,
|
||||
referenceWidth: monsterRefWidth,
|
||||
);
|
||||
final monsterX = frameWidth - monsterWidth;
|
||||
// 바닥 레이어(Y=7) 위에 서있도록 -1
|
||||
final monsterY = frameHeight - normalizedMonster.length - 1;
|
||||
_overlaySpriteWithSpaces(canvas, normalizedMonster, monsterX, monsterY);
|
||||
// 몬스터는 경계 내 완전 렌더링 (내부 공백에 배경이 비치지 않도록)
|
||||
_overlaySpriteWithBounds(canvas, normalizedMonster, monsterX, monsterY);
|
||||
|
||||
// 5. 멀티라인 이펙트 오버레이 (공격/히트 페이즈)
|
||||
if (phase == BattlePhase.attack || phase == BattlePhase.hit) {
|
||||
final effect = getWeaponEffect(weaponCategory);
|
||||
final effectLines = _getEffectLines(effect, phase, subFrame);
|
||||
if (effectLines.isNotEmpty) {
|
||||
// 이펙트 Y 위치: 캐릭터 팔 높이 (2번째 줄, 몸통) 기준
|
||||
final effectY = charY + 1;
|
||||
// 이펙트 Y 위치: 캐릭터 머리 높이 (1번째 줄) 기준 - 수정됨
|
||||
final effectY = charY;
|
||||
// 이펙트 X 위치: 캐릭터 오른쪽에 붙여서 표시
|
||||
final effectX = charX + 6;
|
||||
for (var i = 0; i < effectLines.length; i++) {
|
||||
@@ -129,16 +136,64 @@ class BattleComposer {
|
||||
return sprite.map((line) => line.padRight(width).substring(0, width)).toList();
|
||||
}
|
||||
|
||||
/// 스프라이트를 지정 폭으로 정규화 (오른쪽 정렬)
|
||||
List<String> _normalizeSpriteRight(List<String> sprite, int width) {
|
||||
return sprite.map((line) {
|
||||
final trimmed = line.trimRight();
|
||||
if (trimmed.length >= width) return trimmed.substring(0, width);
|
||||
return trimmed.padLeft(width);
|
||||
/// 스프라이트를 지정 폭으로 정규화 (오른쪽 정렬, 전체 스프라이트 기준)
|
||||
///
|
||||
/// 모든 줄을 동일한 기준점에서 오른쪽 정렬하여
|
||||
/// 머리와 몸통이 분리되지 않도록 함
|
||||
///
|
||||
/// [referenceWidth] 지정 시 해당 너비를 기준으로 정렬 (idle/hit 프레임 일관성용)
|
||||
List<String> _normalizeSpriteRight(
|
||||
List<String> sprite,
|
||||
int width, {
|
||||
int? referenceWidth,
|
||||
}) {
|
||||
// 1. 각 줄의 실제 너비(오른쪽 공백 제외) 계산
|
||||
final trimmedLines = sprite.map((line) => line.trimRight()).toList();
|
||||
|
||||
// 2. 기준 너비 결정 (referenceWidth 있으면 사용, 없으면 현재 스프라이트 기준)
|
||||
int maxLineWidth;
|
||||
if (referenceWidth != null) {
|
||||
maxLineWidth = referenceWidth;
|
||||
} else {
|
||||
maxLineWidth = 0;
|
||||
for (final line in trimmedLines) {
|
||||
if (line.length > maxLineWidth) {
|
||||
maxLineWidth = line.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 전체 스프라이트를 오른쪽 정렬 (width 기준)
|
||||
// 모든 줄에 동일한 왼쪽 패딩 적용
|
||||
final leftPadding = width - maxLineWidth;
|
||||
final paddingStr = leftPadding > 0 ? ' ' * leftPadding : '';
|
||||
|
||||
return trimmedLines.map((line) {
|
||||
// 각 줄을 왼쪽에 공통 패딩 추가 후 width로 자르기
|
||||
final paddedLine = paddingStr + line;
|
||||
if (paddedLine.length > width) {
|
||||
return paddedLine.substring(paddedLine.length - width);
|
||||
}
|
||||
return paddedLine.padRight(width);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// 스프라이트를 캔버스에 오버레이 (공백도 덮어쓰기 - Z-order용)
|
||||
/// 몬스터 스프라이트의 기준 너비 계산 (idle 프레임 기준)
|
||||
int _getMonsterReferenceWidth(MonsterCategory category, MonsterSize size) {
|
||||
final idleFrames = _getMonsterIdleFrames(category, size);
|
||||
int maxWidth = 0;
|
||||
for (final frame in idleFrames) {
|
||||
for (final line in frame) {
|
||||
final trimmedLength = line.trimRight().length;
|
||||
if (trimmedLength > maxWidth) {
|
||||
maxWidth = trimmedLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxWidth;
|
||||
}
|
||||
|
||||
/// 스프라이트를 캔버스에 오버레이 (공백은 투명 처리)
|
||||
void _overlaySpriteWithSpaces(
|
||||
List<List<String>> canvas,
|
||||
List<String> sprite,
|
||||
@@ -163,6 +218,43 @@ class BattleComposer {
|
||||
}
|
||||
}
|
||||
|
||||
/// 스프라이트를 캔버스에 오버레이 (라인별 경계 내 완전 렌더링)
|
||||
///
|
||||
/// 각 라인에서 첫 번째와 마지막 비공백 문자 사이의 모든 문자를 그림.
|
||||
/// 내부 공백도 그려져서 스크롤링 배경이 비치지 않음.
|
||||
void _overlaySpriteWithBounds(
|
||||
List<List<String>> canvas,
|
||||
List<String> sprite,
|
||||
int startX,
|
||||
int startY,
|
||||
) {
|
||||
for (var i = 0; i < sprite.length; i++) {
|
||||
final y = startY + i;
|
||||
if (y < 0 || y >= frameHeight) continue;
|
||||
|
||||
final line = sprite[i];
|
||||
|
||||
// 각 라인에서 첫/마지막 비공백 문자 위치 찾기
|
||||
int firstNonSpace = -1;
|
||||
int lastNonSpace = -1;
|
||||
for (var j = 0; j < line.length; j++) {
|
||||
if (line[j] != ' ') {
|
||||
if (firstNonSpace == -1) firstNonSpace = j;
|
||||
lastNonSpace = j;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstNonSpace == -1) continue; // 빈 라인
|
||||
|
||||
// 경계 내 모든 문자 그리기 (공백 포함)
|
||||
for (var j = firstNonSpace; j <= lastNonSpace; j++) {
|
||||
final x = startX + j;
|
||||
if (x < 0 || x >= frameWidth) continue;
|
||||
canvas[y][x] = line[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 배경 레이어를 캔버스에 그리기
|
||||
void _drawBackgroundLayer(
|
||||
List<List<String>> canvas,
|
||||
|
||||
@@ -108,7 +108,8 @@ const _prepareFrames = [
|
||||
|
||||
// ============================================================================
|
||||
// 공격 프레임 (전진 + 휘두르기) - 5프레임, 심플 3줄 스타일
|
||||
// 구조: [머리, 몸통+팔+무기, 다리]
|
||||
// 구조: [머리+공격, 몸통+팔, 다리]
|
||||
// 수정: 공격 이펙트를 머리 줄로 통일 (1칸 위로)
|
||||
// ============================================================================
|
||||
const _attackFrames = [
|
||||
CharacterFrame([
|
||||
@@ -122,13 +123,13 @@ const _attackFrames = [
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|-- ',
|
||||
r' o-- ',
|
||||
r' /| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|-=>',
|
||||
r' o-=>',
|
||||
r' /| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
@@ -140,22 +141,23 @@ const _attackFrames = [
|
||||
|
||||
// ============================================================================
|
||||
// 히트 프레임 (공격 명중) - 3프레임, 심플 3줄 스타일
|
||||
// 구조: [머리, 몸통+팔+이펙트, 다리]
|
||||
// 구조: [머리+이펙트, 몸통+팔, 다리]
|
||||
// 수정: 히트 이펙트를 머리 줄로 통일 (1칸 위로)
|
||||
// ============================================================================
|
||||
const _hitFrames = [
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|-* ',
|
||||
r' o-* ',
|
||||
r' /| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|=* ',
|
||||
r' o=* ',
|
||||
r' /| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|~* ',
|
||||
r' o~* ',
|
||||
r' /| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user