From 8f351df0b60189b6945a57020f4698ea5b994342 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Mon, 23 Feb 2026 15:49:14 +0900 Subject: [PATCH] =?UTF-8?q?refactor(shared):=20animation,=20l10n,=20theme?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=EC=9D=84=20core=EC=97=90=EC=84=9C=20share?= =?UTF-8?q?d=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - core/animation → shared/animation - core/l10n → shared/l10n - core/constants/ascii_colors → shared/theme/ascii_colors - import 경로 업데이트 --- .../canvas/canvas_battle_composer.dart | 1475 ----------------- .../animation/ascii_animation_data.dart | 2 +- .../animation/ascii_animation_type.dart | 0 .../animation/background_data.dart | 2 +- .../animation/background_layer.dart | 0 .../canvas/ascii_canvas_painter.dart | 6 +- .../animation/canvas/ascii_canvas_widget.dart | 6 +- .../animation/canvas/ascii_cell.dart | 0 .../animation/canvas/ascii_layer.dart | 2 +- .../canvas/canvas_battle_composer.dart | 544 ++++++ .../canvas/canvas_special_composer.dart | 6 +- .../canvas/canvas_town_composer.dart | 6 +- .../canvas/canvas_walking_composer.dart | 10 +- .../animation/canvas/combat_text_frames.dart | 77 + .../animation/canvas/monster_frames.dart | 765 +++++++++ .../animation/canvas/rarity_color_mapper.dart | 2 +- .../animation/character_frames.dart | 0 .../animation/front_screen_animation.dart | 0 .../animation/monster_size.dart | 0 .../animation/race_character_frames.dart | 2 +- .../animation/weapon_category.dart | 0 .../animation/weapon_effects.dart | 2 +- .../{core => shared}/l10n/game_data_l10n.dart | 0 .../theme}/ascii_colors.dart | 0 24 files changed, 1409 insertions(+), 1498 deletions(-) delete mode 100644 lib/src/core/animation/canvas/canvas_battle_composer.dart rename lib/src/{core => shared}/animation/ascii_animation_data.dart (99%) rename lib/src/{core => shared}/animation/ascii_animation_type.dart (100%) rename lib/src/{core => shared}/animation/background_data.dart (98%) rename lib/src/{core => shared}/animation/background_layer.dart (100%) rename lib/src/{core => shared}/animation/canvas/ascii_canvas_painter.dart (97%) rename lib/src/{core => shared}/animation/canvas/ascii_canvas_widget.dart (91%) rename lib/src/{core => shared}/animation/canvas/ascii_cell.dart (100%) rename lib/src/{core => shared}/animation/canvas/ascii_layer.dart (95%) create mode 100644 lib/src/shared/animation/canvas/canvas_battle_composer.dart rename lib/src/{core => shared}/animation/canvas/canvas_special_composer.dart (96%) rename lib/src/{core => shared}/animation/canvas/canvas_town_composer.dart (93%) rename lib/src/{core => shared}/animation/canvas/canvas_walking_composer.dart (91%) create mode 100644 lib/src/shared/animation/canvas/combat_text_frames.dart create mode 100644 lib/src/shared/animation/canvas/monster_frames.dart rename lib/src/{core => shared}/animation/canvas/rarity_color_mapper.dart (91%) rename lib/src/{core => shared}/animation/character_frames.dart (100%) rename lib/src/{core => shared}/animation/front_screen_animation.dart (100%) rename lib/src/{core => shared}/animation/monster_size.dart (100%) rename lib/src/{core => shared}/animation/race_character_frames.dart (99%) rename lib/src/{core => shared}/animation/weapon_category.dart (100%) rename lib/src/{core => shared}/animation/weapon_effects.dart (98%) rename lib/src/{core => shared}/l10n/game_data_l10n.dart (100%) rename lib/src/{core/constants => shared/theme}/ascii_colors.dart (100%) diff --git a/lib/src/core/animation/canvas/canvas_battle_composer.dart b/lib/src/core/animation/canvas/canvas_battle_composer.dart deleted file mode 100644 index 3647db8..0000000 --- a/lib/src/core/animation/canvas/canvas_battle_composer.dart +++ /dev/null @@ -1,1475 +0,0 @@ -import 'package:asciineverdie/src/core/animation/ascii_animation_data.dart'; -import 'package:asciineverdie/src/core/animation/background_data.dart'; -import 'package:asciineverdie/src/core/animation/background_layer.dart'; -import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart'; -import 'package:asciineverdie/src/core/animation/canvas/ascii_layer.dart'; -import 'package:asciineverdie/src/core/animation/character_frames.dart'; -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/weapon_category.dart'; -import 'package:asciineverdie/src/core/animation/weapon_effects.dart'; -import 'package:asciineverdie/src/core/animation/canvas/rarity_color_mapper.dart'; -import 'package:asciineverdie/src/core/model/item_stats.dart'; - -/// Canvas용 전투 프레임 합성기 -/// -/// 기존 BattleComposer의 로직을 레이어 기반으로 변환. -/// 출력: `List` (z-order 정렬됨) -/// -/// PvP 모드: [opponentRaceId]가 설정되면 몬스터 대신 상대 캐릭터(좌우 반전) 표시 -class CanvasBattleComposer { - const CanvasBattleComposer({ - required this.weaponCategory, - required this.hasShield, - required this.monsterCategory, - required this.monsterSize, - this.raceId, - this.weaponRarity, - this.opponentRaceId, - this.opponentHasShield = false, - }); - - final WeaponCategory weaponCategory; - final bool hasShield; - final MonsterCategory monsterCategory; - final MonsterSize monsterSize; - - /// 종족 ID (Phase 4: 종족별 캐릭터 애니메이션) - final String? raceId; - - /// 무기 희귀도 (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; - static const int monsterWidth = 18; - - /// 레이어 기반 프레임 생성 - List composeLayers( - BattlePhase phase, - int subFrame, - String? monsterBaseName, - EnvironmentType environment, - int globalTick, { - 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, - bool hideMonster = false, - }) { - final layers = [ - _createBackgroundLayer(environment, globalTick), - _createCharacterLayer(phase, subFrame, attacker), - // PvP 모드: 몬스터 대신 상대 캐릭터(좌우 반전) 표시 - // hideMonster: 몬스터 사망 애니메이션 중에는 렌더링 안함 - if (!hideMonster) - isPvP - ? _createOpponentCharacterLayer(phase, subFrame, attacker) - : _createMonsterLayer(phase, subFrame, attacker), - ]; - - // 이펙트 레이어 (준비/공격/히트 페이즈에서, 공격자 있을 때) - if ((phase == BattlePhase.prepare || - phase == BattlePhase.attack || - phase == BattlePhase.hit) && - attacker != AttackerType.none) { - final effectLayer = _createEffectLayer(phase, subFrame, attacker); - if (effectLayer != null) { - layers.add(effectLayer); - } - } - - // 텍스트 이펙트 레이어 (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 정렬 - layers.sort((a, b) => a.zIndex.compareTo(b.zIndex)); - - return layers; - } - - /// 배경 레이어 생성 (z=0) - AsciiLayer _createBackgroundLayer( - EnvironmentType environment, - int globalTick, - ) { - final cells = List.generate( - frameHeight, - (_) => List.filled(frameWidth, AsciiCell.empty), - ); - - final bgLayers = getBackgroundLayers(environment); - for (final layer in bgLayers) { - // 스크롤 오프셋 계산 - final offset = (globalTick * layer.scrollSpeed).toInt(); - - for (var i = 0; i < layer.lines.length; i++) { - final y = layer.yStart + i; - if (y >= frameHeight) break; - - final pattern = layer.lines[i]; - if (pattern.isEmpty) continue; - - for (var x = 0; x < frameWidth; x++) { - final patternIdx = (x + offset) % pattern.length; - final char = pattern[patternIdx]; - if (char != ' ') { - cells[y][x] = AsciiCell.fromChar(char); - } - } - } - } - - // 배경 레이어는 50% 투명으로 캐릭터 부각 - return AsciiLayer(cells: cells, zIndex: 0, opacity: 0.5); - } - - /// 캐릭터 레이어 생성 (z=1) - /// - /// Phase 4: 종족별 캐릭터 프레임 지원 - /// Phase 7: 공격자별 위치 분리 (플레이어가 공격자일 때만 이동) - AsciiLayer _createCharacterLayer( - BattlePhase phase, - int subFrame, - AttackerType attacker, - ) { - CharacterFrame charFrame; - - // 종족 ID가 있으면 종족별 프레임 사용 - if (raceId != null && raceId!.isNotEmpty) { - final raceData = RaceCharacterFrames.get(raceId!); - if (raceData != null) { - final frames = raceData.getFrames(phase); - charFrame = frames[subFrame % frames.length]; - } else { - // 종족 데이터 없으면 기본 프레임 사용 - charFrame = getCharacterFrame(phase, subFrame); - } - } else { - charFrame = getCharacterFrame(phase, subFrame); - } - - if (hasShield) { - charFrame = charFrame.withShield(); - } - - // 플레이어가 공격자인지 확인 - final isPlayerAttacking = - attacker == AttackerType.player || attacker == AttackerType.both; - - // 페이즈별 X 위치 (Phase 7: 공격자별 위치 분리) - // 플레이어가 공격자일 때만 이동, 아니면 제자리(12) - // hit 페이즈에서도 플레이어 공격 중이면 위치 유지 (Bug fix) - final charX = switch (phase) { - BattlePhase.idle => 12, - BattlePhase.prepare => isPlayerAttacking ? 15 : 12, - BattlePhase.attack => isPlayerAttacking ? 18 : 12, - BattlePhase.hit => isPlayerAttacking ? 18 : 12, - BattlePhase.recover => isPlayerAttacking ? 15 : 12, - }; - - final cells = _spriteToCells(charFrame.lines); - // 바닥 레이어(Y=7) 위에 서있도록 - final charY = frameHeight - cells.length - 1; - - return AsciiLayer( - cells: cells, - zIndex: 2, // 몬스터(z=1) 위에 캐릭터 표시 - offsetX: charX, - offsetY: charY, - ); - } - - /// 몬스터 레이어 생성 (z=1, 캐릭터보다 뒤) - /// - /// Phase 7: 공격자별 위치 분리 (몬스터가 공격자일 때만 이동) - AsciiLayer _createMonsterLayer( - BattlePhase phase, - int subFrame, - AttackerType attacker, - ) { - final monsterFrames = _getAnimatedMonsterFrames( - monsterCategory, - monsterSize, - phase, - ); - final monsterFrame = monsterFrames[subFrame % monsterFrames.length]; - - // 몬스터 스프라이트를 오른쪽 정렬하여 셀로 변환 - final cells = _spriteToRightAlignedCells(monsterFrame, monsterWidth); - - // 몬스터가 공격자인지 확인 - final isMonsterAttacking = - attacker == AttackerType.monster || attacker == AttackerType.both; - - // 페이즈별 X 위치 (Phase 7: 공격자별 위치 분리) - // 몬스터가 공격자일 때만 이동, 아니면 제자리(48) - // attack 페이즈에서도 몬스터 공격 중이면 위치 유지 (Bug fix) - final monsterRightEdge = switch (phase) { - BattlePhase.idle => 48, - BattlePhase.prepare => isMonsterAttacking ? 45 : 48, - BattlePhase.attack => isMonsterAttacking ? 42 : 48, - BattlePhase.hit => isMonsterAttacking ? 42 : 48, - BattlePhase.recover => isMonsterAttacking ? 45 : 48, - }; - final monsterX = monsterRightEdge - monsterWidth; - - // 바닥 레이어(Y=7) 위에 서있도록 - final monsterY = frameHeight - cells.length - 1; - - return AsciiLayer( - cells: cells, - zIndex: 1, - offsetX: monsterX, - offsetY: monsterY, - ); - } - - /// 상대 캐릭터 레이어 생성 (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 _mirrorLines(List 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: 공격자에 따라 이펙트 위치/모양 분리 - /// - 플레이어 공격: 몬스터 왼쪽에 무기 이펙트 (→ 방향) - /// - 몬스터 공격: 캐릭터 오른쪽에 공격 이펙트 (← 방향) - AsciiLayer? _createEffectLayer( - BattlePhase phase, - int subFrame, - AttackerType attacker, - ) { - // 공격자별 위치 계산을 위한 플래그 (동일 로직 재사용) - final isPlayerAttacking = - attacker == AttackerType.player || attacker == AttackerType.both; - final isMonsterAttacking = - attacker == AttackerType.monster || attacker == AttackerType.both; - - // 캐릭터 위치 계산 (characterLayer와 동일 로직) - const charWidth = 6; - final charX = switch (phase) { - BattlePhase.idle => 12, - BattlePhase.prepare => isPlayerAttacking ? 15 : 12, - BattlePhase.attack => isPlayerAttacking ? 18 : 12, - BattlePhase.hit => isPlayerAttacking ? 18 : 12, - BattlePhase.recover => isPlayerAttacking ? 15 : 12, - }; - - // 몬스터 위치 계산 (monsterLayer와 동일 로직) - final monsterRightEdge = switch (phase) { - BattlePhase.idle => 48, - BattlePhase.prepare => isMonsterAttacking ? 45 : 48, - BattlePhase.attack => isMonsterAttacking ? 42 : 48, - BattlePhase.hit => isMonsterAttacking ? 42 : 48, - BattlePhase.recover => isMonsterAttacking ? 45 : 48, - }; - final monsterX = monsterRightEdge - monsterWidth; - - // 공격자에 따라 다른 이펙트 사용 - final List effectLines; - final int effectX; - - if (attacker == AttackerType.player) { - // 플레이어 공격: 무기 이펙트 → 몬스터 왼쪽 바로 옆 - final effect = getWeaponEffect(weaponCategory); - effectLines = _getEffectLines(effect, phase, subFrame); - effectX = monsterX - 2; - } else if (attacker == AttackerType.monster) { - // 몬스터 공격: 왼쪽 방향 이펙트 → 캐릭터 오른쪽 바로 옆 - effectLines = _getMonsterAttackEffect(phase, subFrame); - effectX = charX + charWidth; - } else { - // 동시 공격(both): 무기 이펙트 두 캐릭터 중앙 - final effect = getWeaponEffect(weaponCategory); - effectLines = _getEffectLines(effect, phase, subFrame); - effectX = (charX + charWidth + monsterX) ~/ 2; - } - - if (effectLines.isEmpty) return null; - - // Phase 9: 플레이어 공격 시 무기 등급 색상 적용 - final List> cells; - if (attacker == AttackerType.player && weaponRarity != null) { - // 무기 등급에 따른 이펙트 색상 - cells = _spriteToCellsWithColor( - effectLines, - weaponRarity!.effectCellColor, - ); - } else { - // 기본 색상 (자동 색상 결정) - cells = _spriteToCells(effectLines); - } - - // 이펙트 높이에 따른 동적 Y 위치 (캔버스 하단 기준) - final effectHeight = effectLines.length; - final effectY = frameHeight - effectHeight - 1; - - return AsciiLayer( - cells: cells, - zIndex: 3, - offsetX: effectX, - offsetY: effectY, - ); - } - - /// 크리티컬 텍스트 레이어 생성 (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> 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 _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], - _ => [], - }; - } - - /// 문자열 스프라이트를 AsciiCell 2D 배열로 변환 - List> _spriteToCells(List lines) { - return lines.map((line) { - return line.split('').map(AsciiCell.fromChar).toList(); - }).toList(); - } - - /// 문자열 스프라이트를 지정된 색상으로 AsciiCell 2D 배열로 변환 (Phase 9) - /// - /// 이펙트 문자(공백 아닌 문자)에 지정 색상 적용 - List> _spriteToCellsWithColor( - List 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 배열로 변환 - List> _spriteToRightAlignedCells( - List lines, - int width, - ) { - // 전체 스프라이트에서 최대 너비 계산 - int maxWidth = 0; - for (final line in lines) { - final trimmed = line.trimRight().length; - if (trimmed > maxWidth) maxWidth = trimmed; - } - - // 왼쪽 패딩 계산 - final leftPadding = width - maxWidth; - - return lines.map((line) { - final trimmed = line.trimRight(); - final cells = []; - - // 왼쪽 패딩 - for (var i = 0; i < leftPadding; i++) { - cells.add(AsciiCell.empty); - } - - // 실제 문자 - for (var i = 0; i < trimmed.length; i++) { - cells.add(AsciiCell.fromChar(trimmed[i])); - } - - // 오른쪽 채우기 (필요시) - while (cells.length < width) { - cells.add(AsciiCell.empty); - } - - return cells; - }).toList(); - } - - /// 멀티라인 이펙트 프레임 반환 - List _getEffectLines( - WeaponEffect effect, - BattlePhase phase, - int subFrame, - ) { - final frames = switch (phase) { - BattlePhase.idle => >[], - BattlePhase.prepare => effect.prepareFrames, - BattlePhase.attack => effect.attackFrames, - BattlePhase.hit => effect.hitFrames, - BattlePhase.recover => >[], - }; - if (frames.isEmpty) return []; - return frames[subFrame % frames.length]; - } -} - -// ============================================================================ -// 몬스터 애니메이션 프레임 (기존 BattleComposer에서 이식) -// ============================================================================ - -/// 몬스터 애니메이션 프레임 반환 (페이즈별 다른 동작) -List> _getAnimatedMonsterFrames( - MonsterCategory category, - MonsterSize size, - BattlePhase phase, -) { - // 피격 상태 - if (phase == BattlePhase.hit) { - return _getMonsterHitFrames(category, size); - } - // 경계 상태 (prepare, attack) - if (phase == BattlePhase.prepare || phase == BattlePhase.attack) { - return _getMonsterAlertFrames(category, size); - } - // 일반 상태 (idle, recover) - return _getMonsterIdleFrames(category, size); -} - -List> _getMonsterIdleFrames( - MonsterCategory category, - MonsterSize size, -) { - return switch (size) { - MonsterSize.tiny => _tinyIdleFrames(category), - MonsterSize.small => _smallIdleFrames(category), - MonsterSize.medium => _mediumIdleFrames(category), - // large, huge, giant, titanic 모두 8줄 (large 프레임 사용) - MonsterSize.large || - MonsterSize.huge || - MonsterSize.giant || - MonsterSize.titanic => _largeIdleFrames(category), - }; -} - -List> _getMonsterHitFrames( - MonsterCategory category, - MonsterSize size, -) { - return switch (size) { - MonsterSize.tiny => _tinyHitFrames(category), - MonsterSize.small => _smallHitFrames(category), - MonsterSize.medium => _mediumHitFrames(category), - MonsterSize.large || - MonsterSize.huge || - MonsterSize.giant || - MonsterSize.titanic => _largeHitFrames(category), - }; -} - -List> _getMonsterAlertFrames( - MonsterCategory category, - MonsterSize size, -) { - return switch (size) { - MonsterSize.tiny => _tinyAlertFrames(category), - MonsterSize.small => _smallAlertFrames(category), - MonsterSize.medium => _mediumAlertFrames(category), - MonsterSize.large || - MonsterSize.huge || - MonsterSize.giant || - MonsterSize.titanic => _largeAlertFrames(category), - }; -} - -// ============================================================================ -// Tiny 몬스터 (2줄) -// ============================================================================ - -List> _tinyIdleFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [r'(o.o)', r' |_|'], - [r'(o o)', r' |_|'], - ], - MonsterCategory.malware => [ - [r'', r' \_/'], - [r'', r' /_\'], - ], - MonsterCategory.network => [ - [r' @', r'/|\'], - [r' @', r'\|/'], - ], - MonsterCategory.system => [ - [r'[x_x]', r' /_\'], - [r'[X_X]', r' \_/'], - ], - MonsterCategory.crypto => [ - [r'$(.)~', r' /_\'], - [r'$(.)`', r' \_/'], - ], - MonsterCategory.ai => [ - [r'{o}', r'/_\'], - [r'{O}', r'\_/'], - ], - MonsterCategory.boss => [ - [r'(|o|)', r' V V'], - [r'(|O|)', r' v v'], - ], - }; -} - -List> _tinyHitFrames(MonsterCategory category) { - return [ - [r'(*!*)', r' X_X'], - [r'(!*!)', r' x_x'], - ]; -} - -List> _tinyAlertFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [r'(O!O)', r' |!|'], - [r'(!O!)', r' |!|'], - ], - MonsterCategory.malware => [ - [r'', r' \!/'], - [r'', r' /!\'], - ], - MonsterCategory.network => [ - [r' @!', r'/|\'], - [r' !@', r'\|/'], - ], - MonsterCategory.system => [ - [r'[!_!]', r' /!\'], - [r'[!_!]', r' \!/'], - ], - MonsterCategory.crypto => [ - [r'$(!)~', r' /!\'], - [r'$(!)`', r' \!/'], - ], - MonsterCategory.ai => [ - [r'{!}', r'/!\'], - [r'{!}', r'\!/'], - ], - MonsterCategory.boss => [ - [r'(|!|)', r' V!V'], - [r'(|!|)', r' v!v'], - ], - }; -} - -// ============================================================================ -// Small 몬스터 (4줄) -// ============================================================================ - -List> _smallIdleFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [r' /\_/\', r'( O.O )', r' > ^ <', r' /| |\'], - [r' /\_/\', r'( O O )', r' > v <', r' \| |/'], - ], - MonsterCategory.malware => [ - [r' /\/\', r' (O O)', r' / \', r' \/ \/'], - [r' \/\/\', r' (O O)', r' \ /', r' /\ /\'], - ], - MonsterCategory.network => [ - [r' O', r' /|\', r' / \', r' _| |_'], - [r' O', r' \|/', r' | |', r' _/ \_'], - ], - MonsterCategory.system => [ - [r' _+_', r' (x_x)', r' /|\', r' _/ \_'], - [r' _+_', r' (X_X)', r' \|/', r' _| |_'], - ], - MonsterCategory.crypto => [ - [r' __', r' <(oo)~', r' / \', r' <_ _>'], - [r' __', r' (oo)>', r' \ /', r' <_ _>'], - ], - MonsterCategory.ai => [ - [r' ___', r' ( )', r' ( )', r' \_/'], - [r' _', r' / \', r' { }', r' \_/'], - ], - MonsterCategory.boss => [ - [r' ^w^', r' (|o|)', r' /|\', r' V V'], - [r' ^W^', r' (|O|)', r' \|/', r' v v'], - ], - }; -} - -List> _smallHitFrames(MonsterCategory category) { - return [ - [r' *!*', r' (>_<)', r' \X/', r' _/_\_'], - [r' !*!', r' (@_@)', r' /X\', r' _\_/_'], - ]; -} - -List> _smallAlertFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [r' /\_/\', r'( O!O )', r' > ! <', r' /| |\'], - [r' /\_/\', r'( !O! )', r' > ! <', r' \| |/'], - ], - MonsterCategory.malware => [ - [r' /\/\', r' (! !)', r' / \', r' \/ \/'], - [r' \/\/\', r' (! !)', r' \ /', r' /\ /\'], - ], - MonsterCategory.network => [ - [r' O!', r' /|\', r' / \', r' _| |_'], - [r' !O', r' \|/', r' | |', r' _/ \_'], - ], - MonsterCategory.system => [ - [r' _!_', r' (!_!)', r' /|\', r' _/ \_'], - [r' _!_', r' (!_!)', r' \|/', r' _| |_'], - ], - MonsterCategory.crypto => [ - [r' __', r' <(!!)~', r' / \', r' <_ _>'], - [r' __', r' (!!)>', r' \ /', r' <_ _>'], - ], - MonsterCategory.ai => [ - [r' ___', r' ( ! )', r' ( ! )', r' \_/'], - [r' _', r' /!\', r' { ! }', r' \_/'], - ], - MonsterCategory.boss => [ - [r' ^!^', r' (|!|)', r' /|\', r' V V'], - [r' ^!^', r' (|!|)', r' \|/', r' v v'], - ], - }; -} - -// ============================================================================ -// Medium 몬스터 (6줄) -// ============================================================================ - -List> _mediumIdleFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [ - r' /\_/\', - r' ( O.O )', - r' > ^ <', - r' /| |\', - r' | | | |', - r'_|_| |_|_', - ], - [ - r' /\_/\', - r' ( O O )', - r' > v <', - r' \| |/', - r' | | | |', - r'_|_| |_|_', - ], - ], - MonsterCategory.malware => [ - [ - r' /\/\', - r' /O O\', - r' \ /', - r' / \', - r' \/ \/', - r' _/ \_', - ], - [ - r' \/\/\', - r' \O O/', - r' / \', - r' \ /', - r' /\ /\', - r' _\ /_', - ], - ], - MonsterCategory.network => [ - [r' O', r' /|\', r' / \', r' | |', r' | |', r' _| |_'], - [r' O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'], - ], - MonsterCategory.system => [ - [r' _+_', r' (X_X)', r' /|\', r' / | \', r' | | |', r'_/ | \_'], - [r' _x_', r' (x_x)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'], - ], - MonsterCategory.crypto => [ - [ - r' __', - r' <(OO)~', - r' / \', - r' / \', - r' | |', - r'<__ __>', - ], - [ - r' __', - r' (OO)>', - r' \ /', - r' \ /', - r' | |', - r'<__ __>', - ], - ], - MonsterCategory.ai => [ - [ - r' ____', - r' / \', - r' ( )', - r' ( )', - r' \ /', - r' \__/', - ], - [ - r' __', - r' / \', - r' / \', - r' { }', - r' \ /', - r' \__/', - ], - ], - MonsterCategory.boss => [ - [r' ^W^', r' (|O|)', r' /|\', r' / | \', r' V V', r' _/ \_'], - [r' ^w^', r' (|o|)', r' \|/', r' \ | /', r' v v', r' _\ /_'], - ], - }; -} - -List> _mediumHitFrames(MonsterCategory category) { - return [ - [r' *!*', r' (>.<)', r' \X/', r' / \', r' | |', r'_/_ \_\'], - [r' !*!', r' (@_@)', r' /X\', r' \ /', r' | |', r'_\_ /_/'], - ]; -} - -List> _mediumAlertFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [ - r' /\_/\', - r' ( O!O )', - r' > ! <', - r' /| |\', - r' | | | |', - r'_|_| |_|_', - ], - [ - r' /\_/\', - r' ( !O! )', - r' > ! <', - r' \| |/', - r' | | | |', - r'_|_| |_|_', - ], - ], - MonsterCategory.malware => [ - [ - r' /\/\', - r' /! !\', - r' \ /', - r' / \', - r' \/ \/', - r' _/ \_', - ], - [ - r' \/\/\', - r' \! !/', - r' / \', - r' \ /', - r' /\ /\', - r' _\ /_', - ], - ], - MonsterCategory.network => [ - [r' O!', r' /|\', r' / \', r' | |', r' | |', r' _| |_'], - [r' !O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'], - ], - MonsterCategory.system => [ - [r' _!_', r' (!_!)', r' /|\', r' / | \', r' | | |', r'_/ | \_'], - [r' _!_', r' (!_!)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'], - ], - MonsterCategory.crypto => [ - [ - r' __', - r' <(!!)~', - r' / \', - r' / \', - r' | |', - r'<__ __>', - ], - [ - r' __', - r' (!!)>', - r' \ /', - r' \ /', - r' | |', - r'<__ __>', - ], - ], - MonsterCategory.ai => [ - [ - r' ____', - r' / ! \', - r' ( ! )', - r' ( ! )', - r' \ /', - r' \__/', - ], - [ - r' __', - r' / !\', - r' / ! \', - r' { ! }', - r' \ /', - r' \__/', - ], - ], - MonsterCategory.boss => [ - [r' ^!^', r' (|!|)', r' /|\', r' / | \', r' V V', r' _/ \_'], - [r' ^!^', r' (|!|)', r' \|/', r' \ | /', r' v v', r' _\ /_'], - ], - }; -} - -// ============================================================================ -// Large 몬스터 (8줄) -// ============================================================================ - -List> _largeIdleFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [ - r' /\__/\', - r' ( O O )', - r' > ^^ <', - r' /| |\', - r' | | | |', - r' | | | |', - r'_| | | |_', - r'|__|____|__|', - ], - [ - r' /\__/\', - r' ( O O )', - r' > vv <', - r' \| |/', - r' | | | |', - r' | | | |', - r'_| | | |_', - r'|__|____|__|', - ], - ], - MonsterCategory.malware => [ - [ - r' /\/\', - r' /O O\', - r' \ /', - r' / \', - r' / \', - r' \/ \/', - r' _/ \_', - r'/__ __\\', - ], - [ - r' \/\/\', - r' \O O/', - r' / \', - r' \ /', - r' \ /', - r' /\ /\', - r' _\ /_', - r'\__ __/', - ], - ], - MonsterCategory.network => [ - [ - r' O', - r' /|\', - r' / \', - r' | |', - r' | |', - r' | |', - r' _| |_', - r'|__ __|', - ], - [ - r' O', - r' \|/', - r' | |', - r' | |', - r' | |', - r' | |', - r' _/ \_', - r'/__ __\\', - ], - ], - MonsterCategory.system => [ - [ - r' _+_', - r' (X_X)', - r' /|\', - r' / | \', - r' | | |', - r' | | |', - r' _/ | \_', - r'|____|____|', - ], - [ - r' _x_', - r' (x_x)', - r' \|/', - r' \ | /', - r' | | |', - r' | | |', - r' _\ | /_', - r'|____|____|', - ], - ], - MonsterCategory.crypto => [ - [ - r' ___', - r' <(O O)~', - r' / \', - r' / \', - r' | |', - r' | |', - r' <__ __>', - r'|___ ___|', - ], - [ - r' ___', - r' (O O)>', - r' \ /', - r' \ /', - r' | |', - r' | |', - r' <__ __>', - r'|___ ___|', - ], - ], - MonsterCategory.ai => [ - [ - r' _____', - r' / \', - r' ( )', - r' ( )', - r' ( )', - r' \ /', - r' \ /', - r' \_/', - ], - [ - r' ___', - r' / \', - r' / \', - r' { }', - r' { }', - r' \ /', - r' \ /', - r' \_/', - ], - ], - MonsterCategory.boss => [ - [ - r' ^W^', - r' /|O|\', - r' /|\', - r' / | \', - r' | | |', - r' V | V', - r' _/ | \_', - r'|_____|_____|', - ], - [ - r' ^w^', - r' \|o|/', - r' \|/', - r' \ | /', - r' | | |', - r' v | v', - r' _\ | /_', - r'|_____|_____|', - ], - ], - }; -} - -List> _largeHitFrames(MonsterCategory category) { - return [ - [ - r' *!*', - r' (>.<)', - r' \X/', - r' / | \', - r' | | |', - r' X | X', - r' _/ | \_', - r'|_____|_____|', - ], - [ - r' !*!', - r' (@_@)', - r' /X\', - r' \ | /', - r' | | |', - r' x | x', - r' _\ | /_', - r'|_____|_____|', - ], - ]; -} - -List> _largeAlertFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [ - r' /\__/\', - r' ( O!O )', - r' > !! <', - r' /| |\', - r' | | | |', - r' | | | |', - r'_| | | |_', - r'|__|____|__|', - ], - [ - r' /\__/\', - r' ( !O! )', - r' > !! <', - r' \| |/', - r' | | | |', - r' | | | |', - r'_| | | |_', - r'|__|____|__|', - ], - ], - MonsterCategory.malware => [ - [ - r' /\/\', - r' /! !\', - r' \ /', - r' / \', - r' / \', - r' \/ \/', - r' _/ \_', - r'/__ __\\', - ], - [ - r' \/\/\', - r' \! !/', - r' / \', - r' \ /', - r' \ /', - r' /\ /\', - r' _\ /_', - r'\__ __/', - ], - ], - MonsterCategory.network => [ - [ - r' O!', - r' /|\', - r' / \', - r' | |', - r' | |', - r' | |', - r' _| |_', - r'|__ __|', - ], - [ - r' !O', - r' \|/', - r' | |', - r' | |', - r' | |', - r' | |', - r' _/ \_', - r'/__ __\\', - ], - ], - MonsterCategory.system => [ - [ - r' _!_', - r' (!_!)', - r' /|\', - r' / | \', - r' | | |', - r' | | |', - r' _/ | \_', - r'|____|____|', - ], - [ - r' _!_', - r' (!_!)', - r' \|/', - r' \ | /', - r' | | |', - r' | | |', - r' _\ | /_', - r'|____|____|', - ], - ], - MonsterCategory.crypto => [ - [ - r' ___', - r' <(! !)~', - r' / \', - r' / \', - r' | |', - r' | |', - r' <__ __>', - r'|___ ___|', - ], - [ - r' ___', - r' (! !)>', - r' \ /', - r' \ /', - r' | |', - r' | |', - r' <__ __>', - r'|___ ___|', - ], - ], - MonsterCategory.ai => [ - [ - r' _____', - r' / ! \', - r' ( ! )', - r' ( ! )', - r' ( ! )', - r' \ /', - r' \ /', - r' \_/', - ], - [ - r' ___', - r' / ! \', - r' / ! \', - r' { ! }', - r' { ! }', - r' \ /', - r' \ /', - r' \_/', - ], - ], - MonsterCategory.boss => [ - [ - r' ^!^', - r' /|!|\', - r' /|\', - r' / | \', - r' | | |', - r' V | V', - r' _/ | \_', - r'|_____|_____|', - ], - [ - r' ^!^', - r' \|!|/', - r' \|/', - r' \ | /', - r' | | |', - r' v | v', - r' _\ | /_', - r'|_____|_____|', - ], - ], - }; -} - -// ============================================================================ -// 몬스터 공격 이펙트 (← 방향, Phase 8) - 5줄 -// ============================================================================ - -/// 몬스터 공격 준비 프레임 (5줄) -const _monsterPrepareFrames = >[ - [r' ', r' ', r' < ', r' ', r' '], - [r' ', r' _ ', r' << ', r' - ', r' '], -]; - -/// 몬스터 공격 프레임 (5줄) -const _monsterAttackFrames = >[ - [r' ', r' __ ', r' <-- ', r' -- ', r' '], - [r' ', r' ___ ', r' <--- ', r' --- ', r' '], - [r' ', r' ____ ', r' <----- ', r' ---- ', r' '], -]; - -/// 몬스터 히트 프레임 (5줄) -const _monsterHitFrames = >[ - [r' *SLASH!* ', r' **** ', r' <----- ', r' **** ', r' '], - [r'*ATTACK!* ', r' **** ', r' <---- ', r' **** ', r' '], -]; - -// ============================================================================ -// 크리티컬 텍스트 프레임 (2줄, Phase 10) -// ============================================================================ - -/// 크리티컬 히트 텍스트 프레임 (반짝임 애니메이션) -const _critTextFrames = >[ - [r'*CRITICAL!*', r' ========='], - [r'=CRITICAL!=', r' *********'], -]; - -// ============================================================================ -// 전투 텍스트 이펙트 프레임 (Phase 11) -// ============================================================================ - -/// 회피 텍스트 프레임 (플레이어 회피 성공) -const _evadeTextFrames = >[ - [r'*EVADE!*', r'========'], - [r'=EVADE!=', r'********'], -]; - -/// 미스 텍스트 프레임 (플레이어 공격 빗나감) -const _missTextFrames = >[ - [r'*MISS!*', r'======='], - [r'=MISS!=', r'*******'], -]; - -/// 디버프 텍스트 프레임 (적에게 디버프 적용) -const _debuffTextFrames = >[ - [r'*DEBUFF!*', r'========='], - [r'=DEBUFF!=', r'*********'], -]; - -/// DOT 텍스트 프레임 (지속 피해) -const _dotTextFrames = >[ - [r'*DOT!*', r'======'], - [r'=DOT!=', r'******'], -]; - -/// 블록 텍스트 프레임 (방패 방어) -const _blockTextFrames = >[ - [r'*BLOCK!*', r'========'], - [r'=BLOCK!=', r'********'], -]; - -/// 패리 텍스트 프레임 (무기 쳐내기) -const _parryTextFrames = >[ - [r'*PARRY!*', r'========'], - [r'=PARRY!=', r'********'], -]; - -/// 몬스터 Idle 프레임 가져오기 (외부에서 접근 가능) -/// -/// 몬스터 사망 애니메이션에서 분해할 프레임을 가져올 때 사용 -List> getMonsterIdleFrames( - MonsterCategory category, - MonsterSize size, -) { - return _getMonsterIdleFrames(category, size); -} diff --git a/lib/src/core/animation/ascii_animation_data.dart b/lib/src/shared/animation/ascii_animation_data.dart similarity index 99% rename from lib/src/core/animation/ascii_animation_data.dart rename to lib/src/shared/animation/ascii_animation_data.dart index b561345..1157cb7 100644 --- a/lib/src/core/animation/ascii_animation_data.dart +++ b/lib/src/shared/animation/ascii_animation_data.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart'; +import 'package:asciineverdie/src/shared/animation/ascii_animation_type.dart'; /// ASCII 애니메이션 프레임 데이터 class AsciiAnimationData { diff --git a/lib/src/core/animation/ascii_animation_type.dart b/lib/src/shared/animation/ascii_animation_type.dart similarity index 100% rename from lib/src/core/animation/ascii_animation_type.dart rename to lib/src/shared/animation/ascii_animation_type.dart diff --git a/lib/src/core/animation/background_data.dart b/lib/src/shared/animation/background_data.dart similarity index 98% rename from lib/src/core/animation/background_data.dart rename to lib/src/shared/animation/background_data.dart index b34127a..d67cf2b 100644 --- a/lib/src/core/animation/background_data.dart +++ b/lib/src/shared/animation/background_data.dart @@ -1,7 +1,7 @@ // 환경별 배경 패턴 데이터 // ASCII Patrol 스타일 - 패럴렉스 스크롤링 배경 -import 'package:asciineverdie/src/core/animation/background_layer.dart'; +import 'package:asciineverdie/src/shared/animation/background_layer.dart'; /// 환경별 배경 레이어 반환 List getBackgroundLayers(EnvironmentType environment) { diff --git a/lib/src/core/animation/background_layer.dart b/lib/src/shared/animation/background_layer.dart similarity index 100% rename from lib/src/core/animation/background_layer.dart rename to lib/src/shared/animation/background_layer.dart diff --git a/lib/src/core/animation/canvas/ascii_canvas_painter.dart b/lib/src/shared/animation/canvas/ascii_canvas_painter.dart similarity index 97% rename from lib/src/core/animation/canvas/ascii_canvas_painter.dart rename to lib/src/shared/animation/canvas/ascii_canvas_painter.dart index b68d84d..97c9dab 100644 --- a/lib/src/core/animation/canvas/ascii_canvas_painter.dart +++ b/lib/src/shared/animation/canvas/ascii_canvas_painter.dart @@ -1,8 +1,8 @@ import 'dart:ui' as ui; -import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart'; -import 'package:asciineverdie/src/core/animation/canvas/ascii_layer.dart'; -import 'package:asciineverdie/src/core/constants/ascii_colors.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/ascii_cell.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/ascii_layer.dart'; +import 'package:asciineverdie/src/shared/theme/ascii_colors.dart'; import 'package:flutter/material.dart'; /// Paragraph 캐시 키 diff --git a/lib/src/core/animation/canvas/ascii_canvas_widget.dart b/lib/src/shared/animation/canvas/ascii_canvas_widget.dart similarity index 91% rename from lib/src/core/animation/canvas/ascii_canvas_widget.dart rename to lib/src/shared/animation/canvas/ascii_canvas_widget.dart index c17d1be..01d8388 100644 --- a/lib/src/core/animation/canvas/ascii_canvas_widget.dart +++ b/lib/src/shared/animation/canvas/ascii_canvas_widget.dart @@ -1,6 +1,6 @@ -import 'package:asciineverdie/src/core/animation/canvas/ascii_canvas_painter.dart'; -import 'package:asciineverdie/src/core/animation/canvas/ascii_layer.dart'; -import 'package:asciineverdie/src/core/constants/ascii_colors.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/ascii_canvas_painter.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/ascii_layer.dart'; +import 'package:asciineverdie/src/shared/theme/ascii_colors.dart'; import 'package:flutter/material.dart'; /// ASCII Canvas 위젯 (RepaintBoundary 포함) diff --git a/lib/src/core/animation/canvas/ascii_cell.dart b/lib/src/shared/animation/canvas/ascii_cell.dart similarity index 100% rename from lib/src/core/animation/canvas/ascii_cell.dart rename to lib/src/shared/animation/canvas/ascii_cell.dart diff --git a/lib/src/core/animation/canvas/ascii_layer.dart b/lib/src/shared/animation/canvas/ascii_layer.dart similarity index 95% rename from lib/src/core/animation/canvas/ascii_layer.dart rename to lib/src/shared/animation/canvas/ascii_layer.dart index df7f2fb..df45c71 100644 --- a/lib/src/core/animation/canvas/ascii_layer.dart +++ b/lib/src/shared/animation/canvas/ascii_layer.dart @@ -1,4 +1,4 @@ -import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/ascii_cell.dart'; /// ASCII 레이어 데이터 구조 (Canvas 렌더러용) /// diff --git a/lib/src/shared/animation/canvas/canvas_battle_composer.dart b/lib/src/shared/animation/canvas/canvas_battle_composer.dart new file mode 100644 index 0000000..134d450 --- /dev/null +++ b/lib/src/shared/animation/canvas/canvas_battle_composer.dart @@ -0,0 +1,544 @@ +import 'package:asciineverdie/src/shared/animation/ascii_animation_data.dart'; +import 'package:asciineverdie/src/shared/animation/background_data.dart'; +import 'package:asciineverdie/src/shared/animation/background_layer.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/ascii_cell.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/ascii_layer.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/combat_text_frames.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/monster_frames.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/rarity_color_mapper.dart'; +import 'package:asciineverdie/src/shared/animation/character_frames.dart'; +import 'package:asciineverdie/src/shared/animation/monster_size.dart'; +import 'package:asciineverdie/src/shared/animation/race_character_frames.dart'; +import 'package:asciineverdie/src/shared/animation/weapon_category.dart'; +import 'package:asciineverdie/src/shared/animation/weapon_effects.dart'; +import 'package:asciineverdie/src/core/model/item_stats.dart'; + +// 하위 호환성(backward compatibility)을 위한 re-export +export 'package:asciineverdie/src/shared/animation/canvas/monster_frames.dart' + show getMonsterIdleFrames; + +/// Canvas용 전투 프레임 합성기 +/// +/// 기존 BattleComposer의 로직을 레이어 기반으로 변환. +/// 출력: `List` (z-order 정렬됨) +/// +/// PvP 모드: [opponentRaceId]가 설정되면 몬스터 대신 상대 캐릭터(좌우 반전) 표시 +class CanvasBattleComposer { + const CanvasBattleComposer({ + required this.weaponCategory, + required this.hasShield, + required this.monsterCategory, + required this.monsterSize, + this.raceId, + this.weaponRarity, + this.opponentRaceId, + this.opponentHasShield = false, + }); + + final WeaponCategory weaponCategory; + final bool hasShield; + final MonsterCategory monsterCategory; + final MonsterSize monsterSize; + + /// 종족 ID (Phase 4: 종족별 캐릭터 애니메이션) + final String? raceId; + + /// 무기 희귀도 (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; + static const int monsterWidth = 18; + + /// 레이어 기반 프레임 생성 + List composeLayers( + BattlePhase phase, + int subFrame, + String? monsterBaseName, + EnvironmentType environment, + int globalTick, { + 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, + bool hideMonster = false, + }) { + final layers = [ + _createBackgroundLayer(environment, globalTick), + _createCharacterLayer(phase, subFrame, attacker), + // PvP 모드: 몬스터 대신 상대 캐릭터(좌우 반전) 표시 + // hideMonster: 몬스터 사망 애니메이션 중에는 렌더링 안함 + if (!hideMonster) + isPvP + ? _createOpponentCharacterLayer(phase, subFrame, attacker) + : _createMonsterLayer(phase, subFrame, attacker), + ]; + + // 이펙트 레이어 (준비/공격/히트 페이즈에서, 공격자 있을 때) + if ((phase == BattlePhase.prepare || + phase == BattlePhase.attack || + phase == BattlePhase.hit) && + attacker != AttackerType.none) { + final effectLayer = _createEffectLayer(phase, subFrame, attacker); + if (effectLayer != null) { + layers.add(effectLayer); + } + } + + // 텍스트 이펙트 레이어 + 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)); + if (isDot) layers.add(_createDotTextLayer(subFrame)); + if (isBlock) layers.add(_createBlockTextLayer(subFrame)); + if (isParry) layers.add(_createParryTextLayer(subFrame)); + + // z-order 정렬 + layers.sort((a, b) => a.zIndex.compareTo(b.zIndex)); + + return layers; + } + + /// 배경 레이어 생성 (z=0) + AsciiLayer _createBackgroundLayer( + EnvironmentType environment, + int globalTick, + ) { + final cells = List.generate( + frameHeight, + (_) => List.filled(frameWidth, AsciiCell.empty), + ); + + final bgLayers = getBackgroundLayers(environment); + for (final layer in bgLayers) { + final offset = (globalTick * layer.scrollSpeed).toInt(); + for (var i = 0; i < layer.lines.length; i++) { + final y = layer.yStart + i; + if (y >= frameHeight) break; + final pattern = layer.lines[i]; + if (pattern.isEmpty) continue; + for (var x = 0; x < frameWidth; x++) { + final patternIdx = (x + offset) % pattern.length; + final char = pattern[patternIdx]; + if (char != ' ') { + cells[y][x] = AsciiCell.fromChar(char); + } + } + } + } + + return AsciiLayer(cells: cells, zIndex: 0, opacity: 0.5); + } + + /// 캐릭터 레이어 생성 (z=2) + AsciiLayer _createCharacterLayer( + BattlePhase phase, + int subFrame, + AttackerType attacker, + ) { + CharacterFrame charFrame; + if (raceId != null && raceId!.isNotEmpty) { + final raceData = RaceCharacterFrames.get(raceId!); + if (raceData != null) { + final frames = raceData.getFrames(phase); + charFrame = frames[subFrame % frames.length]; + } else { + charFrame = getCharacterFrame(phase, subFrame); + } + } else { + charFrame = getCharacterFrame(phase, subFrame); + } + + if (hasShield) charFrame = charFrame.withShield(); + + final isPlayerAttacking = + attacker == AttackerType.player || attacker == AttackerType.both; + final charX = _getCharacterX(phase, isPlayerAttacking); + + final cells = _spriteToCells(charFrame.lines); + final charY = frameHeight - cells.length - 1; + + return AsciiLayer( + cells: cells, + zIndex: 2, + offsetX: charX, + offsetY: charY, + ); + } + + /// 몬스터 레이어 생성 (z=1) + AsciiLayer _createMonsterLayer( + BattlePhase phase, + int subFrame, + AttackerType attacker, + ) { + final monsterFrames = getAnimatedMonsterFrames( + monsterCategory, + monsterSize, + phase, + ); + final monsterFrame = monsterFrames[subFrame % monsterFrames.length]; + final cells = _spriteToRightAlignedCells(monsterFrame, monsterWidth); + + final isMonsterAttacking = + attacker == AttackerType.monster || attacker == AttackerType.both; + final monsterRightEdge = _getMonsterRightEdge(phase, isMonsterAttacking); + final monsterX = monsterRightEdge - monsterWidth; + final monsterY = frameHeight - cells.length - 1; + + return AsciiLayer( + cells: cells, + zIndex: 1, + offsetX: monsterX, + offsetY: monsterY, + ); + } + + /// 상대 캐릭터 레이어 생성 (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; + + const opponentWidth = 6; + final opponentRightEdge = _getMonsterRightEdge(phase, isOpponentAttacking); + final opponentX = opponentRightEdge - opponentWidth; + + final cells = _spriteToCells(mirroredLines); + final opponentY = frameHeight - cells.length - 1; + + return AsciiLayer( + cells: cells, + zIndex: 1, + offsetX: opponentX, + offsetY: opponentY, + ); + } + + /// 이펙트 레이어 생성 (z=3) + AsciiLayer? _createEffectLayer( + BattlePhase phase, + int subFrame, + AttackerType attacker, + ) { + final isPlayerAttacking = + attacker == AttackerType.player || attacker == AttackerType.both; + final isMonsterAttacking = + attacker == AttackerType.monster || attacker == AttackerType.both; + + const charWidth = 6; + final charX = _getCharacterX(phase, isPlayerAttacking); + final monsterRightEdge = _getMonsterRightEdge(phase, isMonsterAttacking); + final monsterX = monsterRightEdge - monsterWidth; + + final List effectLines; + final int effectX; + + if (attacker == AttackerType.player) { + final effect = getWeaponEffect(weaponCategory); + effectLines = _getEffectLines(effect, phase, subFrame); + effectX = monsterX - 2; + } else if (attacker == AttackerType.monster) { + effectLines = _getMonsterAttackEffect(phase, subFrame); + effectX = charX + charWidth; + } else { + final effect = getWeaponEffect(weaponCategory); + effectLines = _getEffectLines(effect, phase, subFrame); + effectX = (charX + charWidth + monsterX) ~/ 2; + } + + if (effectLines.isEmpty) return null; + + final List> cells; + if (attacker == AttackerType.player && weaponRarity != null) { + cells = _spriteToCellsWithColor( + effectLines, + weaponRarity!.effectCellColor, + ); + } else { + cells = _spriteToCells(effectLines); + } + + final effectHeight = effectLines.length; + final effectY = frameHeight - effectHeight - 1; + + return AsciiLayer( + cells: cells, + zIndex: 3, + offsetX: effectX, + offsetY: effectY, + ); + } + + // ============================================================================ + // 텍스트 이펙트 레이어 + // ============================================================================ + + AsciiLayer _createCriticalTextLayer(int subFrame) { + final textLines = subFrame % 2 == 0 + ? critTextFrames[0] + : critTextFrames[1]; + final cells = _textLinesToCells(textLines, AsciiCellColor.positive); + final textWidth = textLines.isNotEmpty ? textLines[0].length : 0; + final offsetX = (frameWidth - textWidth) ~/ 2; + return AsciiLayer(cells: cells, zIndex: 4, offsetX: offsetX, offsetY: 0); + } + + AsciiLayer _createEvadeTextLayer(int subFrame) { + return _createTextLayer( + frames: evadeTextFrames, + subFrame: subFrame, + color: AsciiCellColor.positive, + offsetX: 15, + ); + } + + AsciiLayer _createMissTextLayer(int subFrame) { + return _createTextLayer( + frames: missTextFrames, + subFrame: subFrame, + color: AsciiCellColor.negative, + offsetX: 35, + ); + } + + AsciiLayer _createDebuffTextLayer(int subFrame) { + return _createTextLayer( + frames: debuffTextFrames, + subFrame: subFrame, + color: AsciiCellColor.negative, + offsetX: 35, + ); + } + + AsciiLayer _createDotTextLayer(int subFrame) { + return _createTextLayer( + frames: dotTextFrames, + subFrame: subFrame, + color: AsciiCellColor.negative, + offsetX: 35, + ); + } + + AsciiLayer _createBlockTextLayer(int subFrame) { + return _createTextLayer( + frames: blockTextFrames, + subFrame: subFrame, + color: AsciiCellColor.positive, + offsetX: 15, + ); + } + + AsciiLayer _createParryTextLayer(int subFrame) { + return _createTextLayer( + frames: parryTextFrames, + subFrame: subFrame, + color: AsciiCellColor.positive, + offsetX: 15, + ); + } + + /// 공통 텍스트 레이어 생성 헬퍼 + AsciiLayer _createTextLayer({ + required List> frames, + required int subFrame, + required AsciiCellColor color, + required int offsetX, + }) { + final textLines = frames[subFrame % 2]; + final cells = _textLinesToCells(textLines, color); + return AsciiLayer(cells: cells, zIndex: 4, offsetX: offsetX, offsetY: 0); + } + + /// 텍스트 라인을 AsciiCell 배열로 변환 + List> _textLinesToCells( + List lines, + AsciiCellColor color, + ) { + return lines.map((String line) { + return line.split('').map((String char) { + if (char == ' ') return AsciiCell.empty; + return AsciiCell(char: char, color: color); + }).toList(); + }).toList(); + } + + // ============================================================================ + // 위치 계산 헬퍼 + // ============================================================================ + + /// 캐릭터 X 위치 계산 + int _getCharacterX(BattlePhase phase, bool isPlayerAttacking) { + return switch (phase) { + BattlePhase.idle => 12, + BattlePhase.prepare => isPlayerAttacking ? 15 : 12, + BattlePhase.attack => isPlayerAttacking ? 18 : 12, + BattlePhase.hit => isPlayerAttacking ? 18 : 12, + BattlePhase.recover => isPlayerAttacking ? 15 : 12, + }; + } + + /// 몬스터 오른쪽 가장자리 위치 계산 + int _getMonsterRightEdge(BattlePhase phase, bool isMonsterAttacking) { + return switch (phase) { + BattlePhase.idle => 48, + BattlePhase.prepare => isMonsterAttacking ? 45 : 48, + BattlePhase.attack => isMonsterAttacking ? 42 : 48, + BattlePhase.hit => isMonsterAttacking ? 42 : 48, + BattlePhase.recover => isMonsterAttacking ? 45 : 48, + }; + } + + // ============================================================================ + // 이펙트 헬퍼 + // ============================================================================ + + /// 몬스터 공격 이펙트 (방향) + List _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], + _ => [], + }; + } + + /// 멀티라인 이펙트 프레임 반환 + List _getEffectLines( + WeaponEffect effect, + BattlePhase phase, + int subFrame, + ) { + final frames = switch (phase) { + BattlePhase.idle => >[], + BattlePhase.prepare => effect.prepareFrames, + BattlePhase.attack => effect.attackFrames, + BattlePhase.hit => effect.hitFrames, + BattlePhase.recover => >[], + }; + if (frames.isEmpty) return []; + return frames[subFrame % frames.length]; + } + + // ============================================================================ + // 스프라이트 변환 유틸리티 + // ============================================================================ + + /// 문자열 좌우 반전 (PvP 모드용) + List _mirrorLines(List 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'\' => '/', + '(' => ')', + ')' => '(', + '[' => ']', + ']' => '[', + '{' => '}', + '}' => '{', + '<' => '>', + '>' => '<', + '\u2518' => '\u2514', + '\u2514' => '\u2518', + '\u2510' => '\u250c', + '\u250c' => '\u2510', + _ => char, + }; + } + + /// 문자열 스프라이트를 AsciiCell 2D 배열로 변환 + List> _spriteToCells(List lines) { + return lines.map((line) { + return line.split('').map(AsciiCell.fromChar).toList(); + }).toList(); + } + + /// 문자열 스프라이트를 지정된 색상으로 변환 + List> _spriteToCellsWithColor( + List 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 배열로 변환 + List> _spriteToRightAlignedCells( + List lines, + int width, + ) { + int maxWidth = 0; + for (final line in lines) { + final trimmed = line.trimRight().length; + if (trimmed > maxWidth) maxWidth = trimmed; + } + + final leftPadding = width - maxWidth; + + return lines.map((line) { + final trimmed = line.trimRight(); + final cells = []; + for (var i = 0; i < leftPadding; i++) { + cells.add(AsciiCell.empty); + } + for (var i = 0; i < trimmed.length; i++) { + cells.add(AsciiCell.fromChar(trimmed[i])); + } + while (cells.length < width) { + cells.add(AsciiCell.empty); + } + return cells; + }).toList(); + } +} diff --git a/lib/src/core/animation/canvas/canvas_special_composer.dart b/lib/src/shared/animation/canvas/canvas_special_composer.dart similarity index 96% rename from lib/src/core/animation/canvas/canvas_special_composer.dart rename to lib/src/shared/animation/canvas/canvas_special_composer.dart index 448b757..3446152 100644 --- a/lib/src/core/animation/canvas/canvas_special_composer.dart +++ b/lib/src/shared/animation/canvas/canvas_special_composer.dart @@ -1,6 +1,6 @@ -import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart'; -import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart'; -import 'package:asciineverdie/src/core/animation/canvas/ascii_layer.dart'; +import 'package:asciineverdie/src/shared/animation/ascii_animation_type.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/ascii_cell.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/ascii_layer.dart'; /// Canvas용 특수 이벤트 애니메이션 합성기 /// diff --git a/lib/src/core/animation/canvas/canvas_town_composer.dart b/lib/src/shared/animation/canvas/canvas_town_composer.dart similarity index 93% rename from lib/src/core/animation/canvas/canvas_town_composer.dart rename to lib/src/shared/animation/canvas/canvas_town_composer.dart index 3a202ed..8badbf5 100644 --- a/lib/src/core/animation/canvas/canvas_town_composer.dart +++ b/lib/src/shared/animation/canvas/canvas_town_composer.dart @@ -1,6 +1,6 @@ -import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart'; -import 'package:asciineverdie/src/core/animation/canvas/ascii_layer.dart'; -import 'package:asciineverdie/src/core/animation/race_character_frames.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/ascii_cell.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/ascii_layer.dart'; +import 'package:asciineverdie/src/shared/animation/race_character_frames.dart'; /// Canvas용 마을/상점 애니메이션 합성기 /// diff --git a/lib/src/core/animation/canvas/canvas_walking_composer.dart b/lib/src/shared/animation/canvas/canvas_walking_composer.dart similarity index 91% rename from lib/src/core/animation/canvas/canvas_walking_composer.dart rename to lib/src/shared/animation/canvas/canvas_walking_composer.dart index a2ba285..dc80755 100644 --- a/lib/src/core/animation/canvas/canvas_walking_composer.dart +++ b/lib/src/shared/animation/canvas/canvas_walking_composer.dart @@ -1,8 +1,8 @@ -import 'package:asciineverdie/src/core/animation/background_data.dart'; -import 'package:asciineverdie/src/core/animation/background_layer.dart'; -import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart'; -import 'package:asciineverdie/src/core/animation/canvas/ascii_layer.dart'; -import 'package:asciineverdie/src/core/animation/race_character_frames.dart'; +import 'package:asciineverdie/src/shared/animation/background_data.dart'; +import 'package:asciineverdie/src/shared/animation/background_layer.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/ascii_cell.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/ascii_layer.dart'; +import 'package:asciineverdie/src/shared/animation/race_character_frames.dart'; /// Canvas용 걷기 애니메이션 합성기 /// diff --git a/lib/src/shared/animation/canvas/combat_text_frames.dart b/lib/src/shared/animation/canvas/combat_text_frames.dart new file mode 100644 index 0000000..449634f --- /dev/null +++ b/lib/src/shared/animation/canvas/combat_text_frames.dart @@ -0,0 +1,77 @@ +/// 전투 텍스트 이펙트 프레임 데이터 +/// +/// CanvasBattleComposer에서 분리된 전투 텍스트 프레임 상수. +/// 크리티컬, 회피, 미스, 디버프, DOT, 블록, 패리 텍스트 프레임. + +// ============================================================================ +// 몬스터 공격 이펙트 (← 방향, Phase 8) - 5줄 +// ============================================================================ + +/// 몬스터 공격 준비 프레임 (5줄) +const monsterPrepareFrames = >[ + [r' ', r' ', r' < ', r' ', r' '], + [r' ', r' _ ', r' << ', r' - ', r' '], +]; + +/// 몬스터 공격 프레임 (5줄) +const monsterAttackFrames = >[ + [r' ', r' __ ', r' <-- ', r' -- ', r' '], + [r' ', r' ___ ', r' <--- ', r' --- ', r' '], + [r' ', r' ____ ', r' <----- ', r' ---- ', r' '], +]; + +/// 몬스터 히트 프레임 (5줄) +const monsterHitFrames = >[ + [r' *SLASH!* ', r' **** ', r' <----- ', r' **** ', r' '], + [r'*ATTACK!* ', r' **** ', r' <---- ', r' **** ', r' '], +]; + +// ============================================================================ +// 크리티컬 텍스트 프레임 (2줄, Phase 10) +// ============================================================================ + +/// 크리티컬 히트 텍스트 프레임 (반짝임 애니메이션) +const critTextFrames = >[ + [r'*CRITICAL!*', r' ========='], + [r'=CRITICAL!=', r' *********'], +]; + +// ============================================================================ +// 전투 텍스트 이펙트 프레임 (Phase 11) +// ============================================================================ + +/// 회피 텍스트 프레임 (플레이어 회피 성공) +const evadeTextFrames = >[ + [r'*EVADE!*', r'========'], + [r'=EVADE!=', r'********'], +]; + +/// 미스 텍스트 프레임 (플레이어 공격 빗나감) +const missTextFrames = >[ + [r'*MISS!*', r'======='], + [r'=MISS!=', r'*******'], +]; + +/// 디버프 텍스트 프레임 (적에게 디버프 적용) +const debuffTextFrames = >[ + [r'*DEBUFF!*', r'========='], + [r'=DEBUFF!=', r'*********'], +]; + +/// DOT 텍스트 프레임 (지속 피해) +const dotTextFrames = >[ + [r'*DOT!*', r'======'], + [r'=DOT!=', r'******'], +]; + +/// 블록 텍스트 프레임 (방패 방어) +const blockTextFrames = >[ + [r'*BLOCK!*', r'========'], + [r'=BLOCK!=', r'********'], +]; + +/// 패리 텍스트 프레임 (무기 쳐내기) +const parryTextFrames = >[ + [r'*PARRY!*', r'========'], + [r'=PARRY!=', r'********'], +]; diff --git a/lib/src/shared/animation/canvas/monster_frames.dart b/lib/src/shared/animation/canvas/monster_frames.dart new file mode 100644 index 0000000..7aad7ff --- /dev/null +++ b/lib/src/shared/animation/canvas/monster_frames.dart @@ -0,0 +1,765 @@ +import 'package:asciineverdie/src/shared/animation/ascii_animation_data.dart'; +import 'package:asciineverdie/src/shared/animation/character_frames.dart'; +import 'package:asciineverdie/src/shared/animation/monster_size.dart'; + +/// 몬스터 애니메이션 프레임 데이터 +/// +/// CanvasBattleComposer에서 분리된 몬스터 스프라이트 프레임. +/// 카테고리/크기/페이즈별 프레임을 제공한다. + +/// 몬스터 애니메이션 프레임 반환 (페이즈별 다른 동작) +List> getAnimatedMonsterFrames( + MonsterCategory category, + MonsterSize size, + BattlePhase phase, +) { + // 피격 상태 + if (phase == BattlePhase.hit) { + return getMonsterHitFrames(category, size); + } + // 경계 상태 (prepare, attack) + if (phase == BattlePhase.prepare || phase == BattlePhase.attack) { + return _getMonsterAlertFrames(category, size); + } + // 일반 상태 (idle, recover) + return getMonsterIdleFrames(category, size); +} + +/// 몬스터 Idle 프레임 가져오기 (외부에서 접근 가능) +/// +/// 몬스터 사망 애니메이션에서 분해할 프레임을 가져올 때 사용 +List> getMonsterIdleFrames( + MonsterCategory category, + MonsterSize size, +) { + return switch (size) { + MonsterSize.tiny => _tinyIdleFrames(category), + MonsterSize.small => _smallIdleFrames(category), + MonsterSize.medium => _mediumIdleFrames(category), + // large, huge, giant, titanic 모두 8줄 (large 프레임 사용) + MonsterSize.large || + MonsterSize.huge || + MonsterSize.giant || + MonsterSize.titanic => _largeIdleFrames(category), + }; +} + +List> getMonsterHitFrames( + MonsterCategory category, + MonsterSize size, +) { + return switch (size) { + MonsterSize.tiny => _tinyHitFrames(category), + MonsterSize.small => _smallHitFrames(category), + MonsterSize.medium => _mediumHitFrames(category), + MonsterSize.large || + MonsterSize.huge || + MonsterSize.giant || + MonsterSize.titanic => _largeHitFrames(category), + }; +} + +List> _getMonsterAlertFrames( + MonsterCategory category, + MonsterSize size, +) { + return switch (size) { + MonsterSize.tiny => _tinyAlertFrames(category), + MonsterSize.small => _smallAlertFrames(category), + MonsterSize.medium => _mediumAlertFrames(category), + MonsterSize.large || + MonsterSize.huge || + MonsterSize.giant || + MonsterSize.titanic => _largeAlertFrames(category), + }; +} + +// ============================================================================ +// Tiny 몬스터 (2줄) +// ============================================================================ + +List> _tinyIdleFrames(MonsterCategory category) { + return switch (category) { + MonsterCategory.bug => [ + [r'(o.o)', r' |_|'], + [r'(o o)', r' |_|'], + ], + MonsterCategory.malware => [ + [r'', r' \_/'], + [r'', r' /_\'], + ], + MonsterCategory.network => [ + [r' @', r'/|\'], + [r' @', r'\|/'], + ], + MonsterCategory.system => [ + [r'[x_x]', r' /_\'], + [r'[X_X]', r' \_/'], + ], + MonsterCategory.crypto => [ + [r'$(.)~', r' /_\'], + [r'$(.)`', r' \_/'], + ], + MonsterCategory.ai => [ + [r'{o}', r'/_\'], + [r'{O}', r'\_/'], + ], + MonsterCategory.boss => [ + [r'(|o|)', r' V V'], + [r'(|O|)', r' v v'], + ], + }; +} + +List> _tinyHitFrames(MonsterCategory category) { + return [ + [r'(*!*)', r' X_X'], + [r'(!*!)', r' x_x'], + ]; +} + +List> _tinyAlertFrames(MonsterCategory category) { + return switch (category) { + MonsterCategory.bug => [ + [r'(O!O)', r' |!|'], + [r'(!O!)', r' |!|'], + ], + MonsterCategory.malware => [ + [r'', r' \!/'], + [r'', r' /!\'], + ], + MonsterCategory.network => [ + [r' @!', r'/|\'], + [r' !@', r'\|/'], + ], + MonsterCategory.system => [ + [r'[!_!]', r' /!\'], + [r'[!_!]', r' \!/'], + ], + MonsterCategory.crypto => [ + [r'$(!)~', r' /!\'], + [r'$(!)`', r' \!/'], + ], + MonsterCategory.ai => [ + [r'{!}', r'/!\'], + [r'{!}', r'\!/'], + ], + MonsterCategory.boss => [ + [r'(|!|)', r' V!V'], + [r'(|!|)', r' v!v'], + ], + }; +} + +// ============================================================================ +// Small 몬스터 (4줄) +// ============================================================================ + +List> _smallIdleFrames(MonsterCategory category) { + return switch (category) { + MonsterCategory.bug => [ + [r' /\_/\', r'( O.O )', r' > ^ <', r' /| |\'], + [r' /\_/\', r'( O O )', r' > v <', r' \| |/'], + ], + MonsterCategory.malware => [ + [r' /\/\', r' (O O)', r' / \', r' \/ \/'], + [r' \/\/\', r' (O O)', r' \ /', r' /\ /\'], + ], + MonsterCategory.network => [ + [r' O', r' /|\', r' / \', r' _| |_'], + [r' O', r' \|/', r' | |', r' _/ \_'], + ], + MonsterCategory.system => [ + [r' _+_', r' (x_x)', r' /|\', r' _/ \_'], + [r' _+_', r' (X_X)', r' \|/', r' _| |_'], + ], + MonsterCategory.crypto => [ + [r' __', r' <(oo)~', r' / \', r' <_ _>'], + [r' __', r' (oo)>', r' \ /', r' <_ _>'], + ], + MonsterCategory.ai => [ + [r' ___', r' ( )', r' ( )', r' \_/'], + [r' _', r' / \', r' { }', r' \_/'], + ], + MonsterCategory.boss => [ + [r' ^w^', r' (|o|)', r' /|\', r' V V'], + [r' ^W^', r' (|O|)', r' \|/', r' v v'], + ], + }; +} + +List> _smallHitFrames(MonsterCategory category) { + return [ + [r' *!*', r' (>_<)', r' \X/', r' _/_\_'], + [r' !*!', r' (@_@)', r' /X\', r' _\_/_'], + ]; +} + +List> _smallAlertFrames(MonsterCategory category) { + return switch (category) { + MonsterCategory.bug => [ + [r' /\_/\', r'( O!O )', r' > ! <', r' /| |\'], + [r' /\_/\', r'( !O! )', r' > ! <', r' \| |/'], + ], + MonsterCategory.malware => [ + [r' /\/\', r' (! !)', r' / \', r' \/ \/'], + [r' \/\/\', r' (! !)', r' \ /', r' /\ /\'], + ], + MonsterCategory.network => [ + [r' O!', r' /|\', r' / \', r' _| |_'], + [r' !O', r' \|/', r' | |', r' _/ \_'], + ], + MonsterCategory.system => [ + [r' _!_', r' (!_!)', r' /|\', r' _/ \_'], + [r' _!_', r' (!_!)', r' \|/', r' _| |_'], + ], + MonsterCategory.crypto => [ + [r' __', r' <(!!)~', r' / \', r' <_ _>'], + [r' __', r' (!!)>', r' \ /', r' <_ _>'], + ], + MonsterCategory.ai => [ + [r' ___', r' ( ! )', r' ( ! )', r' \_/'], + [r' _', r' /!\', r' { ! }', r' \_/'], + ], + MonsterCategory.boss => [ + [r' ^!^', r' (|!|)', r' /|\', r' V V'], + [r' ^!^', r' (|!|)', r' \|/', r' v v'], + ], + }; +} + +// ============================================================================ +// Medium 몬스터 (6줄) +// ============================================================================ + +List> _mediumIdleFrames(MonsterCategory category) { + return switch (category) { + MonsterCategory.bug => [ + [ + r' /\_/\', + r' ( O.O )', + r' > ^ <', + r' /| |\', + r' | | | |', + r'_|_| |_|_', + ], + [ + r' /\_/\', + r' ( O O )', + r' > v <', + r' \| |/', + r' | | | |', + r'_|_| |_|_', + ], + ], + MonsterCategory.malware => [ + [ + r' /\/\', + r' /O O\', + r' \ /', + r' / \', + r' \/ \/', + r' _/ \_', + ], + [ + r' \/\/\', + r' \O O/', + r' / \', + r' \ /', + r' /\ /\', + r' _\ /_', + ], + ], + MonsterCategory.network => [ + [r' O', r' /|\', r' / \', r' | |', r' | |', r' _| |_'], + [r' O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'], + ], + MonsterCategory.system => [ + [r' _+_', r' (X_X)', r' /|\', r' / | \', r' | | |', r'_/ | \_'], + [r' _x_', r' (x_x)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'], + ], + MonsterCategory.crypto => [ + [ + r' __', + r' <(OO)~', + r' / \', + r' / \', + r' | |', + r'<__ __>', + ], + [ + r' __', + r' (OO)>', + r' \ /', + r' \ /', + r' | |', + r'<__ __>', + ], + ], + MonsterCategory.ai => [ + [ + r' ____', + r' / \', + r' ( )', + r' ( )', + r' \ /', + r' \__/', + ], + [ + r' __', + r' / \', + r' / \', + r' { }', + r' \ /', + r' \__/', + ], + ], + MonsterCategory.boss => [ + [r' ^W^', r' (|O|)', r' /|\', r' / | \', r' V V', r' _/ \_'], + [r' ^w^', r' (|o|)', r' \|/', r' \ | /', r' v v', r' _\ /_'], + ], + }; +} + +List> _mediumHitFrames(MonsterCategory category) { + return [ + [r' *!*', r' (>.<)', r' \X/', r' / \', r' | |', r'_/_ \_\'], + [r' !*!', r' (@_@)', r' /X\', r' \ /', r' | |', r'_\_ /_/'], + ]; +} + +List> _mediumAlertFrames(MonsterCategory category) { + return switch (category) { + MonsterCategory.bug => [ + [ + r' /\_/\', + r' ( O!O )', + r' > ! <', + r' /| |\', + r' | | | |', + r'_|_| |_|_', + ], + [ + r' /\_/\', + r' ( !O! )', + r' > ! <', + r' \| |/', + r' | | | |', + r'_|_| |_|_', + ], + ], + MonsterCategory.malware => [ + [ + r' /\/\', + r' /! !\', + r' \ /', + r' / \', + r' \/ \/', + r' _/ \_', + ], + [ + r' \/\/\', + r' \! !/', + r' / \', + r' \ /', + r' /\ /\', + r' _\ /_', + ], + ], + MonsterCategory.network => [ + [r' O!', r' /|\', r' / \', r' | |', r' | |', r' _| |_'], + [r' !O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'], + ], + MonsterCategory.system => [ + [r' _!_', r' (!_!)', r' /|\', r' / | \', r' | | |', r'_/ | \_'], + [r' _!_', r' (!_!)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'], + ], + MonsterCategory.crypto => [ + [ + r' __', + r' <(!!)~', + r' / \', + r' / \', + r' | |', + r'<__ __>', + ], + [ + r' __', + r' (!!)>', + r' \ /', + r' \ /', + r' | |', + r'<__ __>', + ], + ], + MonsterCategory.ai => [ + [ + r' ____', + r' / ! \', + r' ( ! )', + r' ( ! )', + r' \ /', + r' \__/', + ], + [ + r' __', + r' / !\', + r' / ! \', + r' { ! }', + r' \ /', + r' \__/', + ], + ], + MonsterCategory.boss => [ + [r' ^!^', r' (|!|)', r' /|\', r' / | \', r' V V', r' _/ \_'], + [r' ^!^', r' (|!|)', r' \|/', r' \ | /', r' v v', r' _\ /_'], + ], + }; +} + +// ============================================================================ +// Large 몬스터 (8줄) +// ============================================================================ + +List> _largeIdleFrames(MonsterCategory category) { + return switch (category) { + MonsterCategory.bug => [ + [ + r' /\__/\', + r' ( O O )', + r' > ^^ <', + r' /| |\', + r' | | | |', + r' | | | |', + r'_| | | |_', + r'|__|____|__|', + ], + [ + r' /\__/\', + r' ( O O )', + r' > vv <', + r' \| |/', + r' | | | |', + r' | | | |', + r'_| | | |_', + r'|__|____|__|', + ], + ], + MonsterCategory.malware => [ + [ + r' /\/\', + r' /O O\', + r' \ /', + r' / \', + r' / \', + r' \/ \/', + r' _/ \_', + r'/__ __\\', + ], + [ + r' \/\/\', + r' \O O/', + r' / \', + r' \ /', + r' \ /', + r' /\ /\', + r' _\ /_', + r'\__ __/', + ], + ], + MonsterCategory.network => [ + [ + r' O', + r' /|\', + r' / \', + r' | |', + r' | |', + r' | |', + r' _| |_', + r'|__ __|', + ], + [ + r' O', + r' \|/', + r' | |', + r' | |', + r' | |', + r' | |', + r' _/ \_', + r'/__ __\\', + ], + ], + MonsterCategory.system => [ + [ + r' _+_', + r' (X_X)', + r' /|\', + r' / | \', + r' | | |', + r' | | |', + r' _/ | \_', + r'|____|____|', + ], + [ + r' _x_', + r' (x_x)', + r' \|/', + r' \ | /', + r' | | |', + r' | | |', + r' _\ | /_', + r'|____|____|', + ], + ], + MonsterCategory.crypto => [ + [ + r' ___', + r' <(O O)~', + r' / \', + r' / \', + r' | |', + r' | |', + r' <__ __>', + r'|___ ___|', + ], + [ + r' ___', + r' (O O)>', + r' \ /', + r' \ /', + r' | |', + r' | |', + r' <__ __>', + r'|___ ___|', + ], + ], + MonsterCategory.ai => [ + [ + r' _____', + r' / \', + r' ( )', + r' ( )', + r' ( )', + r' \ /', + r' \ /', + r' \_/', + ], + [ + r' ___', + r' / \', + r' / \', + r' { }', + r' { }', + r' \ /', + r' \ /', + r' \_/', + ], + ], + MonsterCategory.boss => [ + [ + r' ^W^', + r' /|O|\', + r' /|\', + r' / | \', + r' | | |', + r' V | V', + r' _/ | \_', + r'|_____|_____|', + ], + [ + r' ^w^', + r' \|o|/', + r' \|/', + r' \ | /', + r' | | |', + r' v | v', + r' _\ | /_', + r'|_____|_____|', + ], + ], + }; +} + +List> _largeHitFrames(MonsterCategory category) { + return [ + [ + r' *!*', + r' (>.<)', + r' \X/', + r' / | \', + r' | | |', + r' X | X', + r' _/ | \_', + r'|_____|_____|', + ], + [ + r' !*!', + r' (@_@)', + r' /X\', + r' \ | /', + r' | | |', + r' x | x', + r' _\ | /_', + r'|_____|_____|', + ], + ]; +} + +List> _largeAlertFrames(MonsterCategory category) { + return switch (category) { + MonsterCategory.bug => [ + [ + r' /\__/\', + r' ( O!O )', + r' > !! <', + r' /| |\', + r' | | | |', + r' | | | |', + r'_| | | |_', + r'|__|____|__|', + ], + [ + r' /\__/\', + r' ( !O! )', + r' > !! <', + r' \| |/', + r' | | | |', + r' | | | |', + r'_| | | |_', + r'|__|____|__|', + ], + ], + MonsterCategory.malware => [ + [ + r' /\/\', + r' /! !\', + r' \ /', + r' / \', + r' / \', + r' \/ \/', + r' _/ \_', + r'/__ __\\', + ], + [ + r' \/\/\', + r' \! !/', + r' / \', + r' \ /', + r' \ /', + r' /\ /\', + r' _\ /_', + r'\__ __/', + ], + ], + MonsterCategory.network => [ + [ + r' O!', + r' /|\', + r' / \', + r' | |', + r' | |', + r' | |', + r' _| |_', + r'|__ __|', + ], + [ + r' !O', + r' \|/', + r' | |', + r' | |', + r' | |', + r' | |', + r' _/ \_', + r'/__ __\\', + ], + ], + MonsterCategory.system => [ + [ + r' _!_', + r' (!_!)', + r' /|\', + r' / | \', + r' | | |', + r' | | |', + r' _/ | \_', + r'|____|____|', + ], + [ + r' _!_', + r' (!_!)', + r' \|/', + r' \ | /', + r' | | |', + r' | | |', + r' _\ | /_', + r'|____|____|', + ], + ], + MonsterCategory.crypto => [ + [ + r' ___', + r' <(! !)~', + r' / \', + r' / \', + r' | |', + r' | |', + r' <__ __>', + r'|___ ___|', + ], + [ + r' ___', + r' (! !)>', + r' \ /', + r' \ /', + r' | |', + r' | |', + r' <__ __>', + r'|___ ___|', + ], + ], + MonsterCategory.ai => [ + [ + r' _____', + r' / ! \', + r' ( ! )', + r' ( ! )', + r' ( ! )', + r' \ /', + r' \ /', + r' \_/', + ], + [ + r' ___', + r' / ! \', + r' / ! \', + r' { ! }', + r' { ! }', + r' \ /', + r' \ /', + r' \_/', + ], + ], + MonsterCategory.boss => [ + [ + r' ^!^', + r' /|!|\', + r' /|\', + r' / | \', + r' | | |', + r' V | V', + r' _/ | \_', + r'|_____|_____|', + ], + [ + r' ^!^', + r' \|!|/', + r' \|/', + r' \ | /', + r' | | |', + r' v | v', + r' _\ | /_', + r'|_____|_____|', + ], + ], + }; +} diff --git a/lib/src/core/animation/canvas/rarity_color_mapper.dart b/lib/src/shared/animation/canvas/rarity_color_mapper.dart similarity index 91% rename from lib/src/core/animation/canvas/rarity_color_mapper.dart rename to lib/src/shared/animation/canvas/rarity_color_mapper.dart index 7b8fbb9..4062b96 100644 --- a/lib/src/core/animation/canvas/rarity_color_mapper.dart +++ b/lib/src/shared/animation/canvas/rarity_color_mapper.dart @@ -1,4 +1,4 @@ -import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart'; +import 'package:asciineverdie/src/shared/animation/canvas/ascii_cell.dart'; import 'package:asciineverdie/src/core/model/item_stats.dart'; /// 아이템 희귀도와 애니메이션 색상 간의 매핑 diff --git a/lib/src/core/animation/character_frames.dart b/lib/src/shared/animation/character_frames.dart similarity index 100% rename from lib/src/core/animation/character_frames.dart rename to lib/src/shared/animation/character_frames.dart diff --git a/lib/src/core/animation/front_screen_animation.dart b/lib/src/shared/animation/front_screen_animation.dart similarity index 100% rename from lib/src/core/animation/front_screen_animation.dart rename to lib/src/shared/animation/front_screen_animation.dart diff --git a/lib/src/core/animation/monster_size.dart b/lib/src/shared/animation/monster_size.dart similarity index 100% rename from lib/src/core/animation/monster_size.dart rename to lib/src/shared/animation/monster_size.dart diff --git a/lib/src/core/animation/race_character_frames.dart b/lib/src/shared/animation/race_character_frames.dart similarity index 99% rename from lib/src/core/animation/race_character_frames.dart rename to lib/src/shared/animation/race_character_frames.dart index 3280b2d..f43e61a 100644 --- a/lib/src/core/animation/race_character_frames.dart +++ b/lib/src/shared/animation/race_character_frames.dart @@ -1,7 +1,7 @@ // 종족별 ASCII 캐릭터 프레임 데이터 // 모든 캐릭터는 3줄 × 6자 폭으로 통일 (보스 10줄과 대비) -import 'package:asciineverdie/src/core/animation/character_frames.dart'; +import 'package:asciineverdie/src/shared/animation/character_frames.dart'; /// 종족별 캐릭터 프레임 저장소 class RaceCharacterFrames { diff --git a/lib/src/core/animation/weapon_category.dart b/lib/src/shared/animation/weapon_category.dart similarity index 100% rename from lib/src/core/animation/weapon_category.dart rename to lib/src/shared/animation/weapon_category.dart diff --git a/lib/src/core/animation/weapon_effects.dart b/lib/src/shared/animation/weapon_effects.dart similarity index 98% rename from lib/src/core/animation/weapon_effects.dart rename to lib/src/shared/animation/weapon_effects.dart index 7bb8331..481b984 100644 --- a/lib/src/core/animation/weapon_effects.dart +++ b/lib/src/shared/animation/weapon_effects.dart @@ -1,4 +1,4 @@ -import 'package:asciineverdie/src/core/animation/weapon_category.dart'; +import 'package:asciineverdie/src/shared/animation/weapon_category.dart'; /// 무기 카테고리별 공격 이펙트 ASCII 프레임 /// diff --git a/lib/src/core/l10n/game_data_l10n.dart b/lib/src/shared/l10n/game_data_l10n.dart similarity index 100% rename from lib/src/core/l10n/game_data_l10n.dart rename to lib/src/shared/l10n/game_data_l10n.dart diff --git a/lib/src/core/constants/ascii_colors.dart b/lib/src/shared/theme/ascii_colors.dart similarity index 100% rename from lib/src/core/constants/ascii_colors.dart rename to lib/src/shared/theme/ascii_colors.dart