feat(game): 게임 시스템 전면 개편 및 다국어 지원 확장
## 스킬 시스템 개선 - skill_data.dart: 스킬 데이터 구조 전면 개편 (+1176 라인) - skill_service.dart: 스킬 발동 로직 확장 및 버프 시스템 연동 - skill.dart: 스킬 모델 개선, 쿨다운/효과 타입 추가 ## Canvas 애니메이션 리팩토링 - battle_composer.dart 삭제 (레거시 위젯 기반 렌더러) - monster_colors.dart 삭제 (AsciiCell 색상 시스템으로 통합) - canvas_battle_composer.dart: z-index 정렬 (몬스터 z=1, 캐릭터 z=2, 이펙트 z=3) - ascii_cell.dart, ascii_layer.dart: 코드 정리 ## UI/UX 개선 - hp_mp_bar.dart: l10n 적용, 몬스터 HP 바 컴팩트화 - death_overlay.dart: 사망 화면 개선 - equipment_stats_panel.dart: 장비 스탯 표시 확장 - active_buff_panel.dart: 버프 패널 개선 - notification_overlay.dart: 알림 시스템 개선 ## 다국어 지원 확장 - game_text_l10n.dart: 게임 텍스트 통합 (+758 라인) - 한국어/일본어/영어/중국어 번역 업데이트 - ARB 파일 동기화 ## 게임 로직 개선 - progress_service.dart: 진행 로직 리팩토링 - combat_calculator.dart: 전투 계산 로직 개선 - stat_calculator.dart: 스탯 계산 시스템 개선 - story_service.dart: 스토리 진행 로직 개선 ## 기타 - theme_preferences.dart 삭제 (미사용) - 테스트 파일 업데이트 - class_data.dart: 클래스 데이터 정리
This commit is contained in:
@@ -1,815 +0,0 @@
|
||||
// BattleComposer - 전투 프레임 실시간 합성
|
||||
// Stone Story RPG 스타일 참고 - 8줄 캐릭터/몬스터, 60자 폭
|
||||
// ASCII Patrol 스타일 패럴렉스 배경
|
||||
|
||||
import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart';
|
||||
import 'package:askiineverdie/src/core/animation/background_data.dart';
|
||||
import 'package:askiineverdie/src/core/animation/background_layer.dart';
|
||||
import 'package:askiineverdie/src/core/animation/character_frames.dart';
|
||||
import 'package:askiineverdie/src/core/animation/monster_size.dart';
|
||||
import 'package:askiineverdie/src/core/animation/weapon_category.dart';
|
||||
import 'package:askiineverdie/src/core/animation/weapon_effects.dart';
|
||||
|
||||
/// 전투 프레임 합성기
|
||||
class BattleComposer {
|
||||
const BattleComposer({
|
||||
required this.weaponCategory,
|
||||
required this.hasShield,
|
||||
required this.monsterCategory,
|
||||
required this.monsterSize,
|
||||
});
|
||||
|
||||
final WeaponCategory weaponCategory;
|
||||
final bool hasShield;
|
||||
final MonsterCategory monsterCategory;
|
||||
final MonsterSize monsterSize;
|
||||
|
||||
/// 전체 프레임 폭 (문자 수)
|
||||
static const int frameWidth = 60;
|
||||
|
||||
/// 프레임 높이 (줄 수)
|
||||
static const int frameHeight = 8;
|
||||
|
||||
/// 영역 분할
|
||||
static const int characterWidth = 18;
|
||||
static const int effectWidth = 24;
|
||||
static const int monsterWidth = 18;
|
||||
|
||||
/// 전투 프레임 생성 (배경 없음)
|
||||
String composeFrame(BattlePhase phase, int subFrame, String? monsterBaseName) {
|
||||
// 캐릭터 프레임
|
||||
var charFrame = getCharacterFrame(phase, subFrame);
|
||||
if (hasShield) {
|
||||
charFrame = charFrame.withShield();
|
||||
}
|
||||
|
||||
// 몬스터 프레임 (애니메이션 포함)
|
||||
final monsterFrames =
|
||||
_getAnimatedMonsterFrames(monsterCategory, monsterSize, phase);
|
||||
final monsterFrame = monsterFrames[subFrame % monsterFrames.length];
|
||||
|
||||
// 무기 이펙트 (단일 라인)
|
||||
final effect = getWeaponEffect(weaponCategory);
|
||||
final effectLine = _getEffectLine(effect, phase, subFrame);
|
||||
|
||||
// 프레임 합성
|
||||
return _compose(charFrame.lines, monsterFrame, effectLine, phase);
|
||||
}
|
||||
|
||||
/// 전투 프레임 생성 (배경 포함, ASCII Patrol 스타일)
|
||||
String composeFrameWithBackground(
|
||||
BattlePhase phase,
|
||||
int subFrame,
|
||||
String? monsterBaseName,
|
||||
EnvironmentType environment,
|
||||
int globalTick,
|
||||
) {
|
||||
// 1. 8x60 캔버스 생성 (공백으로 초기화)
|
||||
final canvas =
|
||||
List.generate(frameHeight, (_) => List.filled(frameWidth, ' '));
|
||||
|
||||
// 2. 배경 레이어 그리기 (뒤에서 앞으로)
|
||||
final layers = getBackgroundLayers(environment);
|
||||
for (final layer in layers) {
|
||||
_drawBackgroundLayer(canvas, layer, globalTick);
|
||||
}
|
||||
|
||||
// 3. 캐릭터 프레임 (페이즈에 따라 X 위치 변경 - 근접 전투)
|
||||
var charFrame = getCharacterFrame(phase, subFrame);
|
||||
if (hasShield) {
|
||||
charFrame = charFrame.withShield();
|
||||
}
|
||||
final normalizedChar = _normalizeSprite(charFrame.lines, characterWidth);
|
||||
// 바닥 레이어(Y=7) 위에 서있도록 -1
|
||||
final charY = frameHeight - normalizedChar.length - 1;
|
||||
// 페이즈별 캐릭터 X 위치 (몬스터에게 접근)
|
||||
final charX = switch (phase) {
|
||||
BattlePhase.idle => 0,
|
||||
BattlePhase.prepare => 12,
|
||||
BattlePhase.attack => 24,
|
||||
BattlePhase.hit => 28,
|
||||
BattlePhase.recover => 8,
|
||||
};
|
||||
_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,
|
||||
referenceWidth: monsterRefWidth,
|
||||
);
|
||||
final monsterX = frameWidth - monsterWidth;
|
||||
// 바닥 레이어(Y=7) 위에 서있도록 -1
|
||||
final monsterY = frameHeight - normalizedMonster.length - 1;
|
||||
// 몬스터는 경계 내 완전 렌더링 (내부 공백에 배경이 비치지 않도록)
|
||||
_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 위치: 캐릭터 머리 높이 (1번째 줄) 기준 - 수정됨
|
||||
final effectY = charY;
|
||||
// 이펙트 X 위치: 캐릭터 오른쪽에 붙여서 표시
|
||||
final effectX = charX + 6;
|
||||
for (var i = 0; i < effectLines.length; i++) {
|
||||
final y = effectY + i;
|
||||
if (y >= 0 && y < frameHeight && effectLines[i].isNotEmpty) {
|
||||
_overlayText(canvas, effectLines[i], effectX, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 문자열로 변환
|
||||
return canvas.map((row) => row.join()).join('\n');
|
||||
}
|
||||
|
||||
/// 스프라이트를 지정 폭으로 정규화 (왼쪽 정렬)
|
||||
List<String> _normalizeSprite(List<String> sprite, int width) {
|
||||
return sprite.map((line) => line.padRight(width).substring(0, width)).toList();
|
||||
}
|
||||
|
||||
/// 스프라이트를 지정 폭으로 정규화 (오른쪽 정렬, 전체 스프라이트 기준)
|
||||
///
|
||||
/// 모든 줄을 동일한 기준점에서 오른쪽 정렬하여
|
||||
/// 머리와 몸통이 분리되지 않도록 함
|
||||
///
|
||||
/// [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();
|
||||
}
|
||||
|
||||
/// 몬스터 스프라이트의 기준 너비 계산 (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,
|
||||
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];
|
||||
for (var j = 0; j < line.length; j++) {
|
||||
final x = startX + j;
|
||||
if (x < 0 || x >= frameWidth) continue;
|
||||
|
||||
final char = line[j];
|
||||
// 공백이 아닌 문자만 덮어쓰기 (투명 배경 효과)
|
||||
if (char != ' ') {
|
||||
canvas[y][x] = char;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 스프라이트를 캔버스에 오버레이 (라인별 경계 내 완전 렌더링)
|
||||
///
|
||||
/// 각 라인에서 첫 번째와 마지막 비공백 문자 사이의 모든 문자를 그림.
|
||||
/// 내부 공백도 그려져서 스크롤링 배경이 비치지 않음.
|
||||
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,
|
||||
BackgroundLayer layer,
|
||||
int globalTick,
|
||||
) {
|
||||
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;
|
||||
|
||||
// 스크롤 오프셋 계산
|
||||
final offset = (globalTick * layer.scrollSpeed).toInt() % pattern.length;
|
||||
|
||||
// 패턴을 스크롤하며 그리기
|
||||
for (var x = 0; x < frameWidth; x++) {
|
||||
final patternIdx = (x + offset) % pattern.length;
|
||||
final char = pattern[patternIdx];
|
||||
if (char != ' ') {
|
||||
canvas[y][x] = char;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 텍스트를 캔버스에 오버레이
|
||||
void _overlayText(
|
||||
List<List<String>> canvas,
|
||||
String text,
|
||||
int startX,
|
||||
int y,
|
||||
) {
|
||||
if (y < 0 || y >= frameHeight) return;
|
||||
|
||||
for (var i = 0; i < text.length; i++) {
|
||||
final x = startX + i;
|
||||
if (x < 0 || x >= frameWidth) continue;
|
||||
|
||||
final char = text[i];
|
||||
if (char != ' ') {
|
||||
canvas[y][x] = char;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 멀티라인 이펙트 프레임 반환
|
||||
List<String> _getEffectLines(
|
||||
WeaponEffect effect, BattlePhase phase, int subFrame) {
|
||||
final frames = switch (phase) {
|
||||
BattlePhase.idle => <List<String>>[],
|
||||
BattlePhase.prepare => effect.prepareFrames,
|
||||
BattlePhase.attack => effect.attackFrames,
|
||||
BattlePhase.hit => effect.hitFrames,
|
||||
BattlePhase.recover => <List<String>>[],
|
||||
};
|
||||
if (frames.isEmpty) return [];
|
||||
return frames[subFrame % frames.length];
|
||||
}
|
||||
|
||||
/// 단일 라인 이펙트 (하위 호환용)
|
||||
String _getEffectLine(WeaponEffect effect, BattlePhase phase, int subFrame) {
|
||||
final lines = _getEffectLines(effect, phase, subFrame);
|
||||
if (lines.isEmpty) return '';
|
||||
// 멀티라인 중 중간 라인 반환 (메인 이펙트)
|
||||
final midIndex = lines.length ~/ 2;
|
||||
return lines.length > midIndex ? lines[midIndex] : lines.first;
|
||||
}
|
||||
|
||||
String _compose(
|
||||
List<String> charLines,
|
||||
List<String> monsterLines,
|
||||
String effectLine,
|
||||
BattlePhase phase,
|
||||
) {
|
||||
final result = <String>[];
|
||||
|
||||
// 캐릭터와 몬스터를 하단 정렬 (8줄 기준)
|
||||
final charOffset = frameHeight - charLines.length;
|
||||
final monsterOffset = frameHeight - monsterLines.length;
|
||||
|
||||
// 이펙트 Y 위치: 캐릭터 body/arm 줄 (charOffset + 1)
|
||||
final effectRow = charOffset + 1;
|
||||
|
||||
for (var i = 0; i < frameHeight; i++) {
|
||||
// 캐릭터 파트 (왼쪽 18자)
|
||||
final charIdx = i - charOffset;
|
||||
final charPart =
|
||||
(charIdx >= 0 && charIdx < charLines.length ? charLines[charIdx] : '')
|
||||
.padRight(characterWidth);
|
||||
|
||||
// 이펙트 파트 (중앙 24자) - 캐릭터 팔 높이에 표시
|
||||
String effectPart = '';
|
||||
if (i == effectRow &&
|
||||
(phase == BattlePhase.attack || phase == BattlePhase.hit)) {
|
||||
effectPart = effectLine;
|
||||
}
|
||||
effectPart = effectPart.padRight(effectWidth);
|
||||
|
||||
// 몬스터 파트 (오른쪽 18자)
|
||||
final monsterIdx = i - monsterOffset;
|
||||
final monsterPart = (monsterIdx >= 0 && monsterIdx < monsterLines.length
|
||||
? monsterLines[monsterIdx]
|
||||
: '')
|
||||
.padLeft(monsterWidth);
|
||||
|
||||
result.add('$charPart$effectPart$monsterPart');
|
||||
}
|
||||
|
||||
return result.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 몬스터 애니메이션 프레임
|
||||
// ============================================================================
|
||||
|
||||
/// 몬스터 애니메이션 프레임 반환 (페이즈별 다른 동작)
|
||||
List<List<String>> _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<List<String>> _getMonsterIdleFrames(MonsterCategory category, MonsterSize size) {
|
||||
return switch (size) {
|
||||
MonsterSize.tiny => _tinyIdleFrames(category),
|
||||
MonsterSize.small => _smallIdleFrames(category),
|
||||
MonsterSize.medium => _mediumIdleFrames(category),
|
||||
MonsterSize.large => _largeIdleFrames(category),
|
||||
_ => _hugeIdleFrames(category), // huge 이상은 같은 프레임 사용
|
||||
};
|
||||
}
|
||||
|
||||
/// 피격 상태 몬스터 프레임
|
||||
List<List<String>> _getMonsterHitFrames(MonsterCategory category, MonsterSize size) {
|
||||
return switch (size) {
|
||||
MonsterSize.tiny => _tinyHitFrames(category),
|
||||
MonsterSize.small => _smallHitFrames(category),
|
||||
MonsterSize.medium => _mediumHitFrames(category),
|
||||
MonsterSize.large => _largeHitFrames(category),
|
||||
_ => _hugeHitFrames(category),
|
||||
};
|
||||
}
|
||||
|
||||
/// 경계 상태 몬스터 프레임 (prepare/attack 시)
|
||||
List<List<String>> _getMonsterAlertFrames(MonsterCategory category, MonsterSize size) {
|
||||
return switch (size) {
|
||||
MonsterSize.tiny => _tinyAlertFrames(category),
|
||||
MonsterSize.small => _smallAlertFrames(category),
|
||||
MonsterSize.medium => _mediumAlertFrames(category),
|
||||
MonsterSize.large => _largeAlertFrames(category),
|
||||
_ => _hugeAlertFrames(category),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tiny 몬스터 (2줄, 8줄 캔버스 하단 정렬)
|
||||
// ============================================================================
|
||||
|
||||
List<List<String>> _tinyIdleFrames(MonsterCategory category) {
|
||||
return switch (category) {
|
||||
MonsterCategory.bug => [
|
||||
[r'*', r'/\'],
|
||||
[r'o', r'\/'],
|
||||
],
|
||||
MonsterCategory.malware => [
|
||||
[r'><', r'\/'],
|
||||
[r'<>', r'/\'],
|
||||
],
|
||||
MonsterCategory.network => [
|
||||
[r'o', r'|'],
|
||||
[r'O', r'|'],
|
||||
],
|
||||
MonsterCategory.system => [
|
||||
[r'+', r'|'],
|
||||
[r'x', r'|'],
|
||||
],
|
||||
MonsterCategory.crypto => [
|
||||
[r'~<', r'>>'],
|
||||
[r'<~', r'<<'],
|
||||
],
|
||||
MonsterCategory.ai => [
|
||||
[r'()', r''],
|
||||
[r'{}', r''],
|
||||
],
|
||||
MonsterCategory.boss => [
|
||||
[r'^v', r'\/'],
|
||||
[r'v^', r'/\'],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
List<List<String>> _tinyHitFrames(MonsterCategory category) {
|
||||
return [
|
||||
[r'*!', r'><'],
|
||||
[r'!*', r'<>'],
|
||||
];
|
||||
}
|
||||
|
||||
List<List<String>> _tinyAlertFrames(MonsterCategory category) {
|
||||
return switch (category) {
|
||||
MonsterCategory.bug => [
|
||||
[r'!!', r'/\'],
|
||||
[r'OO', r'><'],
|
||||
],
|
||||
MonsterCategory.malware => [
|
||||
[r'!!', r'\/'],
|
||||
[r'@@', r'/\'],
|
||||
],
|
||||
MonsterCategory.network => [
|
||||
[r'O!', r'|'],
|
||||
[r'!O', r'X'],
|
||||
],
|
||||
MonsterCategory.system => [
|
||||
[r'!!', r'X'],
|
||||
[r'@@', r'|'],
|
||||
],
|
||||
MonsterCategory.crypto => [
|
||||
[r'!<', r'>>'],
|
||||
[r'>!', r'<<'],
|
||||
],
|
||||
MonsterCategory.ai => [
|
||||
[r'(!)', r''],
|
||||
[r'{!}', r''],
|
||||
],
|
||||
MonsterCategory.boss => [
|
||||
[r'^!', r'><'],
|
||||
[r'!^', r'<>'],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Small 몬스터 (4줄)
|
||||
// ============================================================================
|
||||
|
||||
List<List<String>> _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<List<String>> _smallHitFrames(MonsterCategory category) {
|
||||
return [
|
||||
[r' *!*', r' (>_<)', r' \X/', r' _/_\_'],
|
||||
[r' !*!', r' (@_@)', r' /X\', r' _\_/_'],
|
||||
];
|
||||
}
|
||||
|
||||
List<List<String>> _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<List<String>> _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<List<String>> _mediumHitFrames(MonsterCategory category) {
|
||||
return [
|
||||
[r' *!*', r' (>.<)', r' \X/', r' / \', r' | |', r'_/_ \_\'],
|
||||
[r' !*!', r' (@_@)', r' /X\', r' \ /', r' | |', r'_\_ /_/'],
|
||||
];
|
||||
}
|
||||
|
||||
List<List<String>> _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<List<String>> _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' _\+/_', 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<List<String>> _largeHitFrames(MonsterCategory category) {
|
||||
return [
|
||||
[r' *!*!*', r' (>___<)', r' \\X//', r' / \\// \', r' | \\/ |', r' | / \ |', r' _|/ \|_', r'|___/\\___|'],
|
||||
[r' !*!*!', r' (@___@)', r' //X\\', r' \ /\\/ /', r' | //\\ |', r' | \ / |', r' _|\ /|_', r'|___\\/__|'],
|
||||
];
|
||||
}
|
||||
|
||||
List<List<String>> _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'|____|____|'],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Huge+ 몬스터 (8줄, 더 넓게)
|
||||
// ============================================================================
|
||||
|
||||
List<List<String>> _hugeIdleFrames(MonsterCategory category) {
|
||||
return switch (category) {
|
||||
MonsterCategory.bug => [
|
||||
[r' /\____/\', r' ( O O )', r' > ^^^^ <', r' /| |\', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'],
|
||||
[r' /\____/\', r' ( O O )', r' > vvvv <', 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' _\\+/_', 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<List<String>> _hugeHitFrames(MonsterCategory category) {
|
||||
return [
|
||||
[r' *!*!*!*', r' (>_____<)', r' \\\\X////', r' / \\\\// \\', r' | \\\\/ |', r' | / \\ |', r' _|/ \\|_', r'|____/\\\\___|'],
|
||||
[r' !*!*!*!', r' (@_____@)', r' ////X\\\\', r' \\ /\\\\/ /', r' | ////\\\\ |', r' | \\ / |', r' _|\\ /|_', r'|____\\\\/___|'],
|
||||
];
|
||||
}
|
||||
|
||||
List<List<String>> _hugeAlertFrames(MonsterCategory category) {
|
||||
return switch (category) {
|
||||
MonsterCategory.bug => [
|
||||
[r' /\____/\', r' ( ! ! )', r' > !!!! <', r' /| |\', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'],
|
||||
[r' /\____/\', r' ( ! ! )', 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'|_____|_____|'],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// 레거시 호환용 함수
|
||||
List<List<String>> getMonsterFrames(MonsterCategory category, MonsterSize size) {
|
||||
return _getMonsterIdleFrames(category, size);
|
||||
}
|
||||
@@ -15,10 +15,7 @@ enum AsciiCellColor {
|
||||
|
||||
/// 단일 ASCII 셀 데이터
|
||||
class AsciiCell {
|
||||
const AsciiCell({
|
||||
required this.char,
|
||||
this.color = AsciiCellColor.object,
|
||||
});
|
||||
const AsciiCell({required this.char, this.color = AsciiCellColor.object});
|
||||
|
||||
/// 표시할 문자 (단일 문자)
|
||||
final String char;
|
||||
@@ -45,10 +42,7 @@ class AsciiCell {
|
||||
/// 문자열에서 AsciiCell 생성 (자동 색상)
|
||||
factory AsciiCell.fromChar(String char) {
|
||||
if (char.isEmpty || char == ' ') return empty;
|
||||
return AsciiCell(
|
||||
char: char,
|
||||
color: colorFromChar(char),
|
||||
);
|
||||
return AsciiCell(char: char, color: colorFromChar(char));
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -37,11 +37,7 @@ class AsciiLayer {
|
||||
}
|
||||
|
||||
/// 빈 레이어 생성
|
||||
factory AsciiLayer.empty({
|
||||
int width = 60,
|
||||
int height = 8,
|
||||
int zIndex = 0,
|
||||
}) {
|
||||
factory AsciiLayer.empty({int width = 60, int height = 8, int zIndex = 0}) {
|
||||
final cells = List.generate(
|
||||
height,
|
||||
(_) => List.filled(width, AsciiCell.empty),
|
||||
|
||||
@@ -279,8 +279,7 @@ List<List<String>> _getMonsterIdleFrames(
|
||||
MonsterSize.large ||
|
||||
MonsterSize.huge ||
|
||||
MonsterSize.giant ||
|
||||
MonsterSize.titanic =>
|
||||
_largeIdleFrames(category),
|
||||
MonsterSize.titanic => _largeIdleFrames(category),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -295,8 +294,7 @@ List<List<String>> _getMonsterHitFrames(
|
||||
MonsterSize.large ||
|
||||
MonsterSize.huge ||
|
||||
MonsterSize.giant ||
|
||||
MonsterSize.titanic =>
|
||||
_largeHitFrames(category),
|
||||
MonsterSize.titanic => _largeHitFrames(category),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -311,8 +309,7 @@ List<List<String>> _getMonsterAlertFrames(
|
||||
MonsterSize.large ||
|
||||
MonsterSize.huge ||
|
||||
MonsterSize.giant ||
|
||||
MonsterSize.titanic =>
|
||||
_largeAlertFrames(category),
|
||||
MonsterSize.titanic => _largeAlertFrames(category),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -483,7 +480,7 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
||||
r' > ^ <',
|
||||
r' /| |\',
|
||||
r' | | | |',
|
||||
r'_|_| |_|_'
|
||||
r'_|_| |_|_',
|
||||
],
|
||||
[
|
||||
r' /\_/\',
|
||||
@@ -491,7 +488,7 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
||||
r' > v <',
|
||||
r' \| |/',
|
||||
r' | | | |',
|
||||
r'_|_| |_|_'
|
||||
r'_|_| |_|_',
|
||||
],
|
||||
],
|
||||
MonsterCategory.malware => [
|
||||
@@ -501,7 +498,7 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
||||
r' \ /',
|
||||
r' / \',
|
||||
r' \/ \/',
|
||||
r' _/ \_'
|
||||
r' _/ \_',
|
||||
],
|
||||
[
|
||||
r' \/\/\',
|
||||
@@ -509,7 +506,7 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
||||
r' / \',
|
||||
r' \ /',
|
||||
r' /\ /\',
|
||||
r' _\ /_'
|
||||
r' _\ /_',
|
||||
],
|
||||
],
|
||||
MonsterCategory.network => [
|
||||
@@ -517,22 +514,8 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
||||
[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'_\ | /_'
|
||||
],
|
||||
[r' _+_', r' (X_X)', r' /|\', r' / | \', r' | | |', r'_/ | \_'],
|
||||
[r' _x_', r' (x_x)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'],
|
||||
],
|
||||
MonsterCategory.crypto => [
|
||||
[
|
||||
@@ -541,7 +524,7 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
||||
r' / \',
|
||||
r' / \',
|
||||
r' | |',
|
||||
r'<__ __>'
|
||||
r'<__ __>',
|
||||
],
|
||||
[
|
||||
r' __',
|
||||
@@ -549,12 +532,26 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
||||
r' \ /',
|
||||
r' \ /',
|
||||
r' | |',
|
||||
r'<__ __>'
|
||||
r'<__ __>',
|
||||
],
|
||||
],
|
||||
MonsterCategory.ai => [
|
||||
[r' ____', r' / \', r' ( )', r' ( )', r' \ /', r' \__/'],
|
||||
[r' __', r' / \', 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' V V', r' _/ \_'],
|
||||
@@ -579,7 +576,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
||||
r' > ! <',
|
||||
r' /| |\',
|
||||
r' | | | |',
|
||||
r'_|_| |_|_'
|
||||
r'_|_| |_|_',
|
||||
],
|
||||
[
|
||||
r' /\_/\',
|
||||
@@ -587,7 +584,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
||||
r' > ! <',
|
||||
r' \| |/',
|
||||
r' | | | |',
|
||||
r'_|_| |_|_'
|
||||
r'_|_| |_|_',
|
||||
],
|
||||
],
|
||||
MonsterCategory.malware => [
|
||||
@@ -597,7 +594,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
||||
r' \ /',
|
||||
r' / \',
|
||||
r' \/ \/',
|
||||
r' _/ \_'
|
||||
r' _/ \_',
|
||||
],
|
||||
[
|
||||
r' \/\/\',
|
||||
@@ -605,7 +602,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
||||
r' / \',
|
||||
r' \ /',
|
||||
r' /\ /\',
|
||||
r' _\ /_'
|
||||
r' _\ /_',
|
||||
],
|
||||
],
|
||||
MonsterCategory.network => [
|
||||
@@ -613,22 +610,8 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
||||
[r' !O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'],
|
||||
],
|
||||
MonsterCategory.system => [
|
||||
[
|
||||
r' _!_',
|
||||
r' (!_!)',
|
||||
r' /|\',
|
||||
r' / | \',
|
||||
r' | | |',
|
||||
r'_/ | \_'
|
||||
],
|
||||
[
|
||||
r' _!_',
|
||||
r' (!_!)',
|
||||
r' \|/',
|
||||
r' \ | /',
|
||||
r' | | |',
|
||||
r'_\ | /_'
|
||||
],
|
||||
[r' _!_', r' (!_!)', r' /|\', r' / | \', r' | | |', r'_/ | \_'],
|
||||
[r' _!_', r' (!_!)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'],
|
||||
],
|
||||
MonsterCategory.crypto => [
|
||||
[
|
||||
@@ -637,7 +620,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
||||
r' / \',
|
||||
r' / \',
|
||||
r' | |',
|
||||
r'<__ __>'
|
||||
r'<__ __>',
|
||||
],
|
||||
[
|
||||
r' __',
|
||||
@@ -645,7 +628,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
||||
r' \ /',
|
||||
r' \ /',
|
||||
r' | |',
|
||||
r'<__ __>'
|
||||
r'<__ __>',
|
||||
],
|
||||
],
|
||||
MonsterCategory.ai => [
|
||||
@@ -655,9 +638,16 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
||||
r' ( ! )',
|
||||
r' ( ! )',
|
||||
r' \ /',
|
||||
r' \__/'
|
||||
r' \__/',
|
||||
],
|
||||
[
|
||||
r' __',
|
||||
r' / !\',
|
||||
r' / ! \',
|
||||
r' { ! }',
|
||||
r' \ /',
|
||||
r' \__/',
|
||||
],
|
||||
[r' __', r' / !\', r' / ! \', r' { ! }', r' \ /', r' \__/'],
|
||||
],
|
||||
MonsterCategory.boss => [
|
||||
[r' ^!^', r' (|!|)', r' /|\', r' / | \', r' V V', r' _/ \_'],
|
||||
@@ -681,7 +671,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
||||
r' | | | |',
|
||||
r' | | | |',
|
||||
r'_| | | |_',
|
||||
r'|__|____|__|'
|
||||
r'|__|____|__|',
|
||||
],
|
||||
[
|
||||
r' /\__/\',
|
||||
@@ -691,7 +681,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
||||
r' | | | |',
|
||||
r' | | | |',
|
||||
r'_| | | |_',
|
||||
r'|__|____|__|'
|
||||
r'|__|____|__|',
|
||||
],
|
||||
],
|
||||
MonsterCategory.malware => [
|
||||
@@ -703,7 +693,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
||||
r' / \',
|
||||
r' \/ \/',
|
||||
r' _/ \_',
|
||||
r'/__ __\\'
|
||||
r'/__ __\\',
|
||||
],
|
||||
[
|
||||
r' \/\/\',
|
||||
@@ -713,7 +703,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
||||
r' \ /',
|
||||
r' /\ /\',
|
||||
r' _\ /_',
|
||||
r'\__ __/'
|
||||
r'\__ __/',
|
||||
],
|
||||
],
|
||||
MonsterCategory.network => [
|
||||
@@ -725,7 +715,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
||||
r' | |',
|
||||
r' | |',
|
||||
r' _| |_',
|
||||
r'|__ __|'
|
||||
r'|__ __|',
|
||||
],
|
||||
[
|
||||
r' O',
|
||||
@@ -735,7 +725,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
||||
r' | |',
|
||||
r' | |',
|
||||
r' _/ \_',
|
||||
r'/__ __\\'
|
||||
r'/__ __\\',
|
||||
],
|
||||
],
|
||||
MonsterCategory.system => [
|
||||
@@ -747,7 +737,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
||||
r' | | |',
|
||||
r' | | |',
|
||||
r' _/ | \_',
|
||||
r'|____|____|'
|
||||
r'|____|____|',
|
||||
],
|
||||
[
|
||||
r' _x_',
|
||||
@@ -757,7 +747,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
||||
r' | | |',
|
||||
r' | | |',
|
||||
r' _\ | /_',
|
||||
r'|____|____|'
|
||||
r'|____|____|',
|
||||
],
|
||||
],
|
||||
MonsterCategory.crypto => [
|
||||
@@ -769,7 +759,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
||||
r' | |',
|
||||
r' | |',
|
||||
r' <__ __>',
|
||||
r'|___ ___|'
|
||||
r'|___ ___|',
|
||||
],
|
||||
[
|
||||
r' ___',
|
||||
@@ -779,7 +769,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
||||
r' | |',
|
||||
r' | |',
|
||||
r' <__ __>',
|
||||
r'|___ ___|'
|
||||
r'|___ ___|',
|
||||
],
|
||||
],
|
||||
MonsterCategory.ai => [
|
||||
@@ -791,7 +781,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
||||
r' ( )',
|
||||
r' \ /',
|
||||
r' \ /',
|
||||
r' \_/'
|
||||
r' \_/',
|
||||
],
|
||||
[
|
||||
r' ___',
|
||||
@@ -801,7 +791,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
||||
r' { }',
|
||||
r' \ /',
|
||||
r' \ /',
|
||||
r' \_/'
|
||||
r' \_/',
|
||||
],
|
||||
],
|
||||
MonsterCategory.boss => [
|
||||
@@ -813,7 +803,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
||||
r' | | |',
|
||||
r' V | V',
|
||||
r' _/ | \_',
|
||||
r'|_____|_____|'
|
||||
r'|_____|_____|',
|
||||
],
|
||||
[
|
||||
r' ^w^',
|
||||
@@ -823,7 +813,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
||||
r' | | |',
|
||||
r' v | v',
|
||||
r' _\ | /_',
|
||||
r'|_____|_____|'
|
||||
r'|_____|_____|',
|
||||
],
|
||||
],
|
||||
};
|
||||
@@ -839,7 +829,7 @@ List<List<String>> _largeHitFrames(MonsterCategory category) {
|
||||
r' | | |',
|
||||
r' X | X',
|
||||
r' _/ | \_',
|
||||
r'|_____|_____|'
|
||||
r'|_____|_____|',
|
||||
],
|
||||
[
|
||||
r' !*!',
|
||||
@@ -849,7 +839,7 @@ List<List<String>> _largeHitFrames(MonsterCategory category) {
|
||||
r' | | |',
|
||||
r' x | x',
|
||||
r' _\ | /_',
|
||||
r'|_____|_____|'
|
||||
r'|_____|_____|',
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -865,7 +855,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||
r' | | | |',
|
||||
r' | | | |',
|
||||
r'_| | | |_',
|
||||
r'|__|____|__|'
|
||||
r'|__|____|__|',
|
||||
],
|
||||
[
|
||||
r' /\__/\',
|
||||
@@ -875,7 +865,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||
r' | | | |',
|
||||
r' | | | |',
|
||||
r'_| | | |_',
|
||||
r'|__|____|__|'
|
||||
r'|__|____|__|',
|
||||
],
|
||||
],
|
||||
MonsterCategory.malware => [
|
||||
@@ -887,7 +877,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||
r' / \',
|
||||
r' \/ \/',
|
||||
r' _/ \_',
|
||||
r'/__ __\\'
|
||||
r'/__ __\\',
|
||||
],
|
||||
[
|
||||
r' \/\/\',
|
||||
@@ -897,7 +887,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||
r' \ /',
|
||||
r' /\ /\',
|
||||
r' _\ /_',
|
||||
r'\__ __/'
|
||||
r'\__ __/',
|
||||
],
|
||||
],
|
||||
MonsterCategory.network => [
|
||||
@@ -909,7 +899,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||
r' | |',
|
||||
r' | |',
|
||||
r' _| |_',
|
||||
r'|__ __|'
|
||||
r'|__ __|',
|
||||
],
|
||||
[
|
||||
r' !O',
|
||||
@@ -919,7 +909,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||
r' | |',
|
||||
r' | |',
|
||||
r' _/ \_',
|
||||
r'/__ __\\'
|
||||
r'/__ __\\',
|
||||
],
|
||||
],
|
||||
MonsterCategory.system => [
|
||||
@@ -931,7 +921,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||
r' | | |',
|
||||
r' | | |',
|
||||
r' _/ | \_',
|
||||
r'|____|____|'
|
||||
r'|____|____|',
|
||||
],
|
||||
[
|
||||
r' _!_',
|
||||
@@ -941,7 +931,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||
r' | | |',
|
||||
r' | | |',
|
||||
r' _\ | /_',
|
||||
r'|____|____|'
|
||||
r'|____|____|',
|
||||
],
|
||||
],
|
||||
MonsterCategory.crypto => [
|
||||
@@ -953,7 +943,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||
r' | |',
|
||||
r' | |',
|
||||
r' <__ __>',
|
||||
r'|___ ___|'
|
||||
r'|___ ___|',
|
||||
],
|
||||
[
|
||||
r' ___',
|
||||
@@ -963,7 +953,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||
r' | |',
|
||||
r' | |',
|
||||
r' <__ __>',
|
||||
r'|___ ___|'
|
||||
r'|___ ___|',
|
||||
],
|
||||
],
|
||||
MonsterCategory.ai => [
|
||||
@@ -975,7 +965,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||
r' ( ! )',
|
||||
r' \ /',
|
||||
r' \ /',
|
||||
r' \_/'
|
||||
r' \_/',
|
||||
],
|
||||
[
|
||||
r' ___',
|
||||
@@ -985,7 +975,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||
r' { ! }',
|
||||
r' \ /',
|
||||
r' \ /',
|
||||
r' \_/'
|
||||
r' \_/',
|
||||
],
|
||||
],
|
||||
MonsterCategory.boss => [
|
||||
@@ -997,7 +987,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||
r' | | |',
|
||||
r' V | V',
|
||||
r' _/ | \_',
|
||||
r'|_____|_____|'
|
||||
r'|_____|_____|',
|
||||
],
|
||||
[
|
||||
r' ^!^',
|
||||
@@ -1007,7 +997,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||
r' | | |',
|
||||
r' v | v',
|
||||
r' _\ | /_',
|
||||
r'|_____|_____|'
|
||||
r'|_____|_____|',
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
@@ -20,12 +20,18 @@ class CanvasSpecialComposer {
|
||||
) {
|
||||
return switch (type) {
|
||||
AsciiAnimationType.levelUp => _composeLevelUp(frameIndex, globalTick),
|
||||
AsciiAnimationType.questComplete =>
|
||||
_composeQuestComplete(frameIndex, globalTick),
|
||||
AsciiAnimationType.actComplete =>
|
||||
_composeActComplete(frameIndex, globalTick),
|
||||
AsciiAnimationType.resurrection =>
|
||||
_composeResurrection(frameIndex, globalTick),
|
||||
AsciiAnimationType.questComplete => _composeQuestComplete(
|
||||
frameIndex,
|
||||
globalTick,
|
||||
),
|
||||
AsciiAnimationType.actComplete => _composeActComplete(
|
||||
frameIndex,
|
||||
globalTick,
|
||||
),
|
||||
AsciiAnimationType.resurrection => _composeResurrection(
|
||||
frameIndex,
|
||||
globalTick,
|
||||
),
|
||||
_ => [AsciiLayer.empty()],
|
||||
};
|
||||
}
|
||||
@@ -44,7 +50,8 @@ class CanvasSpecialComposer {
|
||||
final layers = <AsciiLayer>[
|
||||
_createEffectBackground(globalTick, '+'),
|
||||
_createCenteredSprite(
|
||||
_questCompleteFrames[frameIndex % _questCompleteFrames.length]),
|
||||
_questCompleteFrames[frameIndex % _questCompleteFrames.length],
|
||||
),
|
||||
];
|
||||
return layers;
|
||||
}
|
||||
@@ -54,7 +61,8 @@ class CanvasSpecialComposer {
|
||||
final layers = <AsciiLayer>[
|
||||
_createEffectBackground(globalTick, '~'),
|
||||
_createCenteredSprite(
|
||||
_actCompleteFrames[frameIndex % _actCompleteFrames.length]),
|
||||
_actCompleteFrames[frameIndex % _actCompleteFrames.length],
|
||||
),
|
||||
];
|
||||
return layers;
|
||||
}
|
||||
@@ -64,7 +72,8 @@ class CanvasSpecialComposer {
|
||||
final layers = <AsciiLayer>[
|
||||
_createEffectBackground(globalTick, '.'),
|
||||
_createCenteredSprite(
|
||||
_resurrectionFrames[frameIndex % _resurrectionFrames.length]),
|
||||
_resurrectionFrames[frameIndex % _resurrectionFrames.length],
|
||||
),
|
||||
];
|
||||
return layers;
|
||||
}
|
||||
@@ -119,41 +128,11 @@ class CanvasSpecialComposer {
|
||||
// ============================================================================
|
||||
|
||||
const _levelUpFrames = [
|
||||
[
|
||||
r' * ',
|
||||
r' \|/ ',
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
],
|
||||
[
|
||||
r' * * ',
|
||||
r' \|/ ',
|
||||
r' O ',
|
||||
r' </|\> ',
|
||||
r' / \ ',
|
||||
],
|
||||
[
|
||||
r' * * * ',
|
||||
r' \|/ ',
|
||||
r' O ',
|
||||
r' <\|/> ',
|
||||
r' / \ ',
|
||||
],
|
||||
[
|
||||
r' * * * * ',
|
||||
r' LEVEL ',
|
||||
r' UP! ',
|
||||
r' \O/ ',
|
||||
r' / \ ',
|
||||
],
|
||||
[
|
||||
r'* * * * *',
|
||||
r' LEVEL ',
|
||||
r' UP! ',
|
||||
r' \O/ ',
|
||||
r' | | ',
|
||||
],
|
||||
[r' * ', r' \|/ ', r' o ', r' /|\ ', r' / \ '],
|
||||
[r' * * ', r' \|/ ', r' O ', r' </|\> ', r' / \ '],
|
||||
[r' * * * ', r' \|/ ', r' O ', r' <\|/> ', r' / \ '],
|
||||
[r' * * * * ', r' LEVEL ', r' UP! ', r' \O/ ', r' / \ '],
|
||||
[r'* * * * *', r' LEVEL ', r' UP! ', r' \O/ ', r' | | '],
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
@@ -161,34 +140,10 @@ const _levelUpFrames = [
|
||||
// ============================================================================
|
||||
|
||||
const _questCompleteFrames = [
|
||||
[
|
||||
r' [?] ',
|
||||
r' | ',
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
],
|
||||
[
|
||||
r' [???] ',
|
||||
r' | ',
|
||||
r' o! ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
],
|
||||
[
|
||||
r' [DONE] ',
|
||||
r' ! ',
|
||||
r' \o/ ',
|
||||
r' | ',
|
||||
r' / \ ',
|
||||
],
|
||||
[
|
||||
r' +[DONE]+',
|
||||
r' \!/ ',
|
||||
r' \o/ ',
|
||||
r' | ',
|
||||
r' / \ ',
|
||||
],
|
||||
[r' [?] ', r' | ', r' o ', r' /|\ ', r' / \ '],
|
||||
[r' [???] ', r' | ', r' o! ', r' /|\ ', r' / \ '],
|
||||
[r' [DONE] ', r' ! ', r' \o/ ', r' | ', r' / \ '],
|
||||
[r' +[DONE]+', r' \!/ ', r' \o/ ', r' | ', r' / \ '],
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
@@ -196,34 +151,10 @@ const _questCompleteFrames = [
|
||||
// ============================================================================
|
||||
|
||||
const _actCompleteFrames = [
|
||||
[
|
||||
r'=========',
|
||||
r' ACT ',
|
||||
r' CLEAR ',
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
],
|
||||
[
|
||||
r'~~~~~~~~~',
|
||||
r' ACT ',
|
||||
r' CLEAR! ',
|
||||
r' \o/ ',
|
||||
r' | ',
|
||||
],
|
||||
[
|
||||
r'*~*~*~*~*',
|
||||
r' ACT ',
|
||||
r' CLEAR!! ',
|
||||
r' \O/ ',
|
||||
r' / \ ',
|
||||
],
|
||||
[
|
||||
r'*********',
|
||||
r' ACT ',
|
||||
r' CLEAR!! ',
|
||||
r' \O/ ',
|
||||
r' | | ',
|
||||
],
|
||||
[r'=========', r' ACT ', r' CLEAR ', r' o ', r' /|\ '],
|
||||
[r'~~~~~~~~~', r' ACT ', r' CLEAR! ', r' \o/ ', r' | '],
|
||||
[r'*~*~*~*~*', r' ACT ', r' CLEAR!! ', r' \O/ ', r' / \ '],
|
||||
[r'*********', r' ACT ', r' CLEAR!! ', r' \O/ ', r' | | '],
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
@@ -232,38 +163,13 @@ const _actCompleteFrames = [
|
||||
|
||||
const _resurrectionFrames = [
|
||||
// 프레임 1: R.I.P 묘비
|
||||
[
|
||||
r' ___ ',
|
||||
r' |RIP| ',
|
||||
r' | | ',
|
||||
r'__|___|__',
|
||||
],
|
||||
[r' ___ ', r' |RIP| ', r' | | ', r'__|___|__'],
|
||||
// 프레임 2: 빛 내림
|
||||
[
|
||||
r' \|/ ',
|
||||
r' -|R|- ',
|
||||
r' | | ',
|
||||
r'__|___|__',
|
||||
],
|
||||
[r' \|/ ', r' -|R|- ', r' | | ', r'__|___|__'],
|
||||
// 프레임 3: 일어남
|
||||
[
|
||||
r' \o/ ',
|
||||
r' --|-- ',
|
||||
r' | | ',
|
||||
r'__|___|__',
|
||||
],
|
||||
[r' \o/ ', r' --|-- ', r' | | ', r'__|___|__'],
|
||||
// 프레임 4: 서있음
|
||||
[
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
r'_________',
|
||||
],
|
||||
[r' o ', r' /|\ ', r' / \ ', r'_________'],
|
||||
// 프레임 5: 부활 완료
|
||||
[
|
||||
r' REVIVED ',
|
||||
r' \o/ ',
|
||||
r' | ',
|
||||
r'___/ \___',
|
||||
],
|
||||
[r' REVIVED ', r' \o/ ', r' | ', r'___/ \___'],
|
||||
];
|
||||
|
||||
@@ -63,12 +63,7 @@ class CanvasTownComposer {
|
||||
const shopX = 32;
|
||||
final shopY = frameHeight - cells.length - 1;
|
||||
|
||||
return AsciiLayer(
|
||||
cells: cells,
|
||||
zIndex: 1,
|
||||
offsetX: shopX,
|
||||
offsetY: shopY,
|
||||
);
|
||||
return AsciiLayer(cells: cells, zIndex: 1, offsetX: shopX, offsetY: shopY);
|
||||
}
|
||||
|
||||
/// 캐릭터 레이어 생성 (z=2)
|
||||
@@ -82,12 +77,7 @@ class CanvasTownComposer {
|
||||
const charX = 25;
|
||||
final charY = frameHeight - cells.length - 1;
|
||||
|
||||
return AsciiLayer(
|
||||
cells: cells,
|
||||
zIndex: 2,
|
||||
offsetX: charX,
|
||||
offsetY: charY,
|
||||
);
|
||||
return AsciiLayer(cells: cells, zIndex: 2, offsetX: charX, offsetY: charY);
|
||||
}
|
||||
|
||||
/// 문자열 스프라이트를 AsciiCell 2D 배열로 변환
|
||||
@@ -104,27 +94,11 @@ class CanvasTownComposer {
|
||||
|
||||
const _shopIdleFrames = [
|
||||
// 프레임 1: 기본
|
||||
[
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
],
|
||||
[r' o ', r' /|\ ', r' / \ '],
|
||||
// 프레임 2: 머리 숙임
|
||||
[
|
||||
r' o ',
|
||||
r' /|~ ',
|
||||
r' / \ ',
|
||||
],
|
||||
[r' o ', r' /|~ ', r' / \ '],
|
||||
// 프레임 3: 물건 보기
|
||||
[
|
||||
r' o? ',
|
||||
r' /| ',
|
||||
r' / \ ',
|
||||
],
|
||||
[r' o? ', r' /| ', r' / \ '],
|
||||
// 프레임 4: 고개 끄덕
|
||||
[
|
||||
r' o! ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
],
|
||||
[r' o! ', r' /|\ ', r' / \ '],
|
||||
];
|
||||
|
||||
@@ -62,26 +62,10 @@ CharacterFrame getCharacterFrame(BattlePhase phase, int subFrame) {
|
||||
// 구조: [머리, 몸통+팔, 다리]
|
||||
// ============================================================================
|
||||
const _idleFrames = [
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' | | ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' O ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' | | ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' O ', r' /|\ ', r' / \ ']),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
@@ -89,21 +73,9 @@ const _idleFrames = [
|
||||
// 구조: [머리, 몸통+팔, 다리]
|
||||
// ============================================================================
|
||||
const _prepareFrames = [
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' \|\ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o_ ',
|
||||
r' \| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o/ ',
|
||||
r' \| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([r' o ', r' \|\ ', r' / \ ']),
|
||||
CharacterFrame([r' o_ ', r' \| ', r' / \ ']),
|
||||
CharacterFrame([r' o/ ', r' \| ', r' / \ ']),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
@@ -112,31 +84,11 @@ const _prepareFrames = [
|
||||
// 수정: 공격 이펙트를 머리 줄로 통일 (1칸 위로)
|
||||
// ============================================================================
|
||||
const _attackFrames = [
|
||||
CharacterFrame([
|
||||
r' o\ ',
|
||||
r' /| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o- ',
|
||||
r' /| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o-- ',
|
||||
r' /| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o-=>',
|
||||
r' /| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([r' o\ ', r' /| ', r' / \ ']),
|
||||
CharacterFrame([r' o- ', r' /| ', r' / \ ']),
|
||||
CharacterFrame([r' o-- ', r' /| ', r' / \ ']),
|
||||
CharacterFrame([r' o-=>', r' /| ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
@@ -145,21 +97,9 @@ const _attackFrames = [
|
||||
// 수정: 히트 이펙트를 머리 줄로 통일 (1칸 위로)
|
||||
// ============================================================================
|
||||
const _hitFrames = [
|
||||
CharacterFrame([
|
||||
r' o-* ',
|
||||
r' /| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o=* ',
|
||||
r' /| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o~* ',
|
||||
r' /| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([r' o-* ', r' /| ', r' / \ ']),
|
||||
CharacterFrame([r' o=* ', r' /| ', r' / \ ']),
|
||||
CharacterFrame([r' o~* ', r' /| ', r' / \ ']),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
@@ -167,19 +107,7 @@ const _hitFrames = [
|
||||
// 구조: [머리, 몸통+팔, 다리]
|
||||
// ============================================================================
|
||||
const _recoverFrames = [
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' | ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' | ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||
];
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
// 몬스터 카테고리별 색상 시스템
|
||||
// 각 몬스터 카테고리에 따라 다른 색상 적용
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
/// 몬스터 카테고리 (ascii_animation_data.dart의 MonsterCategory와 매칭)
|
||||
enum MonsterColorCategory {
|
||||
beast,
|
||||
insect,
|
||||
humanoid,
|
||||
undead,
|
||||
dragon,
|
||||
slime,
|
||||
demon,
|
||||
}
|
||||
|
||||
/// 몬스터 색상 정보
|
||||
class MonsterColors {
|
||||
const MonsterColors({
|
||||
required this.normal,
|
||||
required this.hit,
|
||||
});
|
||||
|
||||
/// 일반 상태 색상
|
||||
final Color normal;
|
||||
|
||||
/// 피격 상태 색상
|
||||
final Color hit;
|
||||
}
|
||||
|
||||
/// 카테고리별 몬스터 색상 반환
|
||||
MonsterColors getMonsterColors(MonsterColorCategory category) {
|
||||
return switch (category) {
|
||||
MonsterColorCategory.beast => const MonsterColors(
|
||||
normal: Color(0xFF00FF00), // 녹색
|
||||
hit: Color(0xFFFF0000), // 빨강
|
||||
),
|
||||
MonsterColorCategory.insect => const MonsterColors(
|
||||
normal: Color(0xFFFFFF00), // 노랑
|
||||
hit: Color(0xFFFF6600), // 주황
|
||||
),
|
||||
MonsterColorCategory.humanoid => const MonsterColors(
|
||||
normal: Color(0xFF00FFFF), // 시안
|
||||
hit: Color(0xFFFF00FF), // 마젠타
|
||||
),
|
||||
MonsterColorCategory.undead => const MonsterColors(
|
||||
normal: Color(0xFF9966FF), // 보라
|
||||
hit: Color(0xFFCCCCCC), // 회색
|
||||
),
|
||||
MonsterColorCategory.dragon => const MonsterColors(
|
||||
normal: Color(0xFFFF6600), // 주황
|
||||
hit: Color(0xFFFFFF00), // 노랑
|
||||
),
|
||||
MonsterColorCategory.slime => const MonsterColors(
|
||||
normal: Color(0xFF66FF66), // 연녹색
|
||||
hit: Color(0xFF00CC00), // 진녹색
|
||||
),
|
||||
MonsterColorCategory.demon => const MonsterColors(
|
||||
normal: Color(0xFFFF0066), // 핑크
|
||||
hit: Color(0xFFFFFFFF), // 흰색
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// 몬스터 기본 이름에서 색상 카테고리 추론
|
||||
///
|
||||
/// ascii_animation_data.dart의 getMonsterCategory 결과를 변환
|
||||
MonsterColorCategory getMonsterColorCategory(String? baseName) {
|
||||
if (baseName == null || baseName.isEmpty) {
|
||||
return MonsterColorCategory.beast;
|
||||
}
|
||||
|
||||
final lower = baseName.toLowerCase();
|
||||
|
||||
// insect (곤충류)
|
||||
if (_matchesAny(lower, _insectKeywords)) {
|
||||
return MonsterColorCategory.insect;
|
||||
}
|
||||
|
||||
// undead (언데드)
|
||||
if (_matchesAny(lower, _undeadKeywords)) {
|
||||
return MonsterColorCategory.undead;
|
||||
}
|
||||
|
||||
// dragon (드래곤류)
|
||||
if (_matchesAny(lower, _dragonKeywords)) {
|
||||
return MonsterColorCategory.dragon;
|
||||
}
|
||||
|
||||
// slime (슬라임류)
|
||||
if (_matchesAny(lower, _slimeKeywords)) {
|
||||
return MonsterColorCategory.slime;
|
||||
}
|
||||
|
||||
// demon (악마류)
|
||||
if (_matchesAny(lower, _demonKeywords)) {
|
||||
return MonsterColorCategory.demon;
|
||||
}
|
||||
|
||||
// humanoid (인간형)
|
||||
if (_matchesAny(lower, _humanoidKeywords)) {
|
||||
return MonsterColorCategory.humanoid;
|
||||
}
|
||||
|
||||
// 기본은 beast
|
||||
return MonsterColorCategory.beast;
|
||||
}
|
||||
|
||||
bool _matchesAny(String text, List<String> keywords) {
|
||||
return keywords.any((kw) => text.contains(kw));
|
||||
}
|
||||
|
||||
const _insectKeywords = [
|
||||
'bug',
|
||||
'beetle',
|
||||
'spider',
|
||||
'ant',
|
||||
'bee',
|
||||
'wasp',
|
||||
'moth',
|
||||
'worm',
|
||||
'larva',
|
||||
'crawler',
|
||||
'centipede',
|
||||
'scorpion',
|
||||
];
|
||||
|
||||
const _undeadKeywords = [
|
||||
'zombie',
|
||||
'skeleton',
|
||||
'ghost',
|
||||
'wraith',
|
||||
'vampire',
|
||||
'lich',
|
||||
'specter',
|
||||
'phantom',
|
||||
'revenant',
|
||||
'undead',
|
||||
'corpse',
|
||||
'bone',
|
||||
];
|
||||
|
||||
const _dragonKeywords = [
|
||||
'dragon',
|
||||
'drake',
|
||||
'wyrm',
|
||||
'wyvern',
|
||||
'serpent',
|
||||
'hydra',
|
||||
'basilisk',
|
||||
];
|
||||
|
||||
const _slimeKeywords = [
|
||||
'slime',
|
||||
'ooze',
|
||||
'blob',
|
||||
'jelly',
|
||||
'pudding',
|
||||
'gel',
|
||||
'goo',
|
||||
];
|
||||
|
||||
const _demonKeywords = [
|
||||
'demon',
|
||||
'devil',
|
||||
'imp',
|
||||
'fiend',
|
||||
'daemon',
|
||||
'succubus',
|
||||
'incubus',
|
||||
'hell',
|
||||
'infernal',
|
||||
];
|
||||
|
||||
const _humanoidKeywords = [
|
||||
'goblin',
|
||||
'orc',
|
||||
'troll',
|
||||
'ogre',
|
||||
'giant',
|
||||
'bandit',
|
||||
'knight',
|
||||
'mage',
|
||||
'wizard',
|
||||
'warrior',
|
||||
'guard',
|
||||
'soldier',
|
||||
'cultist',
|
||||
'hacker',
|
||||
'admin',
|
||||
'user',
|
||||
];
|
||||
@@ -65,12 +65,7 @@ bool _matchesAny(String text, List<String> keywords) {
|
||||
|
||||
// 카테고리별 키워드 목록
|
||||
|
||||
const _cosmicKeywords = [
|
||||
'dyson',
|
||||
'black hole',
|
||||
'universe',
|
||||
'singularity',
|
||||
];
|
||||
const _cosmicKeywords = ['dyson', 'black hole', 'universe', 'singularity'];
|
||||
|
||||
const _cableKeywords = [
|
||||
'cable',
|
||||
|
||||
@@ -58,7 +58,7 @@ const _bluntEffect = WeaponEffect(
|
||||
[r' _/ ', r' / ', r'/ '],
|
||||
[r' /__ ', r'/ ', r' '],
|
||||
[r'/__ ', r' ', r' '],
|
||||
[r'/__=>', r' ', r' '],
|
||||
[r'/__=>', r' ', r' '],
|
||||
],
|
||||
hitFrames: [
|
||||
[r' *BASH* ', r'/__=> ', r' '],
|
||||
|
||||
@@ -24,7 +24,8 @@ class CombatCalculator {
|
||||
/// [attacker] 공격자 (플레이어) 스탯
|
||||
/// [defender] 방어자 (몬스터) 스탯
|
||||
/// Returns: 공격 결과 및 업데이트된 몬스터 스탯
|
||||
({AttackResult result, MonsterCombatStats updatedDefender}) playerAttackMonster({
|
||||
({AttackResult result, MonsterCombatStats updatedDefender})
|
||||
playerAttackMonster({
|
||||
required CombatStats attacker,
|
||||
required MonsterCombatStats defender,
|
||||
}) {
|
||||
@@ -178,7 +179,8 @@ class CombatCalculator {
|
||||
}
|
||||
|
||||
// 전투 종료 체크
|
||||
final isCombatOver = currentPlayerStats.isDead || currentMonsterStats.isDead;
|
||||
final isCombatOver =
|
||||
currentPlayerStats.isDead || currentMonsterStats.isDead;
|
||||
final isPlayerVictory = isCombatOver && currentMonsterStats.isDead;
|
||||
|
||||
return CombatTurnResult(
|
||||
@@ -206,7 +208,8 @@ class CombatCalculator {
|
||||
// 플레이어 DPS (초당 데미지)
|
||||
final playerDamagePerHit = math.max(1, player.atk - monster.def * 0.5);
|
||||
final playerHitsPerSecond = 1000 / player.attackDelayMs;
|
||||
final playerDps = playerDamagePerHit * playerHitsPerSecond * player.accuracy;
|
||||
final playerDps =
|
||||
playerDamagePerHit * playerHitsPerSecond * player.accuracy;
|
||||
|
||||
// 몬스터를 처치하는 데 필요한 시간 (밀리초)
|
||||
final timeToKillMonster = (monster.hpMax / playerDps * 1000).round();
|
||||
@@ -225,17 +228,20 @@ class CombatCalculator {
|
||||
// 플레이어 예상 생존 시간
|
||||
final monsterDamagePerHit = math.max(1, monster.atk - player.def * 0.5);
|
||||
final monsterHitsPerSecond = 1000 / monster.attackDelayMs;
|
||||
final monsterDps = monsterDamagePerHit * monsterHitsPerSecond * monster.accuracy;
|
||||
final monsterDps =
|
||||
monsterDamagePerHit * monsterHitsPerSecond * monster.accuracy;
|
||||
final playerSurvivalTime = player.hpCurrent / monsterDps;
|
||||
|
||||
// 몬스터 예상 생존 시간
|
||||
final playerDamagePerHit = math.max(1, player.atk - monster.def * 0.5);
|
||||
final playerHitsPerSecond = 1000 / player.attackDelayMs;
|
||||
final playerDps = playerDamagePerHit * playerHitsPerSecond * player.accuracy;
|
||||
final playerDps =
|
||||
playerDamagePerHit * playerHitsPerSecond * player.accuracy;
|
||||
final monsterSurvivalTime = monster.hpCurrent / playerDps;
|
||||
|
||||
// 난이도 = 몬스터 생존시간 / (플레이어 생존시간 + 몬스터 생존시간)
|
||||
final difficulty = monsterSurvivalTime / (playerSurvivalTime + monsterSurvivalTime);
|
||||
final difficulty =
|
||||
monsterSurvivalTime / (playerSurvivalTime + monsterSurvivalTime);
|
||||
|
||||
return difficulty.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,9 @@ class ItemService {
|
||||
|
||||
// 교체할 슬롯이 있으면 해당 아이템 무게 제외
|
||||
if (replacingSlot != null) {
|
||||
final existingItem = currentItems.where((i) => i.slot == replacingSlot).firstOrNull;
|
||||
final existingItem = currentItems
|
||||
.where((i) => i.slot == replacingSlot)
|
||||
.firstOrNull;
|
||||
if (existingItem != null) {
|
||||
currentWeight -= existingItem.weight;
|
||||
}
|
||||
@@ -70,7 +72,8 @@ class ItemService {
|
||||
|
||||
if (roll < legendaryChance) return ItemRarity.legendary;
|
||||
if (roll < legendaryChance + epicChance) return ItemRarity.epic;
|
||||
if (roll < legendaryChance + epicChance + rareChance) return ItemRarity.rare;
|
||||
if (roll < legendaryChance + epicChance + rareChance)
|
||||
return ItemRarity.rare;
|
||||
if (roll < legendaryChance + epicChance + rareChance + uncommonChance) {
|
||||
return ItemRarity.uncommon;
|
||||
}
|
||||
@@ -112,7 +115,8 @@ class ItemService {
|
||||
|
||||
// 공속 결정 (600ms ~ 1500ms 범위)
|
||||
// 희귀도가 높을수록 공속 변동 폭 증가
|
||||
final speedVariance = 300 + rarity.index * 100; // Common: 300, Legendary: 700
|
||||
final speedVariance =
|
||||
300 + rarity.index * 100; // Common: 300, Legendary: 700
|
||||
final speedOffset = rng.nextInt(speedVariance * 2) - speedVariance;
|
||||
final attackSpeed = (1000 + speedOffset).clamp(600, 1500);
|
||||
|
||||
@@ -133,14 +137,15 @@ class ItemService {
|
||||
ItemStats _generateShieldStats(int baseValue, ItemRarity rarity) {
|
||||
final blockBonus = 0.05 + rarity.index * 0.02;
|
||||
|
||||
return ItemStats(
|
||||
def: baseValue ~/ 2,
|
||||
blockRate: blockBonus,
|
||||
);
|
||||
return ItemStats(def: baseValue ~/ 2, blockRate: blockBonus);
|
||||
}
|
||||
|
||||
/// 방어구 스탯 생성
|
||||
ItemStats _generateArmorStats(int baseValue, ItemRarity rarity, EquipmentSlot slot) {
|
||||
ItemStats _generateArmorStats(
|
||||
int baseValue,
|
||||
ItemRarity rarity,
|
||||
EquipmentSlot slot,
|
||||
) {
|
||||
// 슬롯별 방어력 가중치
|
||||
final defMultiplier = switch (slot) {
|
||||
EquipmentSlot.hauberk => 1.5, // 갑옷류 최고
|
||||
@@ -161,11 +166,7 @@ class ItemService {
|
||||
final hpBonus = rarity.index >= ItemRarity.rare.index ? baseValue ~/ 2 : 0;
|
||||
final evasionBonus = rarity.index >= ItemRarity.epic.index ? 0.01 : 0.0;
|
||||
|
||||
return ItemStats(
|
||||
def: def,
|
||||
hpBonus: hpBonus,
|
||||
evasion: evasionBonus,
|
||||
);
|
||||
return ItemStats(def: def, hpBonus: hpBonus, evasion: evasionBonus);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -173,10 +174,7 @@ class ItemService {
|
||||
// ============================================================================
|
||||
|
||||
/// 무게 계산 (레벨/슬롯 기반)
|
||||
int calculateWeight({
|
||||
required int level,
|
||||
required EquipmentSlot slot,
|
||||
}) {
|
||||
int calculateWeight({required int level, required EquipmentSlot slot}) {
|
||||
// 슬롯별 기본 무게
|
||||
final baseWeight = switch (slot) {
|
||||
EquipmentSlot.weapon => 10,
|
||||
@@ -209,7 +207,11 @@ class ItemService {
|
||||
ItemRarity? rarity,
|
||||
}) {
|
||||
final itemRarity = rarity ?? determineRarity(level);
|
||||
final stats = generateItemStats(level: level, rarity: itemRarity, slot: slot);
|
||||
final stats = generateItemStats(
|
||||
level: level,
|
||||
rarity: itemRarity,
|
||||
slot: slot,
|
||||
);
|
||||
final weight = calculateWeight(level: level, slot: slot);
|
||||
|
||||
return EquipmentItem(
|
||||
@@ -253,7 +255,8 @@ class ItemService {
|
||||
score += stats.mpBonus;
|
||||
|
||||
// 능력치 보너스 (가중치 5배)
|
||||
score += (stats.strBonus +
|
||||
score +=
|
||||
(stats.strBonus +
|
||||
stats.conBonus +
|
||||
stats.dexBonus +
|
||||
stats.intBonus +
|
||||
|
||||
@@ -238,12 +238,16 @@ class PotionService {
|
||||
}) {
|
||||
final potion = PotionData.getById(potionId);
|
||||
if (potion == null) {
|
||||
return PotionPurchaseResult.failed(PotionPurchaseFailReason.potionNotFound);
|
||||
return PotionPurchaseResult.failed(
|
||||
PotionPurchaseFailReason.potionNotFound,
|
||||
);
|
||||
}
|
||||
|
||||
final totalCost = potion.price * count;
|
||||
if (gold < totalCost) {
|
||||
return PotionPurchaseResult.failed(PotionPurchaseFailReason.insufficientGold);
|
||||
return PotionPurchaseResult.failed(
|
||||
PotionPurchaseFailReason.insufficientGold,
|
||||
);
|
||||
}
|
||||
|
||||
final newInventory = inventory.addPotion(potionId, count);
|
||||
@@ -277,13 +281,17 @@ class PotionService {
|
||||
final mpPotion = PotionData.getMpPotionByTier(tier);
|
||||
|
||||
if (hpPotion == null && mpPotion == null) {
|
||||
return PotionPurchaseResult.failed(PotionPurchaseFailReason.potionNotFound);
|
||||
return PotionPurchaseResult.failed(
|
||||
PotionPurchaseFailReason.potionNotFound,
|
||||
);
|
||||
}
|
||||
|
||||
// 사용 가능 골드
|
||||
final spendableGold = (gold * spendRatio).floor();
|
||||
if (spendableGold <= 0) {
|
||||
return PotionPurchaseResult.failed(PotionPurchaseFailReason.insufficientGold);
|
||||
return PotionPurchaseResult.failed(
|
||||
PotionPurchaseFailReason.insufficientGold,
|
||||
);
|
||||
}
|
||||
|
||||
var currentInventory = inventory;
|
||||
@@ -317,7 +325,9 @@ class PotionService {
|
||||
}
|
||||
|
||||
if (totalSpent == 0) {
|
||||
return PotionPurchaseResult.failed(PotionPurchaseFailReason.insufficientGold);
|
||||
return PotionPurchaseResult.failed(
|
||||
PotionPurchaseFailReason.insufficientGold,
|
||||
);
|
||||
}
|
||||
|
||||
return PotionPurchaseResult(
|
||||
@@ -426,10 +436,7 @@ class PotionUseResult {
|
||||
|
||||
/// 실패 결과 생성
|
||||
factory PotionUseResult.failed(PotionUseFailReason reason) {
|
||||
return PotionUseResult(
|
||||
success: false,
|
||||
failReason: reason,
|
||||
);
|
||||
return PotionUseResult(success: false, failReason: reason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,10 +487,7 @@ class PotionPurchaseResult {
|
||||
|
||||
/// 실패 결과 생성
|
||||
factory PotionPurchaseResult.failed(PotionPurchaseFailReason reason) {
|
||||
return PotionPurchaseResult(
|
||||
success: false,
|
||||
failReason: reason,
|
||||
);
|
||||
return PotionPurchaseResult(success: false, failReason: reason);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,9 @@ class ProgressService {
|
||||
),
|
||||
plotStageCount: 1, // Prologue
|
||||
questCount: 0,
|
||||
plotHistory: [HistoryEntry(caption: l10n.taskPrologue, isComplete: false)],
|
||||
plotHistory: [
|
||||
HistoryEntry(caption: l10n.taskPrologue, isComplete: false),
|
||||
],
|
||||
questHistory: const [],
|
||||
);
|
||||
|
||||
@@ -156,13 +158,17 @@ class ProgressService {
|
||||
|
||||
// 스킬 시스템 시간 업데이트 (Phase 3)
|
||||
final skillService = SkillService(rng: state.rng);
|
||||
var skillSystem = skillService.updateElapsedTime(state.skillSystem, clamped);
|
||||
var skillSystem = skillService.updateElapsedTime(
|
||||
state.skillSystem,
|
||||
clamped,
|
||||
);
|
||||
|
||||
// 만료된 버프 정리
|
||||
skillSystem = skillService.cleanupExpiredBuffs(skillSystem);
|
||||
|
||||
// 비전투 시 MP 회복
|
||||
final isInCombat = progress.currentTask.type == TaskType.kill &&
|
||||
final isInCombat =
|
||||
progress.currentTask.type == TaskType.kill &&
|
||||
progress.currentCombat != null &&
|
||||
progress.currentCombat!.isActive;
|
||||
|
||||
@@ -173,7 +179,10 @@ class ProgressService {
|
||||
wis: nextState.stats.wis,
|
||||
);
|
||||
if (mpRegen > 0) {
|
||||
final newMp = (nextState.stats.mp + mpRegen).clamp(0, nextState.stats.mpMax);
|
||||
final newMp = (nextState.stats.mp + mpRegen).clamp(
|
||||
0,
|
||||
nextState.stats.mpMax,
|
||||
);
|
||||
nextState = nextState.copyWith(
|
||||
stats: nextState.stats.copyWith(mpCurrent: newMp),
|
||||
);
|
||||
@@ -193,7 +202,9 @@ class ProgressService {
|
||||
var updatedCombat = progress.currentCombat;
|
||||
var updatedSkillSystem = nextState.skillSystem;
|
||||
var updatedPotionInventory = nextState.potionInventory;
|
||||
if (progress.currentTask.type == TaskType.kill && updatedCombat != null && updatedCombat.isActive) {
|
||||
if (progress.currentTask.type == TaskType.kill &&
|
||||
updatedCombat != null &&
|
||||
updatedCombat.isActive) {
|
||||
final combatResult = _processCombatTickWithSkills(
|
||||
nextState,
|
||||
updatedCombat,
|
||||
@@ -480,7 +491,8 @@ class ProgressService {
|
||||
final questMonster = state.progress.currentQuestMonster;
|
||||
final questMonsterData = questMonster?.monsterData;
|
||||
final questLevel = questMonsterData != null
|
||||
? int.tryParse(questMonsterData.split('|').elementAtOrNull(1) ?? '') ?? 0
|
||||
? int.tryParse(questMonsterData.split('|').elementAtOrNull(1) ?? '') ??
|
||||
0
|
||||
: null;
|
||||
|
||||
final monsterResult = pq_logic.monsterTask(
|
||||
@@ -501,10 +513,9 @@ class ProgressService {
|
||||
// 전투용 몬스터 레벨 조정 (밸런스)
|
||||
// config의 raw 레벨이 플레이어보다 너무 높으면 전투가 불가능
|
||||
// 플레이어 레벨 ±3 범위로 제한 (최소 1)
|
||||
final effectiveMonsterLevel = monsterResult.level.clamp(
|
||||
math.max(1, level - 3),
|
||||
level + 3,
|
||||
).toInt();
|
||||
final effectiveMonsterLevel = monsterResult.level
|
||||
.clamp(math.max(1, level - 3), level + 3)
|
||||
.toInt();
|
||||
|
||||
final monsterCombatStats = MonsterCombatStats.fromLevel(
|
||||
name: monsterResult.displayName,
|
||||
@@ -907,7 +918,8 @@ class ProgressService {
|
||||
if (hasItemsToSell) {
|
||||
// 다음 아이템 판매 태스크 시작
|
||||
final nextItem = items.first;
|
||||
final itemDesc = l10n.indefiniteL10n(nextItem.name, nextItem.count);
|
||||
final translatedName = l10n.translateItemNameL10n(nextItem.name);
|
||||
final itemDesc = l10n.indefiniteL10n(translatedName, nextItem.count);
|
||||
final taskResult = pq_logic.startTask(
|
||||
state.progress,
|
||||
l10n.taskSelling(itemDesc),
|
||||
@@ -945,7 +957,8 @@ class ProgressService {
|
||||
CombatState combat,
|
||||
SkillSystemState skillSystem,
|
||||
PotionInventory? potionInventory,
|
||||
}) _processCombatTickWithSkills(
|
||||
})
|
||||
_processCombatTickWithSkills(
|
||||
GameState state,
|
||||
CombatState combat,
|
||||
SkillSystemState skillSystem,
|
||||
@@ -988,12 +1001,14 @@ class ProgressService {
|
||||
dotDamageThisTick += damage;
|
||||
|
||||
// DOT 데미지 이벤트 생성
|
||||
newEvents.add(CombatEvent.dotTick(
|
||||
timestamp: timestamp,
|
||||
skillName: dot.skillId,
|
||||
damage: damage,
|
||||
targetName: monsterStats.name,
|
||||
));
|
||||
newEvents.add(
|
||||
CombatEvent.dotTick(
|
||||
timestamp: timestamp,
|
||||
skillName: dot.skillId,
|
||||
damage: damage,
|
||||
targetName: monsterStats.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 만료되지 않은 DOT만 유지
|
||||
@@ -1004,8 +1019,10 @@ class ProgressService {
|
||||
|
||||
// DOT 데미지 적용
|
||||
if (dotDamageThisTick > 0 && monsterStats.isAlive) {
|
||||
final newMonsterHp = (monsterStats.hpCurrent - dotDamageThisTick)
|
||||
.clamp(0, monsterStats.hpMax);
|
||||
final newMonsterHp = (monsterStats.hpCurrent - dotDamageThisTick).clamp(
|
||||
0,
|
||||
monsterStats.hpMax,
|
||||
);
|
||||
monsterStats = monsterStats.copyWith(hpCurrent: newMonsterHp);
|
||||
totalDamageDealt += dotDamageThisTick;
|
||||
}
|
||||
@@ -1024,8 +1041,7 @@ class ProgressService {
|
||||
playerLevel: state.traits.level,
|
||||
);
|
||||
|
||||
if (emergencyPotion != null &&
|
||||
!usedPotionTypes.contains(PotionType.hp)) {
|
||||
if (emergencyPotion != null && !usedPotionTypes.contains(PotionType.hp)) {
|
||||
final result = potionService.usePotion(
|
||||
potionId: emergencyPotion.id,
|
||||
inventory: state.potionInventory,
|
||||
@@ -1040,25 +1056,27 @@ class ProgressService {
|
||||
usedPotionTypes = {...usedPotionTypes, PotionType.hp};
|
||||
updatedPotionInventory = result.newInventory;
|
||||
|
||||
newEvents.add(CombatEvent.playerPotion(
|
||||
timestamp: timestamp,
|
||||
potionName: emergencyPotion.name,
|
||||
healAmount: result.healedAmount,
|
||||
isHp: true,
|
||||
));
|
||||
newEvents.add(
|
||||
CombatEvent.playerPotion(
|
||||
timestamp: timestamp,
|
||||
potionName: emergencyPotion.name,
|
||||
healAmount: result.healedAmount,
|
||||
isHp: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 플레이어 공격 체크
|
||||
if (playerAccumulator >= playerStats.attackDelayMs) {
|
||||
// 스킬 자동 선택
|
||||
final availableSkillIds = updatedSkillSystem.skillStates
|
||||
.map((s) => s.skillId)
|
||||
.toList();
|
||||
// 기본 스킬이 없으면 기본 스킬 추가
|
||||
// SpellBook에서 사용 가능한 스킬 ID 목록 조회
|
||||
var availableSkillIds = skillService.getAvailableSkillIdsFromSpellBook(
|
||||
state.spellBook,
|
||||
);
|
||||
// SpellBook에 스킬이 없으면 기본 스킬 사용
|
||||
if (availableSkillIds.isEmpty) {
|
||||
availableSkillIds.addAll(SkillData.defaultSkillIds);
|
||||
availableSkillIds = SkillData.defaultSkillIds;
|
||||
}
|
||||
|
||||
final selectedSkill = skillService.selectAutoSkill(
|
||||
@@ -1070,12 +1088,18 @@ class ProgressService {
|
||||
);
|
||||
|
||||
if (selectedSkill != null && selectedSkill.isAttack) {
|
||||
// 공격 스킬 사용
|
||||
final skillResult = skillService.useAttackSkill(
|
||||
// 스펠 랭크 조회 (SpellBook 기반)
|
||||
final spellRank = skillService.getSkillRankFromSpellBook(
|
||||
state.spellBook,
|
||||
selectedSkill.id,
|
||||
);
|
||||
// 랭크 스케일링 적용된 공격 스킬 사용
|
||||
final skillResult = skillService.useAttackSkillWithRank(
|
||||
skill: selectedSkill,
|
||||
player: playerStats,
|
||||
monster: monsterStats,
|
||||
skillSystem: updatedSkillSystem,
|
||||
rank: spellRank,
|
||||
);
|
||||
playerStats = skillResult.updatedPlayer;
|
||||
monsterStats = skillResult.updatedMonster;
|
||||
@@ -1083,12 +1107,14 @@ class ProgressService {
|
||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||
|
||||
// 스킬 공격 이벤트 생성
|
||||
newEvents.add(CombatEvent.playerSkill(
|
||||
timestamp: timestamp,
|
||||
skillName: selectedSkill.name,
|
||||
damage: skillResult.result.damage,
|
||||
targetName: monsterStats.name,
|
||||
));
|
||||
newEvents.add(
|
||||
CombatEvent.playerSkill(
|
||||
timestamp: timestamp,
|
||||
skillName: selectedSkill.name,
|
||||
damage: skillResult.result.damage,
|
||||
targetName: monsterStats.name,
|
||||
),
|
||||
);
|
||||
} else if (selectedSkill != null && selectedSkill.isDot) {
|
||||
// DOT 스킬 사용
|
||||
final skillResult = skillService.useDotSkill(
|
||||
@@ -1107,12 +1133,14 @@ class ProgressService {
|
||||
}
|
||||
|
||||
// DOT 스킬 사용 이벤트 생성
|
||||
newEvents.add(CombatEvent.playerSkill(
|
||||
timestamp: timestamp,
|
||||
skillName: selectedSkill.name,
|
||||
damage: skillResult.result.damage,
|
||||
targetName: monsterStats.name,
|
||||
));
|
||||
newEvents.add(
|
||||
CombatEvent.playerSkill(
|
||||
timestamp: timestamp,
|
||||
skillName: selectedSkill.name,
|
||||
damage: skillResult.result.damage,
|
||||
targetName: monsterStats.name,
|
||||
),
|
||||
);
|
||||
} else if (selectedSkill != null && selectedSkill.isHeal) {
|
||||
// 회복 스킬 사용
|
||||
final skillResult = skillService.useHealSkill(
|
||||
@@ -1124,11 +1152,13 @@ class ProgressService {
|
||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||
|
||||
// 회복 이벤트 생성
|
||||
newEvents.add(CombatEvent.playerHeal(
|
||||
timestamp: timestamp,
|
||||
healAmount: skillResult.result.healedAmount,
|
||||
skillName: selectedSkill.name,
|
||||
));
|
||||
newEvents.add(
|
||||
CombatEvent.playerHeal(
|
||||
timestamp: timestamp,
|
||||
healAmount: skillResult.result.healedAmount,
|
||||
skillName: selectedSkill.name,
|
||||
),
|
||||
);
|
||||
} else if (selectedSkill != null && selectedSkill.isBuff) {
|
||||
// 버프 스킬 사용
|
||||
final skillResult = skillService.useBuffSkill(
|
||||
@@ -1140,10 +1170,12 @@ class ProgressService {
|
||||
updatedSkillSystem = skillResult.updatedSkillSystem;
|
||||
|
||||
// 버프 이벤트 생성
|
||||
newEvents.add(CombatEvent.playerBuff(
|
||||
timestamp: timestamp,
|
||||
skillName: selectedSkill.name,
|
||||
));
|
||||
newEvents.add(
|
||||
CombatEvent.playerBuff(
|
||||
timestamp: timestamp,
|
||||
skillName: selectedSkill.name,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 일반 공격
|
||||
final attackResult = calculator.playerAttackMonster(
|
||||
@@ -1156,17 +1188,21 @@ class ProgressService {
|
||||
// 일반 공격 이벤트 생성
|
||||
final result = attackResult.result;
|
||||
if (result.isEvaded) {
|
||||
newEvents.add(CombatEvent.monsterEvade(
|
||||
timestamp: timestamp,
|
||||
targetName: monsterStats.name,
|
||||
));
|
||||
newEvents.add(
|
||||
CombatEvent.monsterEvade(
|
||||
timestamp: timestamp,
|
||||
targetName: monsterStats.name,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
newEvents.add(CombatEvent.playerAttack(
|
||||
timestamp: timestamp,
|
||||
damage: result.damage,
|
||||
targetName: monsterStats.name,
|
||||
isCritical: result.isCritical,
|
||||
));
|
||||
newEvents.add(
|
||||
CombatEvent.playerAttack(
|
||||
timestamp: timestamp,
|
||||
damage: result.damage,
|
||||
targetName: monsterStats.name,
|
||||
isCritical: result.isCritical,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1175,7 +1211,8 @@ class ProgressService {
|
||||
}
|
||||
|
||||
// 몬스터가 살아있으면 반격
|
||||
if (monsterStats.isAlive && monsterAccumulator >= monsterStats.attackDelayMs) {
|
||||
if (monsterStats.isAlive &&
|
||||
monsterAccumulator >= monsterStats.attackDelayMs) {
|
||||
final attackResult = calculator.monsterAttackPlayer(
|
||||
attacker: monsterStats,
|
||||
defender: playerStats,
|
||||
@@ -1187,28 +1224,36 @@ class ProgressService {
|
||||
// 몬스터 공격 이벤트 생성
|
||||
final result = attackResult.result;
|
||||
if (result.isEvaded) {
|
||||
newEvents.add(CombatEvent.playerEvade(
|
||||
timestamp: timestamp,
|
||||
attackerName: monsterStats.name,
|
||||
));
|
||||
newEvents.add(
|
||||
CombatEvent.playerEvade(
|
||||
timestamp: timestamp,
|
||||
attackerName: monsterStats.name,
|
||||
),
|
||||
);
|
||||
} else if (result.isBlocked) {
|
||||
newEvents.add(CombatEvent.playerBlock(
|
||||
timestamp: timestamp,
|
||||
reducedDamage: result.damage,
|
||||
attackerName: monsterStats.name,
|
||||
));
|
||||
newEvents.add(
|
||||
CombatEvent.playerBlock(
|
||||
timestamp: timestamp,
|
||||
reducedDamage: result.damage,
|
||||
attackerName: monsterStats.name,
|
||||
),
|
||||
);
|
||||
} else if (result.isParried) {
|
||||
newEvents.add(CombatEvent.playerParry(
|
||||
timestamp: timestamp,
|
||||
reducedDamage: result.damage,
|
||||
attackerName: monsterStats.name,
|
||||
));
|
||||
newEvents.add(
|
||||
CombatEvent.playerParry(
|
||||
timestamp: timestamp,
|
||||
reducedDamage: result.damage,
|
||||
attackerName: monsterStats.name,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
newEvents.add(CombatEvent.monsterAttack(
|
||||
timestamp: timestamp,
|
||||
damage: result.damage,
|
||||
attackerName: monsterStats.name,
|
||||
));
|
||||
newEvents.add(
|
||||
CombatEvent.monsterAttack(
|
||||
timestamp: timestamp,
|
||||
damage: result.damage,
|
||||
attackerName: monsterStats.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1285,9 +1330,7 @@ class ProgressService {
|
||||
);
|
||||
|
||||
// 전투 상태 초기화
|
||||
final progress = state.progress.copyWith(
|
||||
currentCombat: null,
|
||||
);
|
||||
final progress = state.progress.copyWith(currentCombat: null);
|
||||
|
||||
return state.copyWith(
|
||||
equipment: emptyEquipment,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:askiineverdie/data/class_data.dart';
|
||||
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||
import 'package:askiineverdie/data/race_data.dart';
|
||||
import 'package:askiineverdie/src/core/engine/shop_service.dart';
|
||||
import 'package:askiineverdie/src/core/model/class_traits.dart';
|
||||
@@ -75,9 +76,7 @@ class ResurrectionService {
|
||||
);
|
||||
|
||||
// 전투 상태 초기화
|
||||
final progress = state.progress.copyWith(
|
||||
currentCombat: null,
|
||||
);
|
||||
final progress = state.progress.copyWith(currentCombat: null);
|
||||
|
||||
return state.copyWith(
|
||||
equipment: newEquipment,
|
||||
@@ -109,9 +108,7 @@ class ResurrectionService {
|
||||
// 장비 적용
|
||||
var nextState = state.copyWith(
|
||||
equipment: autoBuyResult.updatedEquipment,
|
||||
inventory: state.inventory.copyWith(
|
||||
gold: autoBuyResult.remainingGold,
|
||||
),
|
||||
inventory: state.inventory.copyWith(gold: autoBuyResult.remainingGold),
|
||||
);
|
||||
|
||||
// 2. 전체 HP/MP 계산 (장비 + 종족 + 클래스 보너스 포함)
|
||||
@@ -137,22 +134,22 @@ class ResurrectionService {
|
||||
// 4. 부활 후 태스크 시퀀스 설정 (큐에 추가)
|
||||
// 순서: 마을 귀환 → 샵 정비 → 사냥터 이동 → 전투
|
||||
final resurrectionQueue = <QueueEntry>[
|
||||
const QueueEntry(
|
||||
QueueEntry(
|
||||
kind: QueueKind.task,
|
||||
durationMillis: 3000, // 3초
|
||||
caption: 'Returning to town...',
|
||||
caption: l10n.taskReturningToTown,
|
||||
taskType: TaskType.neutral, // 걷기 애니메이션
|
||||
),
|
||||
const QueueEntry(
|
||||
QueueEntry(
|
||||
kind: QueueKind.task,
|
||||
durationMillis: 3000, // 3초
|
||||
caption: 'Restocking at shop...',
|
||||
caption: l10n.taskRestockingAtShop,
|
||||
taskType: TaskType.market, // town 애니메이션
|
||||
),
|
||||
const QueueEntry(
|
||||
QueueEntry(
|
||||
kind: QueueKind.task,
|
||||
durationMillis: 2000, // 2초
|
||||
caption: 'Heading to hunting grounds...',
|
||||
caption: l10n.taskHeadingToHuntingGrounds,
|
||||
taskType: TaskType.neutral, // 걷기 애니메이션
|
||||
),
|
||||
];
|
||||
@@ -164,10 +161,7 @@ class ResurrectionService {
|
||||
),
|
||||
// 현재 태스크를 빈 상태로 설정하여 큐에서 다음 태스크를 가져오도록 함
|
||||
progress: nextState.progress.copyWith(
|
||||
currentTask: const TaskInfo(
|
||||
caption: '',
|
||||
type: TaskType.neutral,
|
||||
),
|
||||
currentTask: const TaskInfo(caption: '', type: TaskType.neutral),
|
||||
task: const ProgressBarState(
|
||||
position: 0,
|
||||
max: 1, // 즉시 완료되어 큐에서 다음 태스크 가져옴
|
||||
|
||||
@@ -126,7 +126,11 @@ class ShopService {
|
||||
}
|
||||
|
||||
/// 슬롯과 레벨에 따른 스탯 생성
|
||||
ItemStats _generateItemStats(EquipmentSlot slot, int level, ItemRarity rarity) {
|
||||
ItemStats _generateItemStats(
|
||||
EquipmentSlot slot,
|
||||
int level,
|
||||
ItemRarity rarity,
|
||||
) {
|
||||
final multiplier = rarity.multiplier;
|
||||
final baseValue = (level * multiplier).round();
|
||||
|
||||
@@ -145,10 +149,7 @@ class ShopService {
|
||||
magDef: baseValue ~/ 2,
|
||||
intBonus: level ~/ 10,
|
||||
),
|
||||
EquipmentSlot.hauberk => ItemStats(
|
||||
def: baseValue,
|
||||
hpBonus: level * 2,
|
||||
),
|
||||
EquipmentSlot.hauberk => ItemStats(def: baseValue, hpBonus: level * 2),
|
||||
EquipmentSlot.brassairts => ItemStats(
|
||||
def: baseValue ~/ 2,
|
||||
strBonus: level ~/ 15,
|
||||
@@ -273,11 +274,7 @@ class ShopService {
|
||||
/// 장비 판매
|
||||
SellResult sellItem(EquipmentItem item, int currentGold) {
|
||||
final price = calculateSellPrice(item);
|
||||
return SellResult(
|
||||
item: item,
|
||||
price: price,
|
||||
newGold: currentGold + price,
|
||||
);
|
||||
return SellResult(item: item, price: price, newGold: currentGold + price);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
import 'package:askiineverdie/src/core/model/monster_combat_stats.dart';
|
||||
import 'package:askiineverdie/src/core/model/skill.dart';
|
||||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||
import 'package:askiineverdie/src/core/util/roman.dart';
|
||||
|
||||
/// 스킬 시스템 서비스
|
||||
///
|
||||
@@ -30,7 +31,8 @@ class SkillService {
|
||||
|
||||
// 쿨타임 체크
|
||||
final skillState = skillSystem.getSkillState(skill.id);
|
||||
if (skillState != null && !skillState.isReady(skillSystem.elapsedMs, skill.cooldownMs)) {
|
||||
if (skillState != null &&
|
||||
!skillState.isReady(skillSystem.elapsedMs, skill.cooldownMs)) {
|
||||
return SkillFailReason.onCooldown;
|
||||
}
|
||||
|
||||
@@ -49,7 +51,8 @@ class SkillService {
|
||||
CombatStats updatedPlayer,
|
||||
MonsterCombatStats updatedMonster,
|
||||
SkillSystemState updatedSkillSystem,
|
||||
}) useAttackSkill({
|
||||
})
|
||||
useAttackSkill({
|
||||
required Skill skill,
|
||||
required CombatStats player,
|
||||
required MonsterCombatStats monster,
|
||||
@@ -66,7 +69,9 @@ class SkillService {
|
||||
final effectiveMonsterDef = monster.def * (1 - skill.targetDefReduction);
|
||||
|
||||
// 최종 데미지 계산 (방어력 감산)
|
||||
final finalDamage = (buffedDamage - effectiveMonsterDef * 0.5).round().clamp(1, 9999);
|
||||
final finalDamage = (buffedDamage - effectiveMonsterDef * 0.5)
|
||||
.round()
|
||||
.clamp(1, 9999);
|
||||
|
||||
// 몬스터에 데미지 적용
|
||||
var updatedMonster = monster.applyDamage(finalDamage);
|
||||
@@ -79,17 +84,15 @@ class SkillService {
|
||||
}
|
||||
|
||||
// MP 소모
|
||||
updatedPlayer = updatedPlayer.withMp(updatedPlayer.mpCurrent - skill.mpCost);
|
||||
updatedPlayer = updatedPlayer.withMp(
|
||||
updatedPlayer.mpCurrent - skill.mpCost,
|
||||
);
|
||||
|
||||
// 스킬 상태 업데이트 (쿨타임 시작)
|
||||
final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id);
|
||||
|
||||
return (
|
||||
result: SkillUseResult(
|
||||
skill: skill,
|
||||
success: true,
|
||||
damage: finalDamage,
|
||||
),
|
||||
result: SkillUseResult(skill: skill, success: true, damage: finalDamage),
|
||||
updatedPlayer: updatedPlayer,
|
||||
updatedMonster: updatedMonster,
|
||||
updatedSkillSystem: updatedSkillSystem,
|
||||
@@ -101,7 +104,8 @@ class SkillService {
|
||||
SkillUseResult result,
|
||||
CombatStats updatedPlayer,
|
||||
SkillSystemState updatedSkillSystem,
|
||||
}) useHealSkill({
|
||||
})
|
||||
useHealSkill({
|
||||
required Skill skill,
|
||||
required CombatStats player,
|
||||
required SkillSystemState skillSystem,
|
||||
@@ -116,7 +120,9 @@ class SkillService {
|
||||
var updatedPlayer = player.applyHeal(healAmount);
|
||||
|
||||
// MP 소모
|
||||
updatedPlayer = updatedPlayer.withMp(updatedPlayer.mpCurrent - skill.mpCost);
|
||||
updatedPlayer = updatedPlayer.withMp(
|
||||
updatedPlayer.mpCurrent - skill.mpCost,
|
||||
);
|
||||
|
||||
// 스킬 상태 업데이트
|
||||
final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id);
|
||||
@@ -137,7 +143,8 @@ class SkillService {
|
||||
SkillUseResult result,
|
||||
CombatStats updatedPlayer,
|
||||
SkillSystemState updatedSkillSystem,
|
||||
}) useBuffSkill({
|
||||
})
|
||||
useBuffSkill({
|
||||
required Skill skill,
|
||||
required CombatStats player,
|
||||
required SkillSystemState skillSystem,
|
||||
@@ -158,10 +165,11 @@ class SkillService {
|
||||
);
|
||||
|
||||
// 기존 같은 버프 제거 후 새 버프 추가
|
||||
final updatedBuffs = skillSystem.activeBuffs
|
||||
.where((b) => b.effect.id != skill.buff!.id)
|
||||
.toList()
|
||||
..add(newBuff);
|
||||
final updatedBuffs =
|
||||
skillSystem.activeBuffs
|
||||
.where((b) => b.effect.id != skill.buff!.id)
|
||||
.toList()
|
||||
..add(newBuff);
|
||||
|
||||
// MP 소모
|
||||
var updatedPlayer = player.withMp(player.mpCurrent - skill.mpCost);
|
||||
@@ -171,11 +179,7 @@ class SkillService {
|
||||
updatedSkillSystem = updatedSkillSystem.copyWith(activeBuffs: updatedBuffs);
|
||||
|
||||
return (
|
||||
result: SkillUseResult(
|
||||
skill: skill,
|
||||
success: true,
|
||||
appliedBuff: newBuff,
|
||||
),
|
||||
result: SkillUseResult(skill: skill, success: true, appliedBuff: newBuff),
|
||||
updatedPlayer: updatedPlayer,
|
||||
updatedSkillSystem: updatedSkillSystem,
|
||||
);
|
||||
@@ -190,7 +194,8 @@ class SkillService {
|
||||
CombatStats updatedPlayer,
|
||||
SkillSystemState updatedSkillSystem,
|
||||
DotEffect? dotEffect,
|
||||
}) useDotSkill({
|
||||
})
|
||||
useDotSkill({
|
||||
required Skill skill,
|
||||
required CombatStats player,
|
||||
required SkillSystemState skillSystem,
|
||||
@@ -265,12 +270,15 @@ class SkillService {
|
||||
final availableSkills = availableSkillIds
|
||||
.map((id) => SkillData.getSkillById(id))
|
||||
.whereType<Skill>()
|
||||
.where((skill) => canUseSkill(
|
||||
skill: skill,
|
||||
currentMp: currentMp,
|
||||
skillSystem: skillSystem,
|
||||
) ==
|
||||
null)
|
||||
.where(
|
||||
(skill) =>
|
||||
canUseSkill(
|
||||
skill: skill,
|
||||
currentMp: currentMp,
|
||||
skillSystem: skillSystem,
|
||||
) ==
|
||||
null,
|
||||
)
|
||||
.toList();
|
||||
|
||||
if (availableSkills.isEmpty) return null;
|
||||
@@ -311,9 +319,11 @@ class SkillService {
|
||||
|
||||
// 예상 총 데미지 기준 정렬
|
||||
dotSkills.sort((a, b) {
|
||||
final aTotal = (a.baseDotDamage ?? 0) *
|
||||
final aTotal =
|
||||
(a.baseDotDamage ?? 0) *
|
||||
((a.baseDotDurationMs ?? 0) ~/ (a.baseDotTickMs ?? 1000));
|
||||
final bTotal = (b.baseDotDamage ?? 0) *
|
||||
final bTotal =
|
||||
(b.baseDotDamage ?? 0) *
|
||||
((b.baseDotDurationMs ?? 0) ~/ (b.baseDotTickMs ?? 1000));
|
||||
return bTotal.compareTo(aTotal);
|
||||
});
|
||||
@@ -344,7 +354,9 @@ class SkillService {
|
||||
final attackSkills = skills.where((s) => s.isAttack).toList();
|
||||
if (attackSkills.isEmpty) return null;
|
||||
|
||||
attackSkills.sort((a, b) => b.damageMultiplier.compareTo(a.damageMultiplier));
|
||||
attackSkills.sort(
|
||||
(a, b) => b.damageMultiplier.compareTo(a.damageMultiplier),
|
||||
);
|
||||
return attackSkills.first;
|
||||
}
|
||||
|
||||
@@ -399,7 +411,10 @@ class SkillService {
|
||||
// ============================================================================
|
||||
|
||||
/// 스킬 쿨타임 업데이트
|
||||
SkillSystemState _updateSkillCooldown(SkillSystemState state, String skillId) {
|
||||
SkillSystemState _updateSkillCooldown(
|
||||
SkillSystemState state,
|
||||
String skillId,
|
||||
) {
|
||||
final skillStates = List<SkillState>.from(state.skillStates);
|
||||
|
||||
// 기존 상태 찾기
|
||||
@@ -412,11 +427,9 @@ class SkillService {
|
||||
);
|
||||
} else {
|
||||
// 새 상태 추가
|
||||
skillStates.add(SkillState(
|
||||
skillId: skillId,
|
||||
lastUsedMs: state.elapsedMs,
|
||||
rank: 1,
|
||||
));
|
||||
skillStates.add(
|
||||
SkillState(skillId: skillId, lastUsedMs: state.elapsedMs, rank: 1),
|
||||
);
|
||||
}
|
||||
|
||||
return state.copyWith(skillStates: skillStates);
|
||||
@@ -426,4 +439,142 @@ class SkillService {
|
||||
SkillSystemState updateElapsedTime(SkillSystemState state, int deltaMs) {
|
||||
return state.copyWith(elapsedMs: state.elapsedMs + deltaMs);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SpellBook 연동
|
||||
// ============================================================================
|
||||
|
||||
/// SpellBook에서 사용 가능한 스킬 목록 조회
|
||||
///
|
||||
/// SpellEntry 이름을 Skill로 매핑하여 반환
|
||||
List<Skill> getAvailableSkillsFromSpellBook(SpellBook spellBook) {
|
||||
return spellBook.spells
|
||||
.map((spell) => SkillData.getSkillBySpellName(spell.name))
|
||||
.whereType<Skill>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// SpellBook에서 스킬의 랭크(레벨) 조회
|
||||
///
|
||||
/// 로마숫자 랭크(I, II, III)를 정수로 변환하여 반환
|
||||
/// 스펠이 없으면 1 반환
|
||||
int getSkillRankFromSpellBook(SpellBook spellBook, String skillId) {
|
||||
// skillId로 스킬 찾기
|
||||
final skill = SkillData.getSkillById(skillId);
|
||||
if (skill == null) return 1;
|
||||
|
||||
// 스킬 이름으로 SpellEntry 찾기
|
||||
for (final spell in spellBook.spells) {
|
||||
if (spell.name == skill.name) {
|
||||
return romanToInt(spell.rank);
|
||||
}
|
||||
}
|
||||
|
||||
return 1; // 기본 랭크
|
||||
}
|
||||
|
||||
/// SpellBook에서 스킬 ID 목록 조회
|
||||
///
|
||||
/// 전투 시스템에서 사용 가능한 스킬 ID 목록 반환
|
||||
List<String> getAvailableSkillIdsFromSpellBook(SpellBook spellBook) {
|
||||
return getAvailableSkillsFromSpellBook(
|
||||
spellBook,
|
||||
).map((skill) => skill.id).toList();
|
||||
}
|
||||
|
||||
/// 랭크 스케일링이 적용된 공격 스킬 사용
|
||||
///
|
||||
/// [rank] 스펠 랭크 (SpellBook에서 조회)
|
||||
({
|
||||
SkillUseResult result,
|
||||
CombatStats updatedPlayer,
|
||||
MonsterCombatStats updatedMonster,
|
||||
SkillSystemState updatedSkillSystem,
|
||||
})
|
||||
useAttackSkillWithRank({
|
||||
required Skill skill,
|
||||
required CombatStats player,
|
||||
required MonsterCombatStats monster,
|
||||
required SkillSystemState skillSystem,
|
||||
required int rank,
|
||||
}) {
|
||||
// 랭크 스케일링 적용
|
||||
final rankMult = getRankMultiplier(rank);
|
||||
final mpMult = getRankMpMultiplier(rank);
|
||||
|
||||
// 실제 MP 비용 계산
|
||||
final actualMpCost = (skill.mpCost * mpMult).round();
|
||||
|
||||
// 기본 데미지 계산 (랭크 배율 적용)
|
||||
final baseDamage = player.atk * skill.damageMultiplier * rankMult;
|
||||
|
||||
// 버프 효과 적용
|
||||
final buffMods = skillSystem.totalBuffModifiers;
|
||||
final buffedDamage = baseDamage * (1 + buffMods.atkMod);
|
||||
|
||||
// 적 방어력 감소 적용
|
||||
final effectiveMonsterDef = monster.def * (1 - skill.targetDefReduction);
|
||||
|
||||
// 최종 데미지 계산 (방어력 감산)
|
||||
final finalDamage = (buffedDamage - effectiveMonsterDef * 0.5)
|
||||
.round()
|
||||
.clamp(1, 9999);
|
||||
|
||||
// 몬스터에 데미지 적용
|
||||
var updatedMonster = monster.applyDamage(finalDamage);
|
||||
|
||||
// 자해 데미지 적용
|
||||
var updatedPlayer = player;
|
||||
if (skill.selfDamagePercent > 0) {
|
||||
final selfDamage = (player.hpMax * skill.selfDamagePercent).round();
|
||||
updatedPlayer = player.applyDamage(selfDamage);
|
||||
}
|
||||
|
||||
// MP 소모 (랭크 스케일링 적용)
|
||||
updatedPlayer = updatedPlayer.withMp(
|
||||
updatedPlayer.mpCurrent - actualMpCost,
|
||||
);
|
||||
|
||||
// 스킬 상태 업데이트 (쿨타임 시작, 랭크 저장)
|
||||
// 쿨타임 스케일링은 isReady 체크 시 적용됨
|
||||
final updatedSkillSystem = _updateSkillCooldownWithRank(
|
||||
skillSystem,
|
||||
skill.id,
|
||||
rank,
|
||||
);
|
||||
|
||||
return (
|
||||
result: SkillUseResult(skill: skill, success: true, damage: finalDamage),
|
||||
updatedPlayer: updatedPlayer,
|
||||
updatedMonster: updatedMonster,
|
||||
updatedSkillSystem: updatedSkillSystem,
|
||||
);
|
||||
}
|
||||
|
||||
/// 랭크 정보를 포함한 스킬 쿨타임 업데이트
|
||||
SkillSystemState _updateSkillCooldownWithRank(
|
||||
SkillSystemState state,
|
||||
String skillId,
|
||||
int rank,
|
||||
) {
|
||||
final skillStates = List<SkillState>.from(state.skillStates);
|
||||
|
||||
// 기존 상태 찾기
|
||||
final existingIndex = skillStates.indexWhere((s) => s.skillId == skillId);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
// 기존 상태 업데이트
|
||||
skillStates[existingIndex] = skillStates[existingIndex].copyWith(
|
||||
lastUsedMs: state.elapsedMs,
|
||||
rank: rank,
|
||||
);
|
||||
} else {
|
||||
// 새 상태 추가
|
||||
skillStates.add(
|
||||
SkillState(skillId: skillId, lastUsedMs: state.elapsedMs, rank: rank),
|
||||
);
|
||||
}
|
||||
|
||||
return state.copyWith(skillStates: skillStates);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ class StatCalculator {
|
||||
var str = baseStats.str + race.getModifier(StatType.str);
|
||||
var con = baseStats.con + race.getModifier(StatType.con);
|
||||
var dex = baseStats.dex + race.getModifier(StatType.dex);
|
||||
var intel = baseStats.intelligence + race.getModifier(StatType.intelligence);
|
||||
var intel =
|
||||
baseStats.intelligence + race.getModifier(StatType.intelligence);
|
||||
var wis = baseStats.wis + race.getModifier(StatType.wis);
|
||||
var cha = baseStats.cha + race.getModifier(StatType.cha);
|
||||
|
||||
@@ -108,31 +109,41 @@ class StatCalculator {
|
||||
// 클래스 패시브 적용
|
||||
|
||||
// 물리 공격력 보너스 (Bug Hunter: +20%)
|
||||
final classPhysicalBonus = klass.getPassiveValue(ClassPassiveType.physicalDamageBonus);
|
||||
final classPhysicalBonus = klass.getPassiveValue(
|
||||
ClassPassiveType.physicalDamageBonus,
|
||||
);
|
||||
if (classPhysicalBonus > 0) {
|
||||
atk = (atk * (1 + classPhysicalBonus)).round();
|
||||
}
|
||||
|
||||
// 방어력 보너스 (Debugger Paladin: +15%)
|
||||
final classDefenseBonus = klass.getPassiveValue(ClassPassiveType.defenseBonus);
|
||||
final classDefenseBonus = klass.getPassiveValue(
|
||||
ClassPassiveType.defenseBonus,
|
||||
);
|
||||
if (classDefenseBonus > 0) {
|
||||
def = (def * (1 + classDefenseBonus)).round();
|
||||
}
|
||||
|
||||
// 마법 데미지 보너스 (Compiler Mage: +25%)
|
||||
final classMagicBonus = klass.getPassiveValue(ClassPassiveType.magicDamageBonus);
|
||||
final classMagicBonus = klass.getPassiveValue(
|
||||
ClassPassiveType.magicDamageBonus,
|
||||
);
|
||||
if (classMagicBonus > 0) {
|
||||
magAtk = (magAtk * (1 + classMagicBonus)).round();
|
||||
}
|
||||
|
||||
// 회피율 보너스 (Refactor Monk: +15%)
|
||||
final classEvasionBonus = klass.getPassiveValue(ClassPassiveType.evasionBonus);
|
||||
final classEvasionBonus = klass.getPassiveValue(
|
||||
ClassPassiveType.evasionBonus,
|
||||
);
|
||||
if (classEvasionBonus > 0) {
|
||||
evasion = (evasion + classEvasionBonus).clamp(0.0, 0.6);
|
||||
}
|
||||
|
||||
// 크리티컬 보너스 (Pointer Assassin: +20%)
|
||||
final classCritBonus = klass.getPassiveValue(ClassPassiveType.criticalBonus);
|
||||
final classCritBonus = klass.getPassiveValue(
|
||||
ClassPassiveType.criticalBonus,
|
||||
);
|
||||
if (classCritBonus > 0) {
|
||||
criRate = (criRate + classCritBonus).clamp(0.0, 0.8);
|
||||
}
|
||||
|
||||
@@ -13,11 +13,7 @@ enum StoryEventType {
|
||||
|
||||
/// 스토리 이벤트 (Story Event)
|
||||
class StoryEvent {
|
||||
const StoryEvent({
|
||||
required this.type,
|
||||
required this.act,
|
||||
this.data,
|
||||
});
|
||||
const StoryEvent({required this.type, required this.act, this.data});
|
||||
|
||||
final StoryEventType type;
|
||||
final StoryAct act;
|
||||
@@ -73,18 +69,14 @@ class StoryService {
|
||||
// 이전 Act 완료 처리
|
||||
if (_currentAct != StoryAct.prologue) {
|
||||
_completedActs.add(_currentAct);
|
||||
_eventController.add(StoryEvent(
|
||||
type: StoryEventType.actComplete,
|
||||
act: _currentAct,
|
||||
));
|
||||
_eventController.add(
|
||||
StoryEvent(type: StoryEventType.actComplete, act: _currentAct),
|
||||
);
|
||||
}
|
||||
|
||||
// 새 Act 시작
|
||||
_currentAct = newAct;
|
||||
final event = StoryEvent(
|
||||
type: StoryEventType.actStart,
|
||||
act: newAct,
|
||||
);
|
||||
final event = StoryEvent(type: StoryEventType.actStart, act: newAct);
|
||||
_eventController.add(event);
|
||||
return event;
|
||||
}
|
||||
@@ -126,10 +118,9 @@ class StoryService {
|
||||
void _triggerEnding() {
|
||||
_completedActs.add(StoryAct.act5);
|
||||
_currentAct = StoryAct.ending;
|
||||
_eventController.add(StoryEvent(
|
||||
type: StoryEventType.ending,
|
||||
act: StoryAct.ending,
|
||||
));
|
||||
_eventController.add(
|
||||
StoryEvent(type: StoryEventType.ending, act: StoryAct.ending),
|
||||
);
|
||||
}
|
||||
|
||||
/// 시네마틱 데이터 가져오기 (Get Cinematic Data)
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||
import 'package:askiineverdie/data/game_translations_ko.dart';
|
||||
import 'package:askiineverdie/src/core/util/pq_logic.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// 게임 데이터 번역을 위한 헬퍼 클래스
|
||||
/// 현재 로케일에 따라 게임 데이터의 번역을 제공합니다.
|
||||
///
|
||||
/// 글로벌 로케일 시스템(game_text_l10n.dart)을 사용하여 일관성 보장
|
||||
class GameDataL10n {
|
||||
GameDataL10n._();
|
||||
|
||||
/// 현재 로케일이 한국어인지 확인
|
||||
/// 현재 로케일이 한국어인지 확인 (글로벌 로케일 사용)
|
||||
static bool _isKorean(BuildContext context) {
|
||||
// 글로벌 로케일 우선, 폴백으로 context 로케일 사용
|
||||
if (l10n.isKoreanLocale) return true;
|
||||
final locale = Localizations.localeOf(context);
|
||||
return locale.languageCode == 'ko';
|
||||
}
|
||||
@@ -307,17 +312,19 @@ class GameDataL10n {
|
||||
for (final entry in baseMap.entries) {
|
||||
if (remaining.endsWith(entry.key)) {
|
||||
baseTranslated = entry.value;
|
||||
modifierPart = remaining.substring(
|
||||
0,
|
||||
remaining.length - entry.key.length,
|
||||
).trim();
|
||||
modifierPart = remaining
|
||||
.substring(0, remaining.length - entry.key.length)
|
||||
.trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 수식어 번역
|
||||
final isWeapon = slotIndex == 0;
|
||||
final modWords = modifierPart.split(' ').where((s) => s.isNotEmpty).toList();
|
||||
final modWords = modifierPart
|
||||
.split(' ')
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
final translatedMods = modWords.map((mod) {
|
||||
if (isWeapon) {
|
||||
return offenseAttribTranslationsKo[mod] ??
|
||||
@@ -423,19 +430,20 @@ class GameDataL10n {
|
||||
// 드롭 아이템 앞 부분이 몬스터 이름
|
||||
String monsterPart;
|
||||
if (itemString.endsWith(dropItemProperCase)) {
|
||||
monsterPart =
|
||||
itemString.substring(0, itemString.length - dropItemProperCase.length).trim();
|
||||
monsterPart = itemString
|
||||
.substring(0, itemString.length - dropItemProperCase.length)
|
||||
.trim();
|
||||
} else {
|
||||
monsterPart =
|
||||
itemString.substring(0, itemString.length - dropItem.length).trim();
|
||||
monsterPart = itemString
|
||||
.substring(0, itemString.length - dropItem.length)
|
||||
.trim();
|
||||
}
|
||||
|
||||
if (monsterPart.isEmpty) continue;
|
||||
|
||||
// 몬스터 이름 번역 (소문자를 원래 형태로 변환하여 찾기)
|
||||
final monsterNameKey = _toTitleCase(monsterPart);
|
||||
final monsterKo =
|
||||
monsterTranslationsKo[monsterNameKey] ?? monsterPart;
|
||||
final monsterKo = monsterTranslationsKo[monsterNameKey] ?? monsterPart;
|
||||
|
||||
final dropKo = entry.value;
|
||||
return '$monsterKo의 $dropKo';
|
||||
@@ -452,9 +460,12 @@ class GameDataL10n {
|
||||
|
||||
/// 각 단어의 첫 글자를 대문자로 (Title Case)
|
||||
static String _toTitleCase(String s) {
|
||||
return s.split(' ').map((word) {
|
||||
if (word.isEmpty) return word;
|
||||
return word[0].toUpperCase() + word.substring(1);
|
||||
}).join(' ');
|
||||
return s
|
||||
.split(' ')
|
||||
.map((word) {
|
||||
if (word.isEmpty) return word;
|
||||
return word[0].toUpperCase() + word.substring(1);
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +233,8 @@ class CombatStats {
|
||||
final effectiveStr = stats.str + equipStats.strBonus + raceStr + classStr;
|
||||
final effectiveCon = stats.con + equipStats.conBonus + raceCon + classCon;
|
||||
final effectiveDex = stats.dex + equipStats.dexBonus + raceDex + classDex;
|
||||
final effectiveInt = stats.intelligence + equipStats.intBonus + raceInt + classInt;
|
||||
final effectiveInt =
|
||||
stats.intelligence + equipStats.intBonus + raceInt + classInt;
|
||||
final effectiveWis = stats.wis + equipStats.wisBonus + raceWis + classWis;
|
||||
final effectiveCha = stats.cha + equipStats.chaBonus + raceCha + classCha;
|
||||
|
||||
@@ -276,7 +277,10 @@ class CombatStats {
|
||||
final weaponSpeed = weaponItem.stats.attackSpeed;
|
||||
final baseAttackSpeed = weaponSpeed > 0 ? weaponSpeed : 1000;
|
||||
final speedModifier = 1.0 + (effectiveDex - 10) * 0.02;
|
||||
final attackDelayMs = (baseAttackSpeed / speedModifier).round().clamp(300, 2000);
|
||||
final attackDelayMs = (baseAttackSpeed / speedModifier).round().clamp(
|
||||
300,
|
||||
2000,
|
||||
);
|
||||
|
||||
// HP/MP: 기본 + 장비 보너스
|
||||
var totalHpMax = stats.hpMax + equipStats.hpBonus;
|
||||
@@ -299,7 +303,8 @@ class CombatStats {
|
||||
}
|
||||
|
||||
// 마법 데미지 보너스 (Null Elf: +15%)
|
||||
final raceMagicBonus = race?.getPassiveValue(PassiveType.magicDamageBonus) ?? 0.0;
|
||||
final raceMagicBonus =
|
||||
race?.getPassiveValue(PassiveType.magicDamageBonus) ?? 0.0;
|
||||
if (raceMagicBonus > 0) {
|
||||
baseMagAtk = (baseMagAtk * (1 + raceMagicBonus)).round();
|
||||
}
|
||||
@@ -311,7 +316,8 @@ class CombatStats {
|
||||
}
|
||||
|
||||
// 크리티컬 보너스 (Stack Goblin: +5%)
|
||||
final raceCritBonus = race?.getPassiveValue(PassiveType.criticalBonus) ?? 0.0;
|
||||
final raceCritBonus =
|
||||
race?.getPassiveValue(PassiveType.criticalBonus) ?? 0.0;
|
||||
criRate += raceCritBonus;
|
||||
|
||||
// ========================================================================
|
||||
@@ -319,35 +325,41 @@ class CombatStats {
|
||||
// ========================================================================
|
||||
|
||||
// HP 보너스 (Garbage Collector: +30%)
|
||||
final classHpBonus = klass?.getPassiveValue(ClassPassiveType.hpBonus) ?? 0.0;
|
||||
final classHpBonus =
|
||||
klass?.getPassiveValue(ClassPassiveType.hpBonus) ?? 0.0;
|
||||
if (classHpBonus > 0) {
|
||||
totalHpMax = (totalHpMax * (1 + classHpBonus)).round();
|
||||
}
|
||||
|
||||
// 물리 공격력 보너스 (Bug Hunter: +20%)
|
||||
final classPhysBonus = klass?.getPassiveValue(ClassPassiveType.physicalDamageBonus) ?? 0.0;
|
||||
final classPhysBonus =
|
||||
klass?.getPassiveValue(ClassPassiveType.physicalDamageBonus) ?? 0.0;
|
||||
if (classPhysBonus > 0) {
|
||||
baseAtk = (baseAtk * (1 + classPhysBonus)).round();
|
||||
}
|
||||
|
||||
// 방어력 보너스 (Debugger Paladin: +15%)
|
||||
final classDefBonus = klass?.getPassiveValue(ClassPassiveType.defenseBonus) ?? 0.0;
|
||||
final classDefBonus =
|
||||
klass?.getPassiveValue(ClassPassiveType.defenseBonus) ?? 0.0;
|
||||
if (classDefBonus > 0) {
|
||||
baseDef = (baseDef * (1 + classDefBonus)).round();
|
||||
}
|
||||
|
||||
// 마법 데미지 보너스 (Compiler Mage: +25%)
|
||||
final classMagBonus = klass?.getPassiveValue(ClassPassiveType.magicDamageBonus) ?? 0.0;
|
||||
final classMagBonus =
|
||||
klass?.getPassiveValue(ClassPassiveType.magicDamageBonus) ?? 0.0;
|
||||
if (classMagBonus > 0) {
|
||||
baseMagAtk = (baseMagAtk * (1 + classMagBonus)).round();
|
||||
}
|
||||
|
||||
// 회피율 보너스 (Refactor Monk: +15%)
|
||||
final classEvasionBonus = klass?.getPassiveValue(ClassPassiveType.evasionBonus) ?? 0.0;
|
||||
final classEvasionBonus =
|
||||
klass?.getPassiveValue(ClassPassiveType.evasionBonus) ?? 0.0;
|
||||
evasion += classEvasionBonus;
|
||||
|
||||
// 크리티컬 보너스 (Pointer Assassin: +20%)
|
||||
final classCritBonus = klass?.getPassiveValue(ClassPassiveType.criticalBonus) ?? 0.0;
|
||||
final classCritBonus =
|
||||
klass?.getPassiveValue(ClassPassiveType.criticalBonus) ?? 0.0;
|
||||
criRate += classCritBonus;
|
||||
|
||||
// 최종 클램핑
|
||||
|
||||
@@ -209,11 +209,8 @@ class SkillSystemState {
|
||||
/// 게임 진행 경과 시간 (밀리초, 스킬 쿨타임 계산용)
|
||||
final int elapsedMs;
|
||||
|
||||
factory SkillSystemState.empty() => const SkillSystemState(
|
||||
skillStates: [],
|
||||
activeBuffs: [],
|
||||
elapsedMs: 0,
|
||||
);
|
||||
factory SkillSystemState.empty() =>
|
||||
const SkillSystemState(skillStates: [], activeBuffs: [], elapsedMs: 0);
|
||||
|
||||
/// 특정 스킬 상태 가져오기
|
||||
SkillState? getSkillState(String skillId) {
|
||||
@@ -224,7 +221,8 @@ class SkillSystemState {
|
||||
}
|
||||
|
||||
/// 버프 효과 합산 (동일 버프는 중복 적용 안 됨)
|
||||
({double atkMod, double defMod, double criMod, double evasionMod}) get totalBuffModifiers {
|
||||
({double atkMod, double defMod, double criMod, double evasionMod})
|
||||
get totalBuffModifiers {
|
||||
double atkMod = 0;
|
||||
double defMod = 0;
|
||||
double criMod = 0;
|
||||
@@ -243,7 +241,12 @@ class SkillSystemState {
|
||||
}
|
||||
}
|
||||
|
||||
return (atkMod: atkMod, defMod: defMod, criMod: criMod, evasionMod: evasionMod);
|
||||
return (
|
||||
atkMod: atkMod,
|
||||
defMod: defMod,
|
||||
criMod: criMod,
|
||||
evasionMod: evasionMod,
|
||||
);
|
||||
}
|
||||
|
||||
SkillSystemState copyWith({
|
||||
@@ -477,10 +480,8 @@ class Inventory {
|
||||
/// Phase 2에서 EquipmentItem 기반으로 확장됨.
|
||||
/// 기존 문자열 API(weapon, shield 등)는 호환성을 위해 유지.
|
||||
class Equipment {
|
||||
Equipment({
|
||||
required this.items,
|
||||
required this.bestIndex,
|
||||
}) : assert(items.length == slotCount, 'Equipment must have $slotCount items');
|
||||
Equipment({required this.items, required this.bestIndex})
|
||||
: assert(items.length == slotCount, 'Equipment must have $slotCount items');
|
||||
|
||||
/// 장비 아이템 목록 (11개 슬롯)
|
||||
final List<EquipmentItem> items;
|
||||
@@ -525,10 +526,7 @@ class Equipment {
|
||||
|
||||
/// 모든 장비 스탯 합산
|
||||
ItemStats get totalStats {
|
||||
return items.fold(
|
||||
ItemStats.empty,
|
||||
(sum, item) => sum + item.stats,
|
||||
);
|
||||
return items.fold(ItemStats.empty, (sum, item) => sum + item.stats);
|
||||
}
|
||||
|
||||
/// 모든 장비 무게 합산
|
||||
@@ -647,10 +645,7 @@ class Equipment {
|
||||
return Equipment(items: newItems, bestIndex: bestIndex);
|
||||
}
|
||||
|
||||
Equipment copyWith({
|
||||
List<EquipmentItem>? items,
|
||||
int? bestIndex,
|
||||
}) {
|
||||
Equipment copyWith({List<EquipmentItem>? items, int? bestIndex}) {
|
||||
return Equipment(
|
||||
items: items ?? List<EquipmentItem>.from(this.items),
|
||||
bestIndex: bestIndex ?? this.bestIndex,
|
||||
|
||||
@@ -175,9 +175,7 @@ class HallOfFame {
|
||||
|
||||
/// JSON으로 직렬화
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'entries': entries.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
return {'entries': entries.map((e) => e.toJson()).toList()};
|
||||
}
|
||||
|
||||
/// JSON에서 역직렬화
|
||||
|
||||
@@ -88,10 +88,7 @@ class PotionInventory {
|
||||
PotionInventory addPotion(String potionId, [int count = 1]) {
|
||||
final newPotions = Map<String, int>.from(potions);
|
||||
newPotions[potionId] = (newPotions[potionId] ?? 0) + count;
|
||||
return PotionInventory(
|
||||
potions: newPotions,
|
||||
usedInBattle: usedInBattle,
|
||||
);
|
||||
return PotionInventory(potions: newPotions, usedInBattle: usedInBattle);
|
||||
}
|
||||
|
||||
/// 물약 사용 (수량 감소)
|
||||
@@ -107,18 +104,12 @@ class PotionInventory {
|
||||
|
||||
final newUsed = Set<PotionType>.from(usedInBattle)..add(type);
|
||||
|
||||
return PotionInventory(
|
||||
potions: newPotions,
|
||||
usedInBattle: newUsed,
|
||||
);
|
||||
return PotionInventory(potions: newPotions, usedInBattle: newUsed);
|
||||
}
|
||||
|
||||
/// 전투 종료 시 사용 기록 초기화
|
||||
PotionInventory resetBattleUsage() {
|
||||
return PotionInventory(
|
||||
potions: potions,
|
||||
usedInBattle: const {},
|
||||
);
|
||||
return PotionInventory(potions: potions, usedInBattle: const {});
|
||||
}
|
||||
|
||||
/// 빈 인벤토리
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
/// 스탯 타입 열거형 (stat type)
|
||||
enum StatType {
|
||||
str,
|
||||
con,
|
||||
dex,
|
||||
intelligence,
|
||||
wis,
|
||||
cha,
|
||||
}
|
||||
enum StatType { str, con, dex, intelligence, wis, cha }
|
||||
|
||||
/// 패시브 능력 타입 (passive ability type)
|
||||
enum PassiveType {
|
||||
|
||||
@@ -1,3 +1,28 @@
|
||||
// ============================================================================
|
||||
// 랭크 스케일링 (Rank Scaling)
|
||||
// ============================================================================
|
||||
|
||||
/// 스펠 랭크에 따른 스킬 배율 계산
|
||||
///
|
||||
/// 랭크 1: 1.0x, 랭크 2: 1.15x, 랭크 3: 1.30x, ...
|
||||
double getRankMultiplier(int rank) => 1.0 + (rank - 1) * 0.15;
|
||||
|
||||
/// 랭크에 따른 쿨타임 감소율 계산
|
||||
///
|
||||
/// 랭크당 5% 감소 (최대 50% 감소)
|
||||
double getRankCooldownMultiplier(int rank) =>
|
||||
(1.0 - (rank - 1) * 0.05).clamp(0.5, 1.0);
|
||||
|
||||
/// 랭크에 따른 MP 비용 감소율 계산
|
||||
///
|
||||
/// 랭크당 3% 감소 (최대 30% 감소)
|
||||
double getRankMpMultiplier(int rank) =>
|
||||
(1.0 - (rank - 1) * 0.03).clamp(0.7, 1.0);
|
||||
|
||||
// ============================================================================
|
||||
// 스킬 타입 (Skill Types)
|
||||
// ============================================================================
|
||||
|
||||
/// 스킬 타입
|
||||
enum SkillType {
|
||||
/// 공격 스킬
|
||||
@@ -103,6 +128,9 @@ class Skill {
|
||||
this.baseDotDamage,
|
||||
this.baseDotDurationMs,
|
||||
this.baseDotTickMs,
|
||||
this.hitCount = 1,
|
||||
this.lifestealPercent = 0.0,
|
||||
this.mpHealAmount = 0,
|
||||
});
|
||||
|
||||
/// 스킬 ID
|
||||
@@ -156,6 +184,15 @@ class Skill {
|
||||
/// DOT 기본 틱 간격 (밀리초, 스킬 레벨로 결정)
|
||||
final int? baseDotTickMs;
|
||||
|
||||
/// 다중 타격 횟수 (기본 1)
|
||||
final int hitCount;
|
||||
|
||||
/// HP 흡수율 (0.0 ~ 1.0, 데미지의 N% 회복)
|
||||
final double lifestealPercent;
|
||||
|
||||
/// MP 회복량 (MP 회복 스킬용)
|
||||
final int mpHealAmount;
|
||||
|
||||
/// 공격 스킬 여부
|
||||
bool get isAttack => type == SkillType.attack;
|
||||
|
||||
@@ -207,11 +244,7 @@ class SkillState {
|
||||
return cooldownMs - elapsed;
|
||||
}
|
||||
|
||||
SkillState copyWith({
|
||||
String? skillId,
|
||||
int? lastUsedMs,
|
||||
int? rank,
|
||||
}) {
|
||||
SkillState copyWith({String? skillId, int? lastUsedMs, int? rank}) {
|
||||
return SkillState(
|
||||
skillId: skillId ?? this.skillId,
|
||||
lastUsedMs: lastUsedMs ?? this.lastUsedMs,
|
||||
@@ -302,11 +335,7 @@ class SkillUseResult {
|
||||
|
||||
/// 실패 결과 생성
|
||||
factory SkillUseResult.failed(Skill skill, SkillFailReason reason) {
|
||||
return SkillUseResult(
|
||||
skill: skill,
|
||||
success: false,
|
||||
failReason: reason,
|
||||
);
|
||||
return SkillUseResult(skill: skill, success: false, failReason: reason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,7 +413,11 @@ class DotEffect {
|
||||
/// [skill] DOT 스킬
|
||||
/// [playerInt] 플레이어 INT (틱당 데미지 보정)
|
||||
/// [playerWis] 플레이어 WIS (틱 간격 보정)
|
||||
factory DotEffect.fromSkill(Skill skill, {int playerInt = 10, int playerWis = 10}) {
|
||||
factory DotEffect.fromSkill(
|
||||
Skill skill, {
|
||||
int playerInt = 10,
|
||||
int playerWis = 10,
|
||||
}) {
|
||||
assert(skill.isDot, 'DOT 스킬만 DotEffect 생성 가능');
|
||||
assert(skill.baseDotDamage != null, 'baseDotDamage 필수');
|
||||
assert(skill.baseDotDurationMs != null, 'baseDotDurationMs 필수');
|
||||
@@ -396,7 +429,9 @@ class DotEffect {
|
||||
|
||||
// WIS → 틱 간격 보정 (WIS 10 기준, ±2%/포인트, 빨라짐)
|
||||
final wisMod = 1.0 + (playerWis - 10) * 0.02;
|
||||
final actualTickMs = (skill.baseDotTickMs! / wisMod).clamp(200, 2000).round();
|
||||
final actualTickMs = (skill.baseDotTickMs! / wisMod)
|
||||
.clamp(200, 2000)
|
||||
.round();
|
||||
|
||||
return DotEffect(
|
||||
skillId: skill.id,
|
||||
|
||||
@@ -56,24 +56,28 @@ class NotificationService {
|
||||
|
||||
/// 레벨업 알림 (Level Up Notification)
|
||||
void showLevelUp(int newLevel) {
|
||||
show(GameNotification(
|
||||
type: NotificationType.levelUp,
|
||||
title: 'LEVEL UP!',
|
||||
subtitle: 'Level $newLevel',
|
||||
data: {'level': newLevel},
|
||||
duration: const Duration(seconds: 2),
|
||||
));
|
||||
show(
|
||||
GameNotification(
|
||||
type: NotificationType.levelUp,
|
||||
title: 'LEVEL UP!',
|
||||
subtitle: 'Level $newLevel',
|
||||
data: {'level': newLevel},
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 퀘스트 완료 알림
|
||||
void showQuestComplete(String questName) {
|
||||
show(GameNotification(
|
||||
type: NotificationType.questComplete,
|
||||
title: 'QUEST COMPLETE!',
|
||||
subtitle: questName,
|
||||
data: {'quest': questName},
|
||||
duration: const Duration(seconds: 2),
|
||||
));
|
||||
show(
|
||||
GameNotification(
|
||||
type: NotificationType.questComplete,
|
||||
title: 'QUEST COMPLETE!',
|
||||
subtitle: questName,
|
||||
data: {'quest': questName},
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 막 완료 알림 (Act Complete)
|
||||
@@ -82,44 +86,52 @@ class NotificationService {
|
||||
final title = actNumber == 0
|
||||
? 'PROLOGUE COMPLETE!'
|
||||
: 'ACT $actNumber COMPLETE!';
|
||||
show(GameNotification(
|
||||
type: NotificationType.actComplete,
|
||||
title: title,
|
||||
duration: const Duration(seconds: 3),
|
||||
));
|
||||
show(
|
||||
GameNotification(
|
||||
type: NotificationType.actComplete,
|
||||
title: title,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 새 주문 알림
|
||||
void showNewSpell(String spellName) {
|
||||
show(GameNotification(
|
||||
type: NotificationType.newSpell,
|
||||
title: 'NEW SPELL!',
|
||||
subtitle: spellName,
|
||||
data: {'spell': spellName},
|
||||
duration: const Duration(seconds: 2),
|
||||
));
|
||||
show(
|
||||
GameNotification(
|
||||
type: NotificationType.newSpell,
|
||||
title: 'NEW SPELL!',
|
||||
subtitle: spellName,
|
||||
data: {'spell': spellName},
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 새 장비 알림
|
||||
void showNewEquipment(String equipmentName, String slot) {
|
||||
show(GameNotification(
|
||||
type: NotificationType.newEquipment,
|
||||
title: 'NEW EQUIPMENT!',
|
||||
subtitle: equipmentName,
|
||||
data: {'equipment': equipmentName, 'slot': slot},
|
||||
duration: const Duration(seconds: 2),
|
||||
));
|
||||
show(
|
||||
GameNotification(
|
||||
type: NotificationType.newEquipment,
|
||||
title: 'NEW EQUIPMENT!',
|
||||
subtitle: equipmentName,
|
||||
data: {'equipment': equipmentName, 'slot': slot},
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 보스 처치 알림
|
||||
void showBossDefeat(String bossName) {
|
||||
show(GameNotification(
|
||||
type: NotificationType.bossDefeat,
|
||||
title: 'BOSS DEFEATED!',
|
||||
subtitle: bossName,
|
||||
data: {'boss': bossName},
|
||||
duration: const Duration(seconds: 3),
|
||||
));
|
||||
show(
|
||||
GameNotification(
|
||||
type: NotificationType.bossDefeat,
|
||||
title: 'BOSS DEFEATED!',
|
||||
subtitle: bossName,
|
||||
data: {'boss': bossName},
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 큐 처리 (Process Queue)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart';
|
||||
|
||||
/// 테마 설정 저장/로드 서비스
|
||||
class ThemePreferences {
|
||||
static const _keyColorTheme = 'ascii_color_theme';
|
||||
|
||||
/// 테마 설정 저장
|
||||
static Future<void> saveColorTheme(AsciiColorTheme theme) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_keyColorTheme, theme.index);
|
||||
}
|
||||
|
||||
/// 테마 설정 로드 (기본값: green)
|
||||
static Future<AsciiColorTheme> loadColorTheme() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final index = prefs.getInt(_keyColorTheme);
|
||||
if (index == null || index < 0 || index >= AsciiColorTheme.values.length) {
|
||||
return AsciiColorTheme.green;
|
||||
}
|
||||
return AsciiColorTheme.values[index];
|
||||
}
|
||||
}
|
||||
@@ -42,12 +42,7 @@ class EquipResult {
|
||||
|
||||
/// 아이템 생성 결과 (구조화된 데이터로 l10n 지원)
|
||||
class ItemResult {
|
||||
const ItemResult({
|
||||
this.attrib,
|
||||
this.special,
|
||||
this.itemOf,
|
||||
this.boringItem,
|
||||
});
|
||||
const ItemResult({this.attrib, this.special, this.itemOf, this.boringItem});
|
||||
|
||||
/// 아이템 속성 (예: "Golden")
|
||||
final String? attrib;
|
||||
@@ -592,7 +587,8 @@ MonsterTaskResult monsterTask(
|
||||
}
|
||||
|
||||
if (!definite) {
|
||||
name = indefinite(name, qty);
|
||||
// l10n 지원: 한국어/일본어에서는 관사 불필요
|
||||
name = l10n.indefiniteL10n(name, qty);
|
||||
}
|
||||
|
||||
return MonsterTaskResult(
|
||||
@@ -638,10 +634,13 @@ class QuestResult {
|
||||
|
||||
final String caption;
|
||||
final RewardKind reward;
|
||||
|
||||
/// 몬스터 전체 데이터 (예: "Rat|5|tail") - 원본 fQuest.Caption
|
||||
final String? monsterName;
|
||||
|
||||
/// 몬스터 레벨 (파싱된 값)
|
||||
final int? monsterLevel;
|
||||
|
||||
/// 몬스터 인덱스 (config.monsters에서의 위치) - 원본 fQuest.Tag
|
||||
final int? monsterIndex;
|
||||
}
|
||||
@@ -694,10 +693,7 @@ QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
|
||||
case 2:
|
||||
final itemEn = boringItem(config, rng);
|
||||
final item = l10n.translateBoringItem(itemEn);
|
||||
return QuestResult(
|
||||
caption: l10n.questTransfer(item),
|
||||
reward: reward,
|
||||
);
|
||||
return QuestResult(caption: l10n.questTransfer(item), reward: reward);
|
||||
case 3:
|
||||
final itemEn = boringItem(config, rng);
|
||||
final item = l10n.translateBoringItem(itemEn);
|
||||
|
||||
Reference in New Issue
Block a user