Compare commits
5 Commits
a41984d998
...
23f15f41d3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23f15f41d3 | ||
|
|
133d516b94 | ||
|
|
07fb105d7c | ||
|
|
e77c3c4a05 | ||
|
|
f466e1c408 |
File diff suppressed because it is too large
Load Diff
262
lib/src/core/engine/act_progression_service.dart
Normal file
262
lib/src/core/engine/act_progression_service.dart
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
|
import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/combat_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/combat_stats.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/monster_combat_stats.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/balance_constants.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||||
|
|
||||||
|
/// Act 진행 관련 로직을 처리하는 서비스
|
||||||
|
///
|
||||||
|
/// ProgressService에서 추출된 Act 완료, 보스 생성 등의 로직 담당.
|
||||||
|
class ActProgressionService {
|
||||||
|
const ActProgressionService({
|
||||||
|
required this.config,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PqConfig config;
|
||||||
|
|
||||||
|
/// Act 완료 처리
|
||||||
|
///
|
||||||
|
/// 플롯 진행, Act Boss 시네마틱 후 호출.
|
||||||
|
/// Returns gameComplete=true if Final Boss was defeated (game ends).
|
||||||
|
({GameState state, bool gameComplete}) completeAct(GameState state) {
|
||||||
|
// Act V 완료 시 (plotStageCount == 6) → 최종 보스 전투 시작
|
||||||
|
// plotStageCount: 1=Prologue, 2=Act I, 3=Act II, 4=Act III, 5=Act IV, 6=Act V
|
||||||
|
if (state.progress.plotStageCount >= 6) {
|
||||||
|
// 이미 최종 보스가 처치되었으면 게임 클리어
|
||||||
|
if (state.progress.finalBossState == FinalBossState.defeated) {
|
||||||
|
final updatedPlotHistory = [
|
||||||
|
...state.progress.plotHistory.map(
|
||||||
|
(e) => e.isComplete ? e : e.copyWith(isComplete: true),
|
||||||
|
),
|
||||||
|
const HistoryEntry(caption: '*** THE END ***', isComplete: true),
|
||||||
|
];
|
||||||
|
|
||||||
|
final updatedProgress = state.progress.copyWith(
|
||||||
|
plotHistory: updatedPlotHistory,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
state: state.copyWith(progress: updatedProgress),
|
||||||
|
gameComplete: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 최종 보스가 아직 등장하지 않았으면 보스 전투 시작
|
||||||
|
if (state.progress.finalBossState == FinalBossState.notSpawned) {
|
||||||
|
final updatedProgress = state.progress.copyWith(
|
||||||
|
finalBossState: FinalBossState.fighting,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 게임은 아직 끝나지 않음 - 보스 전투 진행
|
||||||
|
return (
|
||||||
|
state: state.copyWith(progress: updatedProgress),
|
||||||
|
gameComplete: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 보스 전투 중이면 계속 진행 (게임 종료 안 함)
|
||||||
|
return (state: state, gameComplete: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
final actResult = pq_logic.completeAct(state.progress.plotStageCount);
|
||||||
|
var nextState = state;
|
||||||
|
|
||||||
|
// 보상 처리는 호출자(ProgressService)가 담당
|
||||||
|
// 여기서는 플롯 상태만 업데이트
|
||||||
|
|
||||||
|
final plotStages = nextState.progress.plotStageCount + 1;
|
||||||
|
|
||||||
|
// 플롯 히스토리 업데이트: 이전 플롯 완료 표시, 새 플롯 추가
|
||||||
|
final updatedPlotHistory = [
|
||||||
|
...nextState.progress.plotHistory.map(
|
||||||
|
(e) => e.isComplete ? e : e.copyWith(isComplete: true),
|
||||||
|
),
|
||||||
|
HistoryEntry(caption: actResult.actTitle, isComplete: false),
|
||||||
|
];
|
||||||
|
|
||||||
|
var updatedProgress = nextState.progress.copyWith(
|
||||||
|
plot: ProgressBarState(position: 0, max: actResult.plotBarMaxSeconds),
|
||||||
|
plotStageCount: plotStages,
|
||||||
|
plotHistory: updatedPlotHistory,
|
||||||
|
);
|
||||||
|
|
||||||
|
nextState = nextState.copyWith(progress: updatedProgress);
|
||||||
|
|
||||||
|
// Act I 완료 후(Prologue -> Act I) 첫 퀘스트 시작 (원본 Main.pas 로직)
|
||||||
|
// plotStages == 2는 Prologue(1) -> Act I(2) 전환을 의미
|
||||||
|
if (plotStages == 2) {
|
||||||
|
nextState = startFirstQuest(nextState);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (state: nextState, gameComplete: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Act 완료 시 적용할 보상 목록 반환
|
||||||
|
List<pq_logic.RewardKind> getActRewards(int plotStageCount) {
|
||||||
|
final actResult = pq_logic.completeAct(plotStageCount);
|
||||||
|
return actResult.rewards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 첫 퀘스트 시작 (Act I 시작 시)
|
||||||
|
GameState startFirstQuest(GameState state) {
|
||||||
|
final result = pq_logic.completeQuest(
|
||||||
|
config,
|
||||||
|
state.rng,
|
||||||
|
state.traits.level,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 퀘스트 바 초기화
|
||||||
|
final questBar = ProgressBarState(
|
||||||
|
position: 0,
|
||||||
|
max: 50 + state.rng.nextInt(100),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 첫 퀘스트 히스토리 추가
|
||||||
|
final questHistory = [
|
||||||
|
HistoryEntry(caption: result.caption, isComplete: false),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 퀘스트 몬스터 정보 저장 (Exterminate 타입용)
|
||||||
|
final questMonster = result.monsterIndex != null
|
||||||
|
? QuestMonsterInfo(
|
||||||
|
monsterData: result.monsterName!,
|
||||||
|
monsterIndex: result.monsterIndex!,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// 첫 퀘스트 추가
|
||||||
|
final updatedQueue = QueueState(
|
||||||
|
entries: [
|
||||||
|
...state.queue.entries,
|
||||||
|
QueueEntry(
|
||||||
|
kind: QueueKind.task,
|
||||||
|
durationMillis: 50 + state.rng.nextInt(100),
|
||||||
|
caption: result.caption,
|
||||||
|
taskType: TaskType.neutral,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final progress = state.progress.copyWith(
|
||||||
|
quest: questBar,
|
||||||
|
questCount: 1,
|
||||||
|
questHistory: questHistory,
|
||||||
|
currentQuestMonster: questMonster,
|
||||||
|
);
|
||||||
|
|
||||||
|
return state.copyWith(progress: progress, queue: updatedQueue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Act Boss 생성 (Act 완료 시)
|
||||||
|
///
|
||||||
|
/// 보스 레벨 = min(플레이어 레벨, Act 최소 레벨)로 설정하여
|
||||||
|
/// 플레이어가 이길 수 있는 수준 보장
|
||||||
|
CombatState createActBoss(GameState state) {
|
||||||
|
final plotStage = state.progress.plotStageCount;
|
||||||
|
final actNumber = plotStage + 1;
|
||||||
|
|
||||||
|
// 보스 레벨 = min(플레이어 레벨, Act 최소 레벨)
|
||||||
|
// → 플레이어가 현재 레벨보다 높은 보스를 만나지 않도록 보장
|
||||||
|
final actMinLevel = ActMonsterLevel.forPlotStage(actNumber);
|
||||||
|
final bossLevel = math.min(state.traits.level, actMinLevel);
|
||||||
|
|
||||||
|
// Named monster 생성 (pq_logic.namedMonster 활용)
|
||||||
|
final bossName = pq_logic.namedMonster(config, state.rng, bossLevel);
|
||||||
|
|
||||||
|
final bossStats = MonsterBaseStats.forLevel(bossLevel);
|
||||||
|
|
||||||
|
// 플레이어 전투 스탯 생성
|
||||||
|
final playerCombatStats = CombatStats.fromStats(
|
||||||
|
stats: state.stats,
|
||||||
|
equipment: state.equipment,
|
||||||
|
level: state.traits.level,
|
||||||
|
monsterLevel: bossLevel,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Boss 몬스터 스탯 생성 (일반 몬스터 대비 강화)
|
||||||
|
final monsterCombatStats = MonsterCombatStats(
|
||||||
|
name: bossName,
|
||||||
|
level: bossLevel,
|
||||||
|
atk: (bossStats.atk * 1.5).round(), // Boss 보정 (1.5배)
|
||||||
|
def: (bossStats.def * 1.5).round(),
|
||||||
|
hpMax: (bossStats.hp * 2.0).round(), // HP는 2.0배 (보스다운 전투 시간)
|
||||||
|
hpCurrent: (bossStats.hp * 2.0).round(),
|
||||||
|
criRate: 0.05,
|
||||||
|
criDamage: 1.5,
|
||||||
|
evasion: 0.0,
|
||||||
|
accuracy: 0.8,
|
||||||
|
attackDelayMs: 1000,
|
||||||
|
expReward: (bossStats.exp * 2.5).round(), // 경험치 보상 증가
|
||||||
|
);
|
||||||
|
|
||||||
|
// 전투 상태 초기화
|
||||||
|
return CombatState.start(
|
||||||
|
playerStats: playerCombatStats,
|
||||||
|
monsterStats: monsterCombatStats,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 최종 보스(Glitch God) 전투 시작
|
||||||
|
///
|
||||||
|
/// Act V 플롯 완료 후 호출되며, 글리치 갓과의 전투를 설정합니다.
|
||||||
|
({ProgressState progress, QueueState queue}) startFinalBossFight(
|
||||||
|
GameState state,
|
||||||
|
ProgressState progress,
|
||||||
|
QueueState queue,
|
||||||
|
) {
|
||||||
|
final level = state.traits.level;
|
||||||
|
|
||||||
|
// Glitch God 생성 (레벨 100 최종 보스)
|
||||||
|
final glitchGod = MonsterCombatStats.glitchGod();
|
||||||
|
|
||||||
|
// 플레이어 전투 스탯 생성 (Phase 12: 보스 레벨 기반 페널티 적용)
|
||||||
|
final playerCombatStats = CombatStats.fromStats(
|
||||||
|
stats: state.stats,
|
||||||
|
equipment: state.equipment,
|
||||||
|
level: level,
|
||||||
|
monsterLevel: glitchGod.level,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 전투 상태 초기화
|
||||||
|
final combatState = CombatState.start(
|
||||||
|
playerStats: playerCombatStats,
|
||||||
|
monsterStats: glitchGod,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 전투 시간 추정 (보스 전투는 더 길게)
|
||||||
|
final combatCalculator = CombatCalculator(rng: state.rng);
|
||||||
|
final baseDuration = combatCalculator.estimateCombatDurationMs(
|
||||||
|
player: playerCombatStats,
|
||||||
|
monster: glitchGod,
|
||||||
|
);
|
||||||
|
// 최종 보스는 최소 10초, 최대 60초
|
||||||
|
final durationMillis = baseDuration.clamp(10000, 60000);
|
||||||
|
|
||||||
|
final taskResult = pq_logic.startTask(
|
||||||
|
progress,
|
||||||
|
l10n.taskFinalBoss(glitchGod.name),
|
||||||
|
durationMillis,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedProgress = taskResult.progress.copyWith(
|
||||||
|
currentTask: TaskInfo(
|
||||||
|
caption: taskResult.caption,
|
||||||
|
type: TaskType.kill,
|
||||||
|
monsterBaseName: 'Glitch God',
|
||||||
|
monsterPart: '*', // 특수 전리품
|
||||||
|
monsterLevel: glitchGod.level,
|
||||||
|
monsterGrade: MonsterGrade.boss, // 최종 보스는 항상 boss 등급
|
||||||
|
),
|
||||||
|
currentCombat: combatState,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (progress: updatedProgress, queue: queue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
|
import 'package:asciineverdie/src/core/engine/act_progression_service.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
|
import 'package:asciineverdie/src/core/engine/combat_calculator.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/combat_tick_service.dart';
|
import 'package:asciineverdie/src/core/engine/combat_tick_service.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/game_mutations.dart';
|
import 'package:asciineverdie/src/core/engine/game_mutations.dart';
|
||||||
@@ -434,7 +435,8 @@ class ProgressService {
|
|||||||
progress.plot.position >= progress.plot.max &&
|
progress.plot.position >= progress.plot.max &&
|
||||||
!progress.pendingActCompletion) {
|
!progress.pendingActCompletion) {
|
||||||
// Act Boss 소환 및 플래그 설정
|
// Act Boss 소환 및 플래그 설정
|
||||||
final actBoss = _createActBoss(nextState);
|
final actProgressionService = ActProgressionService(config: config);
|
||||||
|
final actBoss = actProgressionService.createActBoss(nextState);
|
||||||
progress = progress.copyWith(
|
progress = progress.copyWith(
|
||||||
plot: progress.plot.copyWith(position: 0), // Plot bar 리셋
|
plot: progress.plot.copyWith(position: 0), // Plot bar 리셋
|
||||||
currentCombat: actBoss,
|
currentCombat: actBoss,
|
||||||
@@ -561,7 +563,8 @@ class ProgressService {
|
|||||||
// 3. Act Boss 리트라이 체크
|
// 3. Act Boss 리트라이 체크
|
||||||
// pendingActCompletion이 true면 Act Boss 재소환
|
// pendingActCompletion이 true면 Act Boss 재소환
|
||||||
if (state.progress.pendingActCompletion) {
|
if (state.progress.pendingActCompletion) {
|
||||||
final actBoss = _createActBoss(state);
|
final actProgressionService = ActProgressionService(config: config);
|
||||||
|
final actBoss = actProgressionService.createActBoss(state);
|
||||||
final combatCalculator = CombatCalculator(rng: state.rng);
|
final combatCalculator = CombatCalculator(rng: state.rng);
|
||||||
final durationMillis = combatCalculator.estimateCombatDurationMs(
|
final durationMillis = combatCalculator.estimateCombatDurationMs(
|
||||||
player: actBoss.playerStats,
|
player: actBoss.playerStats,
|
||||||
@@ -601,7 +604,8 @@ class ProgressService {
|
|||||||
if (state.progress.bossLevelingEndTime != null) {
|
if (state.progress.bossLevelingEndTime != null) {
|
||||||
progress = progress.copyWith(clearBossLevelingEndTime: true);
|
progress = progress.copyWith(clearBossLevelingEndTime: true);
|
||||||
}
|
}
|
||||||
return _startFinalBossFight(state, progress, queue);
|
final actProgressionService = ActProgressionService(config: config);
|
||||||
|
return actProgressionService.startFinalBossFight(state, progress, queue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -684,63 +688,6 @@ class ProgressService {
|
|||||||
return (progress: progress, queue: queue);
|
return (progress: progress, queue: queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 최종 보스(Glitch God) 전투 시작
|
|
||||||
///
|
|
||||||
/// Act V 플롯 완료 후 호출되며, 글리치 갓과의 전투를 설정합니다.
|
|
||||||
({ProgressState progress, QueueState queue}) _startFinalBossFight(
|
|
||||||
GameState state,
|
|
||||||
ProgressState progress,
|
|
||||||
QueueState queue,
|
|
||||||
) {
|
|
||||||
final level = state.traits.level;
|
|
||||||
|
|
||||||
// Glitch God 생성 (레벨 100 최종 보스)
|
|
||||||
final glitchGod = MonsterCombatStats.glitchGod();
|
|
||||||
|
|
||||||
// 플레이어 전투 스탯 생성 (Phase 12: 보스 레벨 기반 페널티 적용)
|
|
||||||
final playerCombatStats = CombatStats.fromStats(
|
|
||||||
stats: state.stats,
|
|
||||||
equipment: state.equipment,
|
|
||||||
level: level,
|
|
||||||
monsterLevel: glitchGod.level,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 전투 상태 초기화
|
|
||||||
final combatState = CombatState.start(
|
|
||||||
playerStats: playerCombatStats,
|
|
||||||
monsterStats: glitchGod,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 전투 시간 추정 (보스 전투는 더 길게)
|
|
||||||
final combatCalculator = CombatCalculator(rng: state.rng);
|
|
||||||
final baseDuration = combatCalculator.estimateCombatDurationMs(
|
|
||||||
player: playerCombatStats,
|
|
||||||
monster: glitchGod,
|
|
||||||
);
|
|
||||||
// 최종 보스는 최소 10초, 최대 60초
|
|
||||||
final durationMillis = baseDuration.clamp(10000, 60000);
|
|
||||||
|
|
||||||
final taskResult = pq_logic.startTask(
|
|
||||||
progress,
|
|
||||||
l10n.taskFinalBoss(glitchGod.name),
|
|
||||||
durationMillis,
|
|
||||||
);
|
|
||||||
|
|
||||||
final updatedProgress = taskResult.progress.copyWith(
|
|
||||||
currentTask: TaskInfo(
|
|
||||||
caption: taskResult.caption,
|
|
||||||
type: TaskType.kill,
|
|
||||||
monsterBaseName: 'Glitch God',
|
|
||||||
monsterPart: '*', // 특수 전리품
|
|
||||||
monsterLevel: glitchGod.level,
|
|
||||||
monsterGrade: MonsterGrade.boss, // 최종 보스는 항상 boss 등급
|
|
||||||
),
|
|
||||||
currentCombat: combatState,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (progress: updatedProgress, queue: queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Advances quest completion, applies reward, and enqueues next quest task.
|
/// Advances quest completion, applies reward, and enqueues next quest task.
|
||||||
GameState completeQuest(GameState state) {
|
GameState completeQuest(GameState state) {
|
||||||
final result = pq_logic.completeQuest(
|
final result = pq_logic.completeQuest(
|
||||||
@@ -801,127 +748,24 @@ class ProgressService {
|
|||||||
/// Advances plot to next act and applies any act-level rewards.
|
/// Advances plot to next act and applies any act-level rewards.
|
||||||
/// Returns gameComplete=true if Final Boss was defeated (game ends).
|
/// Returns gameComplete=true if Final Boss was defeated (game ends).
|
||||||
({GameState state, bool gameComplete}) completeAct(GameState state) {
|
({GameState state, bool gameComplete}) completeAct(GameState state) {
|
||||||
// Act V 완료 시 (plotStageCount == 6) → 최종 보스 전투 시작
|
final actProgressionService = ActProgressionService(config: config);
|
||||||
// plotStageCount: 1=Prologue, 2=Act I, 3=Act II, 4=Act III, 5=Act IV, 6=Act V
|
|
||||||
if (state.progress.plotStageCount >= 6) {
|
|
||||||
// 이미 최종 보스가 처치되었으면 게임 클리어
|
|
||||||
if (state.progress.finalBossState == FinalBossState.defeated) {
|
|
||||||
final updatedPlotHistory = [
|
|
||||||
...state.progress.plotHistory.map(
|
|
||||||
(e) => e.isComplete ? e : e.copyWith(isComplete: true),
|
|
||||||
),
|
|
||||||
const HistoryEntry(caption: '*** THE END ***', isComplete: true),
|
|
||||||
];
|
|
||||||
|
|
||||||
final updatedProgress = state.progress.copyWith(
|
// Act 보상 먼저 적용
|
||||||
plotHistory: updatedPlotHistory,
|
final actRewards = actProgressionService.getActRewards(
|
||||||
|
state.progress.plotStageCount,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
|
||||||
state: state.copyWith(progress: updatedProgress),
|
|
||||||
gameComplete: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 최종 보스가 아직 등장하지 않았으면 보스 전투 시작
|
|
||||||
if (state.progress.finalBossState == FinalBossState.notSpawned) {
|
|
||||||
final updatedProgress = state.progress.copyWith(
|
|
||||||
finalBossState: FinalBossState.fighting,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 게임은 아직 끝나지 않음 - 보스 전투 진행
|
|
||||||
return (
|
|
||||||
state: state.copyWith(progress: updatedProgress),
|
|
||||||
gameComplete: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 보스 전투 중이면 계속 진행 (게임 종료 안 함)
|
|
||||||
return (state: state, gameComplete: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
final actResult = pq_logic.completeAct(state.progress.plotStageCount);
|
|
||||||
var nextState = state;
|
var nextState = state;
|
||||||
for (final reward in actResult.rewards) {
|
for (final reward in actRewards) {
|
||||||
nextState = _applyReward(nextState, reward);
|
nextState = _applyReward(nextState, reward);
|
||||||
}
|
}
|
||||||
|
|
||||||
final plotStages = nextState.progress.plotStageCount + 1;
|
// Act 완료 처리 (ActProgressionService 위임)
|
||||||
|
final result = actProgressionService.completeAct(nextState);
|
||||||
|
|
||||||
// 플롯 히스토리 업데이트: 이전 플롯 완료 표시, 새 플롯 추가
|
return (
|
||||||
final updatedPlotHistory = [
|
state: _recalculateEncumbrance(result.state),
|
||||||
...nextState.progress.plotHistory.map(
|
gameComplete: result.gameComplete,
|
||||||
(e) => e.isComplete ? e : e.copyWith(isComplete: true),
|
|
||||||
),
|
|
||||||
HistoryEntry(caption: actResult.actTitle, isComplete: false),
|
|
||||||
];
|
|
||||||
|
|
||||||
var updatedProgress = nextState.progress.copyWith(
|
|
||||||
plot: ProgressBarState(position: 0, max: actResult.plotBarMaxSeconds),
|
|
||||||
plotStageCount: plotStages,
|
|
||||||
plotHistory: updatedPlotHistory,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
nextState = nextState.copyWith(progress: updatedProgress);
|
|
||||||
|
|
||||||
// Act I 완료 후(Prologue -> Act I) 첫 퀘스트 시작 (원본 Main.pas 로직)
|
|
||||||
// plotStages == 2는 Prologue(1) -> Act I(2) 전환을 의미
|
|
||||||
if (plotStages == 2) {
|
|
||||||
nextState = _startFirstQuest(nextState);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (state: _recalculateEncumbrance(nextState), gameComplete: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 첫 퀘스트 시작 (Act I 시작 시)
|
|
||||||
GameState _startFirstQuest(GameState state) {
|
|
||||||
final result = pq_logic.completeQuest(
|
|
||||||
config,
|
|
||||||
state.rng,
|
|
||||||
state.traits.level,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 퀘스트 바 초기화
|
|
||||||
final questBar = ProgressBarState(
|
|
||||||
position: 0,
|
|
||||||
max: 50 + state.rng.nextInt(100),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 첫 퀘스트 히스토리 추가
|
|
||||||
final questHistory = [
|
|
||||||
HistoryEntry(caption: result.caption, isComplete: false),
|
|
||||||
];
|
|
||||||
|
|
||||||
// 퀘스트 몬스터 정보 저장 (Exterminate 타입용)
|
|
||||||
// 원본 fQuest.Caption = monsterData, fQuest.Tag = monsterIndex
|
|
||||||
final questMonster = result.monsterIndex != null
|
|
||||||
? QuestMonsterInfo(
|
|
||||||
monsterData: result.monsterName!,
|
|
||||||
monsterIndex: result.monsterIndex!,
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// 첫 퀘스트 추가
|
|
||||||
final updatedQueue = QueueState(
|
|
||||||
entries: [
|
|
||||||
...state.queue.entries,
|
|
||||||
QueueEntry(
|
|
||||||
kind: QueueKind.task,
|
|
||||||
durationMillis: 50 + state.rng.nextInt(100),
|
|
||||||
caption: result.caption,
|
|
||||||
taskType: TaskType.neutral,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
final progress = state.progress.copyWith(
|
|
||||||
quest: questBar,
|
|
||||||
questCount: 1,
|
|
||||||
questHistory: questHistory,
|
|
||||||
currentQuestMonster: questMonster,
|
|
||||||
);
|
|
||||||
|
|
||||||
return state.copyWith(progress: progress, queue: updatedQueue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Developer-only cheat hooks for quickly finishing bars.
|
/// Developer-only cheat hooks for quickly finishing bars.
|
||||||
@@ -1096,55 +940,6 @@ class ProgressService {
|
|||||||
return s[0].toUpperCase() + s.substring(1);
|
return s[0].toUpperCase() + s.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Act Boss 생성 (Act 완료 시)
|
|
||||||
///
|
|
||||||
/// 보스 레벨 = min(플레이어 레벨, Act 최소 레벨)로 설정하여
|
|
||||||
/// 플레이어가 이길 수 있는 수준 보장
|
|
||||||
CombatState _createActBoss(GameState state) {
|
|
||||||
final plotStage = state.progress.plotStageCount;
|
|
||||||
final actNumber = plotStage + 1;
|
|
||||||
|
|
||||||
// 보스 레벨 = min(플레이어 레벨, Act 최소 레벨)
|
|
||||||
// → 플레이어가 현재 레벨보다 높은 보스를 만나지 않도록 보장
|
|
||||||
final actMinLevel = ActMonsterLevel.forPlotStage(actNumber);
|
|
||||||
final bossLevel = math.min(state.traits.level, actMinLevel);
|
|
||||||
|
|
||||||
// Named monster 생성 (pq_logic.namedMonster 활용)
|
|
||||||
final bossName = pq_logic.namedMonster(config, state.rng, bossLevel);
|
|
||||||
|
|
||||||
final bossStats = MonsterBaseStats.forLevel(bossLevel);
|
|
||||||
|
|
||||||
// 플레이어 전투 스탯 생성
|
|
||||||
final playerCombatStats = CombatStats.fromStats(
|
|
||||||
stats: state.stats,
|
|
||||||
equipment: state.equipment,
|
|
||||||
level: state.traits.level,
|
|
||||||
monsterLevel: bossLevel,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Boss 몬스터 스탯 생성 (일반 몬스터 대비 강화)
|
|
||||||
final monsterCombatStats = MonsterCombatStats(
|
|
||||||
name: bossName,
|
|
||||||
level: bossLevel,
|
|
||||||
atk: (bossStats.atk * 1.5).round(), // Boss 보정 (1.5배)
|
|
||||||
def: (bossStats.def * 1.5).round(),
|
|
||||||
hpMax: (bossStats.hp * 2.0).round(), // HP는 2.0배 (보스다운 전투 시간)
|
|
||||||
hpCurrent: (bossStats.hp * 2.0).round(),
|
|
||||||
criRate: 0.05,
|
|
||||||
criDamage: 1.5,
|
|
||||||
evasion: 0.0,
|
|
||||||
accuracy: 0.8,
|
|
||||||
attackDelayMs: 1000,
|
|
||||||
expReward: (bossStats.exp * 2.5).round(), // 경험치 보상 증가
|
|
||||||
);
|
|
||||||
|
|
||||||
// 전투 상태 초기화
|
|
||||||
return CombatState.start(
|
|
||||||
playerStats: playerCombatStats,
|
|
||||||
monsterStats: monsterCombatStats,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 플레이어 사망 처리 (Phase 4)
|
/// 플레이어 사망 처리 (Phase 4)
|
||||||
///
|
///
|
||||||
/// 모든 장비 상실 및 사망 정보 기록
|
/// 모든 장비 상실 및 사망 정보 기록
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
import 'package:asciineverdie/src/core/model/class_traits.dart';
|
import 'package:asciineverdie/src/core/model/class_traits.dart';
|
||||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
import 'package:asciineverdie/src/core/model/race_traits.dart';
|
import 'package:asciineverdie/src/core/model/race_traits.dart';
|
||||||
|
|
||||||
|
part 'combat_stats.freezed.dart';
|
||||||
|
part 'combat_stats.g.dart';
|
||||||
|
|
||||||
/// 전투용 파생 스탯
|
/// 전투용 파생 스탯
|
||||||
///
|
///
|
||||||
/// 기본 Stats와 Equipment를 기반으로 계산되는 전투 관련 수치.
|
/// 기본 Stats와 Equipment를 기반으로 계산되는 전투 관련 수치.
|
||||||
/// 불변(immutable) 객체로 설계되어 상태 변경 시 새 인스턴스 생성.
|
/// 불변(immutable) 객체로 설계되어 상태 변경 시 새 인스턴스 생성.
|
||||||
class CombatStats {
|
@freezed
|
||||||
|
class CombatStats with _$CombatStats {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 레벨 페널티 상수 (Phase 12)
|
// 레벨 페널티 상수 (Phase 12)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -38,121 +44,65 @@ class CombatStats {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// 레벨 차이에 따른 확률 감소 배율 (플레이어 전용)
|
/// 레벨 차이에 따른 확률 감소 배율 (플레이어 전용)
|
||||||
///
|
|
||||||
/// - levelDiff = monsterLevel - playerLevel (몬스터가 높으면 양수)
|
|
||||||
/// - 0레벨 차이: 1.0 (100% 유지)
|
|
||||||
/// - 10레벨 이상 차이: 0.2 (20% = 최저)
|
|
||||||
/// - 상승 없음 (플레이어가 높아도 보너스 없음)
|
|
||||||
static double _getLevelPenalty(int playerLevel, int monsterLevel) {
|
static double _getLevelPenalty(int playerLevel, int monsterLevel) {
|
||||||
final levelDiff = monsterLevel - playerLevel;
|
final levelDiff = monsterLevel - playerLevel;
|
||||||
if (levelDiff <= 0) return 1.0; // 플레이어가 높거나 같으면 페널티 없음
|
if (levelDiff <= 0) return 1.0;
|
||||||
|
|
||||||
// 1레벨당 8%씩 감소 (100% → 92% → 84% → ... → 20%)
|
|
||||||
final penalty = 1.0 - (levelDiff * _levelPenaltyPerLevel);
|
final penalty = 1.0 - (levelDiff * _levelPenaltyPerLevel);
|
||||||
return penalty.clamp(_minLevelMultiplier, 1.0);
|
return penalty.clamp(_minLevelMultiplier, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CombatStats({
|
const CombatStats._();
|
||||||
// 기본 스탯 (Stats에서 복사)
|
|
||||||
required this.str,
|
|
||||||
required this.con,
|
|
||||||
required this.dex,
|
|
||||||
required this.intelligence,
|
|
||||||
required this.wis,
|
|
||||||
required this.cha,
|
|
||||||
// 파생 스탯
|
|
||||||
required this.atk,
|
|
||||||
required this.def,
|
|
||||||
required this.magAtk,
|
|
||||||
required this.magDef,
|
|
||||||
required this.criRate,
|
|
||||||
required this.criDamage,
|
|
||||||
required this.evasion,
|
|
||||||
required this.accuracy,
|
|
||||||
required this.blockRate,
|
|
||||||
required this.parryRate,
|
|
||||||
required this.attackDelayMs,
|
|
||||||
// 자원
|
|
||||||
required this.hpMax,
|
|
||||||
required this.hpCurrent,
|
|
||||||
required this.mpMax,
|
|
||||||
required this.mpCurrent,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================================================
|
const factory CombatStats({
|
||||||
// 기본 스탯
|
// 기본 스탯
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// 힘: 물리 공격력 보정
|
/// 힘: 물리 공격력 보정
|
||||||
final int str;
|
required int str,
|
||||||
|
|
||||||
/// 체력: HP, 방어력 보정
|
/// 체력: HP, 방어력 보정
|
||||||
final int con;
|
required int con,
|
||||||
|
|
||||||
/// 민첩: 회피율, 크리티컬율, 명중률, 공격 속도
|
/// 민첩: 회피율, 크리티컬율, 명중률, 공격 속도
|
||||||
final int dex;
|
required int dex,
|
||||||
|
|
||||||
/// 지능: 마법 공격력, MP
|
/// 지능: 마법 공격력, MP
|
||||||
final int intelligence;
|
required int intelligence,
|
||||||
|
|
||||||
/// 지혜: 마법 방어력, MP 회복
|
/// 지혜: 마법 방어력, MP 회복
|
||||||
final int wis;
|
required int wis,
|
||||||
|
|
||||||
/// 매력: 상점 가격, 드롭률 보정
|
/// 매력: 상점 가격, 드롭률 보정
|
||||||
final int cha;
|
required int cha,
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 파생 스탯 (전투용)
|
// 파생 스탯 (전투용)
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// 물리 공격력
|
/// 물리 공격력
|
||||||
final int atk;
|
required int atk,
|
||||||
|
|
||||||
/// 물리 방어력
|
/// 물리 방어력
|
||||||
final int def;
|
required int def,
|
||||||
|
|
||||||
/// 마법 공격력
|
/// 마법 공격력
|
||||||
final int magAtk;
|
required int magAtk,
|
||||||
|
|
||||||
/// 마법 방어력
|
/// 마법 방어력
|
||||||
final int magDef;
|
required int magDef,
|
||||||
|
|
||||||
/// 크리티컬 확률 (0.0 ~ 1.0)
|
/// 크리티컬 확률 (0.0 ~ 1.0)
|
||||||
final double criRate;
|
required double criRate,
|
||||||
|
|
||||||
/// 크리티컬 데미지 배율 (1.5 ~ 3.0)
|
/// 크리티컬 데미지 배율 (1.5 ~ 3.0)
|
||||||
final double criDamage;
|
required double criDamage,
|
||||||
|
|
||||||
/// 회피율 (0.0 ~ 0.5)
|
/// 회피율 (0.0 ~ 0.5)
|
||||||
final double evasion;
|
required double evasion,
|
||||||
|
|
||||||
/// 명중률 (0.8 ~ 1.0)
|
/// 명중률 (0.8 ~ 1.0)
|
||||||
final double accuracy;
|
required double accuracy,
|
||||||
|
|
||||||
/// 방패 방어율 (0.0 ~ 0.4)
|
/// 방패 방어율 (0.0 ~ 0.4)
|
||||||
final double blockRate;
|
required double blockRate,
|
||||||
|
|
||||||
/// 무기로 쳐내기 확률 (0.0 ~ 0.3)
|
/// 무기로 쳐내기 확률 (0.0 ~ 0.3)
|
||||||
final double parryRate;
|
required double parryRate,
|
||||||
|
|
||||||
/// 공격 딜레이 (밀리초)
|
/// 공격 딜레이 (밀리초)
|
||||||
final int attackDelayMs;
|
required int attackDelayMs,
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 자원
|
// 자원
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/// 최대 HP
|
/// 최대 HP
|
||||||
final int hpMax;
|
required int hpMax,
|
||||||
|
|
||||||
/// 현재 HP
|
/// 현재 HP
|
||||||
final int hpCurrent;
|
required int hpCurrent,
|
||||||
|
|
||||||
/// 최대 MP
|
/// 최대 MP
|
||||||
final int mpMax;
|
required int mpMax,
|
||||||
|
|
||||||
/// 현재 MP
|
/// 현재 MP
|
||||||
final int mpCurrent;
|
required int mpCurrent,
|
||||||
|
}) = _CombatStats;
|
||||||
|
|
||||||
|
factory CombatStats.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$CombatStatsFromJson(json);
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 유틸리티
|
// 유틸리티
|
||||||
@@ -190,66 +140,11 @@ class CombatStats {
|
|||||||
return withHp(hpCurrent + amount);
|
return withHp(hpCurrent + amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
CombatStats copyWith({
|
|
||||||
int? str,
|
|
||||||
int? con,
|
|
||||||
int? dex,
|
|
||||||
int? intelligence,
|
|
||||||
int? wis,
|
|
||||||
int? cha,
|
|
||||||
int? atk,
|
|
||||||
int? def,
|
|
||||||
int? magAtk,
|
|
||||||
int? magDef,
|
|
||||||
double? criRate,
|
|
||||||
double? criDamage,
|
|
||||||
double? evasion,
|
|
||||||
double? accuracy,
|
|
||||||
double? blockRate,
|
|
||||||
double? parryRate,
|
|
||||||
int? attackDelayMs,
|
|
||||||
int? hpMax,
|
|
||||||
int? hpCurrent,
|
|
||||||
int? mpMax,
|
|
||||||
int? mpCurrent,
|
|
||||||
}) {
|
|
||||||
return CombatStats(
|
|
||||||
str: str ?? this.str,
|
|
||||||
con: con ?? this.con,
|
|
||||||
dex: dex ?? this.dex,
|
|
||||||
intelligence: intelligence ?? this.intelligence,
|
|
||||||
wis: wis ?? this.wis,
|
|
||||||
cha: cha ?? this.cha,
|
|
||||||
atk: atk ?? this.atk,
|
|
||||||
def: def ?? this.def,
|
|
||||||
magAtk: magAtk ?? this.magAtk,
|
|
||||||
magDef: magDef ?? this.magDef,
|
|
||||||
criRate: criRate ?? this.criRate,
|
|
||||||
criDamage: criDamage ?? this.criDamage,
|
|
||||||
evasion: evasion ?? this.evasion,
|
|
||||||
accuracy: accuracy ?? this.accuracy,
|
|
||||||
blockRate: blockRate ?? this.blockRate,
|
|
||||||
parryRate: parryRate ?? this.parryRate,
|
|
||||||
attackDelayMs: attackDelayMs ?? this.attackDelayMs,
|
|
||||||
hpMax: hpMax ?? this.hpMax,
|
|
||||||
hpCurrent: hpCurrent ?? this.hpCurrent,
|
|
||||||
mpMax: mpMax ?? this.mpMax,
|
|
||||||
mpCurrent: mpCurrent ?? this.mpCurrent,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 팩토리 메서드
|
// 팩토리 메서드
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Stats와 Equipment에서 CombatStats 생성
|
/// Stats와 Equipment에서 CombatStats 생성
|
||||||
///
|
|
||||||
/// [stats] 캐릭터 기본 스탯
|
|
||||||
/// [equipment] 장착 장비 (장비 스탯 적용)
|
|
||||||
/// [level] 캐릭터 레벨 (스케일링용)
|
|
||||||
/// [race] 종족 특성 (선택사항, Phase 5)
|
|
||||||
/// [klass] 클래스 특성 (선택사항, Phase 5)
|
|
||||||
/// [monsterLevel] 상대 몬스터 레벨 (레벨 페널티 계산용, Phase 12)
|
|
||||||
factory CombatStats.fromStats({
|
factory CombatStats.fromStats({
|
||||||
required Stats stats,
|
required Stats stats,
|
||||||
required Equipment equipment,
|
required Equipment equipment,
|
||||||
@@ -321,7 +216,6 @@ class CombatStats {
|
|||||||
final parryRate = (baseParryRate + equipStats.parryRate).clamp(0.0, 0.4);
|
final parryRate = (baseParryRate + equipStats.parryRate).clamp(0.0, 0.4);
|
||||||
|
|
||||||
// 공격 속도: 무기 기본 공속 + DEX 보정
|
// 공격 속도: 무기 기본 공속 + DEX 보정
|
||||||
// 무기 attackSpeed가 0이면 기본값 1000ms 사용
|
|
||||||
final weaponItem = equipment.items[0]; // 무기 슬롯
|
final weaponItem = equipment.items[0]; // 무기 슬롯
|
||||||
final weaponSpeed = weaponItem.stats.attackSpeed;
|
final weaponSpeed = weaponItem.stats.attackSpeed;
|
||||||
final baseAttackSpeed = weaponSpeed > 0 ? weaponSpeed : 1000;
|
final baseAttackSpeed = weaponSpeed > 0 ? weaponSpeed : 1000;
|
||||||
@@ -339,32 +233,27 @@ class CombatStats {
|
|||||||
// 종족 패시브 적용 (Phase 5)
|
// 종족 패시브 적용 (Phase 5)
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
// HP 보너스 (Heap Troll: +20%)
|
|
||||||
final raceHpBonus = race?.getPassiveValue(PassiveType.hpBonus) ?? 0.0;
|
final raceHpBonus = race?.getPassiveValue(PassiveType.hpBonus) ?? 0.0;
|
||||||
if (raceHpBonus > 0) {
|
if (raceHpBonus > 0) {
|
||||||
totalHpMax = (totalHpMax * (1 + raceHpBonus)).round();
|
totalHpMax = (totalHpMax * (1 + raceHpBonus)).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
// MP 보너스 (Pointer Fairy: +20%)
|
|
||||||
final raceMpBonus = race?.getPassiveValue(PassiveType.mpBonus) ?? 0.0;
|
final raceMpBonus = race?.getPassiveValue(PassiveType.mpBonus) ?? 0.0;
|
||||||
if (raceMpBonus > 0) {
|
if (raceMpBonus > 0) {
|
||||||
totalMpMax = (totalMpMax * (1 + raceMpBonus)).round();
|
totalMpMax = (totalMpMax * (1 + raceMpBonus)).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 마법 데미지 보너스 (Null Elf: +15%)
|
|
||||||
final raceMagicBonus =
|
final raceMagicBonus =
|
||||||
race?.getPassiveValue(PassiveType.magicDamageBonus) ?? 0.0;
|
race?.getPassiveValue(PassiveType.magicDamageBonus) ?? 0.0;
|
||||||
if (raceMagicBonus > 0) {
|
if (raceMagicBonus > 0) {
|
||||||
baseMagAtk = (baseMagAtk * (1 + raceMagicBonus)).round();
|
baseMagAtk = (baseMagAtk * (1 + raceMagicBonus)).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 방어력 보너스 (Buffer Dwarf: +10%)
|
|
||||||
final raceDefBonus = race?.getPassiveValue(PassiveType.defenseBonus) ?? 0.0;
|
final raceDefBonus = race?.getPassiveValue(PassiveType.defenseBonus) ?? 0.0;
|
||||||
if (raceDefBonus > 0) {
|
if (raceDefBonus > 0) {
|
||||||
baseDef = (baseDef * (1 + raceDefBonus)).round();
|
baseDef = (baseDef * (1 + raceDefBonus)).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 크리티컬 보너스 (Stack Goblin: +5%)
|
|
||||||
final raceCritBonus =
|
final raceCritBonus =
|
||||||
race?.getPassiveValue(PassiveType.criticalBonus) ?? 0.0;
|
race?.getPassiveValue(PassiveType.criticalBonus) ?? 0.0;
|
||||||
criRate += raceCritBonus;
|
criRate += raceCritBonus;
|
||||||
@@ -373,40 +262,34 @@ class CombatStats {
|
|||||||
// 클래스 패시브 적용 (Phase 5)
|
// 클래스 패시브 적용 (Phase 5)
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
// HP 보너스 (Garbage Collector: +30%)
|
|
||||||
final classHpBonus =
|
final classHpBonus =
|
||||||
klass?.getPassiveValue(ClassPassiveType.hpBonus) ?? 0.0;
|
klass?.getPassiveValue(ClassPassiveType.hpBonus) ?? 0.0;
|
||||||
if (classHpBonus > 0) {
|
if (classHpBonus > 0) {
|
||||||
totalHpMax = (totalHpMax * (1 + classHpBonus)).round();
|
totalHpMax = (totalHpMax * (1 + classHpBonus)).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 물리 공격력 보너스 (Bug Hunter: +20%)
|
|
||||||
final classPhysBonus =
|
final classPhysBonus =
|
||||||
klass?.getPassiveValue(ClassPassiveType.physicalDamageBonus) ?? 0.0;
|
klass?.getPassiveValue(ClassPassiveType.physicalDamageBonus) ?? 0.0;
|
||||||
if (classPhysBonus > 0) {
|
if (classPhysBonus > 0) {
|
||||||
baseAtk = (baseAtk * (1 + classPhysBonus)).round();
|
baseAtk = (baseAtk * (1 + classPhysBonus)).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 방어력 보너스 (Debugger Paladin: +15%)
|
|
||||||
final classDefBonus =
|
final classDefBonus =
|
||||||
klass?.getPassiveValue(ClassPassiveType.defenseBonus) ?? 0.0;
|
klass?.getPassiveValue(ClassPassiveType.defenseBonus) ?? 0.0;
|
||||||
if (classDefBonus > 0) {
|
if (classDefBonus > 0) {
|
||||||
baseDef = (baseDef * (1 + classDefBonus)).round();
|
baseDef = (baseDef * (1 + classDefBonus)).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 마법 데미지 보너스 (Compiler Mage: +25%)
|
|
||||||
final classMagBonus =
|
final classMagBonus =
|
||||||
klass?.getPassiveValue(ClassPassiveType.magicDamageBonus) ?? 0.0;
|
klass?.getPassiveValue(ClassPassiveType.magicDamageBonus) ?? 0.0;
|
||||||
if (classMagBonus > 0) {
|
if (classMagBonus > 0) {
|
||||||
baseMagAtk = (baseMagAtk * (1 + classMagBonus)).round();
|
baseMagAtk = (baseMagAtk * (1 + classMagBonus)).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 회피율 보너스 (Refactor Monk: +15%)
|
|
||||||
final classEvasionBonus =
|
final classEvasionBonus =
|
||||||
klass?.getPassiveValue(ClassPassiveType.evasionBonus) ?? 0.0;
|
klass?.getPassiveValue(ClassPassiveType.evasionBonus) ?? 0.0;
|
||||||
evasion += classEvasionBonus;
|
evasion += classEvasionBonus;
|
||||||
|
|
||||||
// 크리티컬 보너스 (Pointer Assassin: +20%)
|
|
||||||
final classCritBonus =
|
final classCritBonus =
|
||||||
klass?.getPassiveValue(ClassPassiveType.criticalBonus) ?? 0.0;
|
klass?.getPassiveValue(ClassPassiveType.criticalBonus) ?? 0.0;
|
||||||
criRate += classCritBonus;
|
criRate += classCritBonus;
|
||||||
@@ -415,13 +298,11 @@ class CombatStats {
|
|||||||
// 레벨 페널티 및 최종 클램핑 (Phase 12)
|
// 레벨 페널티 및 최종 클램핑 (Phase 12)
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
// 레벨 페널티 적용 (크리/회피/블록/패리)
|
|
||||||
criRate *= levelPenalty;
|
criRate *= levelPenalty;
|
||||||
evasion *= levelPenalty;
|
evasion *= levelPenalty;
|
||||||
var finalBlockRate = blockRate * levelPenalty;
|
var finalBlockRate = blockRate * levelPenalty;
|
||||||
var finalParryRate = parryRate * levelPenalty;
|
var finalParryRate = parryRate * levelPenalty;
|
||||||
|
|
||||||
// 최종 클램핑 (새 캡 적용)
|
|
||||||
criRate = criRate.clamp(0.05, _maxCriRate);
|
criRate = criRate.clamp(0.05, _maxCriRate);
|
||||||
evasion = evasion.clamp(0.0, _maxEvasion);
|
evasion = evasion.clamp(0.0, _maxEvasion);
|
||||||
finalBlockRate = finalBlockRate.clamp(0.0, _maxBlockRate);
|
finalBlockRate = finalBlockRate.clamp(0.0, _maxBlockRate);
|
||||||
@@ -452,60 +333,6 @@ class CombatStats {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JSON으로 직렬화
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'str': str,
|
|
||||||
'con': con,
|
|
||||||
'dex': dex,
|
|
||||||
'intelligence': intelligence,
|
|
||||||
'wis': wis,
|
|
||||||
'cha': cha,
|
|
||||||
'atk': atk,
|
|
||||||
'def': def,
|
|
||||||
'magAtk': magAtk,
|
|
||||||
'magDef': magDef,
|
|
||||||
'criRate': criRate,
|
|
||||||
'criDamage': criDamage,
|
|
||||||
'evasion': evasion,
|
|
||||||
'accuracy': accuracy,
|
|
||||||
'blockRate': blockRate,
|
|
||||||
'parryRate': parryRate,
|
|
||||||
'attackDelayMs': attackDelayMs,
|
|
||||||
'hpMax': hpMax,
|
|
||||||
'hpCurrent': hpCurrent,
|
|
||||||
'mpMax': mpMax,
|
|
||||||
'mpCurrent': mpCurrent,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// JSON에서 역직렬화
|
|
||||||
factory CombatStats.fromJson(Map<String, dynamic> json) {
|
|
||||||
return CombatStats(
|
|
||||||
str: json['str'] as int,
|
|
||||||
con: json['con'] as int,
|
|
||||||
dex: json['dex'] as int,
|
|
||||||
intelligence: json['intelligence'] as int,
|
|
||||||
wis: json['wis'] as int,
|
|
||||||
cha: json['cha'] as int,
|
|
||||||
atk: json['atk'] as int,
|
|
||||||
def: json['def'] as int,
|
|
||||||
magAtk: json['magAtk'] as int,
|
|
||||||
magDef: json['magDef'] as int,
|
|
||||||
criRate: (json['criRate'] as num).toDouble(),
|
|
||||||
criDamage: (json['criDamage'] as num).toDouble(),
|
|
||||||
evasion: (json['evasion'] as num).toDouble(),
|
|
||||||
accuracy: (json['accuracy'] as num).toDouble(),
|
|
||||||
blockRate: (json['blockRate'] as num).toDouble(),
|
|
||||||
parryRate: (json['parryRate'] as num).toDouble(),
|
|
||||||
attackDelayMs: json['attackDelayMs'] as int,
|
|
||||||
hpMax: json['hpMax'] as int,
|
|
||||||
hpCurrent: json['hpCurrent'] as int,
|
|
||||||
mpMax: json['mpMax'] as int,
|
|
||||||
mpCurrent: json['mpCurrent'] as int,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 테스트/디버그용 기본값
|
/// 테스트/디버그용 기본값
|
||||||
factory CombatStats.empty() => const CombatStats(
|
factory CombatStats.empty() => const CombatStats(
|
||||||
str: 10,
|
str: 10,
|
||||||
|
|||||||
733
lib/src/core/model/combat_stats.freezed.dart
Normal file
733
lib/src/core/model/combat_stats.freezed.dart
Normal file
@@ -0,0 +1,733 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'combat_stats.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
|
||||||
|
);
|
||||||
|
|
||||||
|
CombatStats _$CombatStatsFromJson(Map<String, dynamic> json) {
|
||||||
|
return _CombatStats.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$CombatStats {
|
||||||
|
// 기본 스탯
|
||||||
|
/// 힘: 물리 공격력 보정
|
||||||
|
int get str => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 체력: HP, 방어력 보정
|
||||||
|
int get con => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 민첩: 회피율, 크리티컬율, 명중률, 공격 속도
|
||||||
|
int get dex => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 지능: 마법 공격력, MP
|
||||||
|
int get intelligence => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 지혜: 마법 방어력, MP 회복
|
||||||
|
int get wis => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 매력: 상점 가격, 드롭률 보정
|
||||||
|
int get cha => throw _privateConstructorUsedError; // 파생 스탯 (전투용)
|
||||||
|
/// 물리 공격력
|
||||||
|
int get atk => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 물리 방어력
|
||||||
|
int get def => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 마법 공격력
|
||||||
|
int get magAtk => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 마법 방어력
|
||||||
|
int get magDef => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 크리티컬 확률 (0.0 ~ 1.0)
|
||||||
|
double get criRate => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 크리티컬 데미지 배율 (1.5 ~ 3.0)
|
||||||
|
double get criDamage => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 회피율 (0.0 ~ 0.5)
|
||||||
|
double get evasion => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 명중률 (0.8 ~ 1.0)
|
||||||
|
double get accuracy => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 방패 방어율 (0.0 ~ 0.4)
|
||||||
|
double get blockRate => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 무기로 쳐내기 확률 (0.0 ~ 0.3)
|
||||||
|
double get parryRate => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 공격 딜레이 (밀리초)
|
||||||
|
int get attackDelayMs => throw _privateConstructorUsedError; // 자원
|
||||||
|
/// 최대 HP
|
||||||
|
int get hpMax => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 현재 HP
|
||||||
|
int get hpCurrent => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 최대 MP
|
||||||
|
int get mpMax => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 현재 MP
|
||||||
|
int get mpCurrent => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this CombatStats to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of CombatStats
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$CombatStatsCopyWith<CombatStats> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $CombatStatsCopyWith<$Res> {
|
||||||
|
factory $CombatStatsCopyWith(
|
||||||
|
CombatStats value,
|
||||||
|
$Res Function(CombatStats) then,
|
||||||
|
) = _$CombatStatsCopyWithImpl<$Res, CombatStats>;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
int str,
|
||||||
|
int con,
|
||||||
|
int dex,
|
||||||
|
int intelligence,
|
||||||
|
int wis,
|
||||||
|
int cha,
|
||||||
|
int atk,
|
||||||
|
int def,
|
||||||
|
int magAtk,
|
||||||
|
int magDef,
|
||||||
|
double criRate,
|
||||||
|
double criDamage,
|
||||||
|
double evasion,
|
||||||
|
double accuracy,
|
||||||
|
double blockRate,
|
||||||
|
double parryRate,
|
||||||
|
int attackDelayMs,
|
||||||
|
int hpMax,
|
||||||
|
int hpCurrent,
|
||||||
|
int mpMax,
|
||||||
|
int mpCurrent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$CombatStatsCopyWithImpl<$Res, $Val extends CombatStats>
|
||||||
|
implements $CombatStatsCopyWith<$Res> {
|
||||||
|
_$CombatStatsCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of CombatStats
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? str = null,
|
||||||
|
Object? con = null,
|
||||||
|
Object? dex = null,
|
||||||
|
Object? intelligence = null,
|
||||||
|
Object? wis = null,
|
||||||
|
Object? cha = null,
|
||||||
|
Object? atk = null,
|
||||||
|
Object? def = null,
|
||||||
|
Object? magAtk = null,
|
||||||
|
Object? magDef = null,
|
||||||
|
Object? criRate = null,
|
||||||
|
Object? criDamage = null,
|
||||||
|
Object? evasion = null,
|
||||||
|
Object? accuracy = null,
|
||||||
|
Object? blockRate = null,
|
||||||
|
Object? parryRate = null,
|
||||||
|
Object? attackDelayMs = null,
|
||||||
|
Object? hpMax = null,
|
||||||
|
Object? hpCurrent = null,
|
||||||
|
Object? mpMax = null,
|
||||||
|
Object? mpCurrent = null,
|
||||||
|
}) {
|
||||||
|
return _then(
|
||||||
|
_value.copyWith(
|
||||||
|
str: null == str
|
||||||
|
? _value.str
|
||||||
|
: str // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
con: null == con
|
||||||
|
? _value.con
|
||||||
|
: con // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
dex: null == dex
|
||||||
|
? _value.dex
|
||||||
|
: dex // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
intelligence: null == intelligence
|
||||||
|
? _value.intelligence
|
||||||
|
: intelligence // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
wis: null == wis
|
||||||
|
? _value.wis
|
||||||
|
: wis // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
cha: null == cha
|
||||||
|
? _value.cha
|
||||||
|
: cha // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
atk: null == atk
|
||||||
|
? _value.atk
|
||||||
|
: atk // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
def: null == def
|
||||||
|
? _value.def
|
||||||
|
: def // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
magAtk: null == magAtk
|
||||||
|
? _value.magAtk
|
||||||
|
: magAtk // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
magDef: null == magDef
|
||||||
|
? _value.magDef
|
||||||
|
: magDef // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
criRate: null == criRate
|
||||||
|
? _value.criRate
|
||||||
|
: criRate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
criDamage: null == criDamage
|
||||||
|
? _value.criDamage
|
||||||
|
: criDamage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
evasion: null == evasion
|
||||||
|
? _value.evasion
|
||||||
|
: evasion // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
accuracy: null == accuracy
|
||||||
|
? _value.accuracy
|
||||||
|
: accuracy // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
blockRate: null == blockRate
|
||||||
|
? _value.blockRate
|
||||||
|
: blockRate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
parryRate: null == parryRate
|
||||||
|
? _value.parryRate
|
||||||
|
: parryRate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
attackDelayMs: null == attackDelayMs
|
||||||
|
? _value.attackDelayMs
|
||||||
|
: attackDelayMs // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
hpMax: null == hpMax
|
||||||
|
? _value.hpMax
|
||||||
|
: hpMax // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
hpCurrent: null == hpCurrent
|
||||||
|
? _value.hpCurrent
|
||||||
|
: hpCurrent // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
mpMax: null == mpMax
|
||||||
|
? _value.mpMax
|
||||||
|
: mpMax // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
mpCurrent: null == mpCurrent
|
||||||
|
? _value.mpCurrent
|
||||||
|
: mpCurrent // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
)
|
||||||
|
as $Val,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$CombatStatsImplCopyWith<$Res>
|
||||||
|
implements $CombatStatsCopyWith<$Res> {
|
||||||
|
factory _$$CombatStatsImplCopyWith(
|
||||||
|
_$CombatStatsImpl value,
|
||||||
|
$Res Function(_$CombatStatsImpl) then,
|
||||||
|
) = __$$CombatStatsImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
int str,
|
||||||
|
int con,
|
||||||
|
int dex,
|
||||||
|
int intelligence,
|
||||||
|
int wis,
|
||||||
|
int cha,
|
||||||
|
int atk,
|
||||||
|
int def,
|
||||||
|
int magAtk,
|
||||||
|
int magDef,
|
||||||
|
double criRate,
|
||||||
|
double criDamage,
|
||||||
|
double evasion,
|
||||||
|
double accuracy,
|
||||||
|
double blockRate,
|
||||||
|
double parryRate,
|
||||||
|
int attackDelayMs,
|
||||||
|
int hpMax,
|
||||||
|
int hpCurrent,
|
||||||
|
int mpMax,
|
||||||
|
int mpCurrent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$CombatStatsImplCopyWithImpl<$Res>
|
||||||
|
extends _$CombatStatsCopyWithImpl<$Res, _$CombatStatsImpl>
|
||||||
|
implements _$$CombatStatsImplCopyWith<$Res> {
|
||||||
|
__$$CombatStatsImplCopyWithImpl(
|
||||||
|
_$CombatStatsImpl _value,
|
||||||
|
$Res Function(_$CombatStatsImpl) _then,
|
||||||
|
) : super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of CombatStats
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? str = null,
|
||||||
|
Object? con = null,
|
||||||
|
Object? dex = null,
|
||||||
|
Object? intelligence = null,
|
||||||
|
Object? wis = null,
|
||||||
|
Object? cha = null,
|
||||||
|
Object? atk = null,
|
||||||
|
Object? def = null,
|
||||||
|
Object? magAtk = null,
|
||||||
|
Object? magDef = null,
|
||||||
|
Object? criRate = null,
|
||||||
|
Object? criDamage = null,
|
||||||
|
Object? evasion = null,
|
||||||
|
Object? accuracy = null,
|
||||||
|
Object? blockRate = null,
|
||||||
|
Object? parryRate = null,
|
||||||
|
Object? attackDelayMs = null,
|
||||||
|
Object? hpMax = null,
|
||||||
|
Object? hpCurrent = null,
|
||||||
|
Object? mpMax = null,
|
||||||
|
Object? mpCurrent = null,
|
||||||
|
}) {
|
||||||
|
return _then(
|
||||||
|
_$CombatStatsImpl(
|
||||||
|
str: null == str
|
||||||
|
? _value.str
|
||||||
|
: str // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
con: null == con
|
||||||
|
? _value.con
|
||||||
|
: con // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
dex: null == dex
|
||||||
|
? _value.dex
|
||||||
|
: dex // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
intelligence: null == intelligence
|
||||||
|
? _value.intelligence
|
||||||
|
: intelligence // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
wis: null == wis
|
||||||
|
? _value.wis
|
||||||
|
: wis // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
cha: null == cha
|
||||||
|
? _value.cha
|
||||||
|
: cha // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
atk: null == atk
|
||||||
|
? _value.atk
|
||||||
|
: atk // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
def: null == def
|
||||||
|
? _value.def
|
||||||
|
: def // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
magAtk: null == magAtk
|
||||||
|
? _value.magAtk
|
||||||
|
: magAtk // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
magDef: null == magDef
|
||||||
|
? _value.magDef
|
||||||
|
: magDef // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
criRate: null == criRate
|
||||||
|
? _value.criRate
|
||||||
|
: criRate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
criDamage: null == criDamage
|
||||||
|
? _value.criDamage
|
||||||
|
: criDamage // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
evasion: null == evasion
|
||||||
|
? _value.evasion
|
||||||
|
: evasion // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
accuracy: null == accuracy
|
||||||
|
? _value.accuracy
|
||||||
|
: accuracy // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
blockRate: null == blockRate
|
||||||
|
? _value.blockRate
|
||||||
|
: blockRate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
parryRate: null == parryRate
|
||||||
|
? _value.parryRate
|
||||||
|
: parryRate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
attackDelayMs: null == attackDelayMs
|
||||||
|
? _value.attackDelayMs
|
||||||
|
: attackDelayMs // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
hpMax: null == hpMax
|
||||||
|
? _value.hpMax
|
||||||
|
: hpMax // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
hpCurrent: null == hpCurrent
|
||||||
|
? _value.hpCurrent
|
||||||
|
: hpCurrent // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
mpMax: null == mpMax
|
||||||
|
? _value.mpMax
|
||||||
|
: mpMax // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
mpCurrent: null == mpCurrent
|
||||||
|
? _value.mpCurrent
|
||||||
|
: mpCurrent // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$CombatStatsImpl extends _CombatStats {
|
||||||
|
const _$CombatStatsImpl({
|
||||||
|
required this.str,
|
||||||
|
required this.con,
|
||||||
|
required this.dex,
|
||||||
|
required this.intelligence,
|
||||||
|
required this.wis,
|
||||||
|
required this.cha,
|
||||||
|
required this.atk,
|
||||||
|
required this.def,
|
||||||
|
required this.magAtk,
|
||||||
|
required this.magDef,
|
||||||
|
required this.criRate,
|
||||||
|
required this.criDamage,
|
||||||
|
required this.evasion,
|
||||||
|
required this.accuracy,
|
||||||
|
required this.blockRate,
|
||||||
|
required this.parryRate,
|
||||||
|
required this.attackDelayMs,
|
||||||
|
required this.hpMax,
|
||||||
|
required this.hpCurrent,
|
||||||
|
required this.mpMax,
|
||||||
|
required this.mpCurrent,
|
||||||
|
}) : super._();
|
||||||
|
|
||||||
|
factory _$CombatStatsImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$CombatStatsImplFromJson(json);
|
||||||
|
|
||||||
|
// 기본 스탯
|
||||||
|
/// 힘: 물리 공격력 보정
|
||||||
|
@override
|
||||||
|
final int str;
|
||||||
|
|
||||||
|
/// 체력: HP, 방어력 보정
|
||||||
|
@override
|
||||||
|
final int con;
|
||||||
|
|
||||||
|
/// 민첩: 회피율, 크리티컬율, 명중률, 공격 속도
|
||||||
|
@override
|
||||||
|
final int dex;
|
||||||
|
|
||||||
|
/// 지능: 마법 공격력, MP
|
||||||
|
@override
|
||||||
|
final int intelligence;
|
||||||
|
|
||||||
|
/// 지혜: 마법 방어력, MP 회복
|
||||||
|
@override
|
||||||
|
final int wis;
|
||||||
|
|
||||||
|
/// 매력: 상점 가격, 드롭률 보정
|
||||||
|
@override
|
||||||
|
final int cha;
|
||||||
|
// 파생 스탯 (전투용)
|
||||||
|
/// 물리 공격력
|
||||||
|
@override
|
||||||
|
final int atk;
|
||||||
|
|
||||||
|
/// 물리 방어력
|
||||||
|
@override
|
||||||
|
final int def;
|
||||||
|
|
||||||
|
/// 마법 공격력
|
||||||
|
@override
|
||||||
|
final int magAtk;
|
||||||
|
|
||||||
|
/// 마법 방어력
|
||||||
|
@override
|
||||||
|
final int magDef;
|
||||||
|
|
||||||
|
/// 크리티컬 확률 (0.0 ~ 1.0)
|
||||||
|
@override
|
||||||
|
final double criRate;
|
||||||
|
|
||||||
|
/// 크리티컬 데미지 배율 (1.5 ~ 3.0)
|
||||||
|
@override
|
||||||
|
final double criDamage;
|
||||||
|
|
||||||
|
/// 회피율 (0.0 ~ 0.5)
|
||||||
|
@override
|
||||||
|
final double evasion;
|
||||||
|
|
||||||
|
/// 명중률 (0.8 ~ 1.0)
|
||||||
|
@override
|
||||||
|
final double accuracy;
|
||||||
|
|
||||||
|
/// 방패 방어율 (0.0 ~ 0.4)
|
||||||
|
@override
|
||||||
|
final double blockRate;
|
||||||
|
|
||||||
|
/// 무기로 쳐내기 확률 (0.0 ~ 0.3)
|
||||||
|
@override
|
||||||
|
final double parryRate;
|
||||||
|
|
||||||
|
/// 공격 딜레이 (밀리초)
|
||||||
|
@override
|
||||||
|
final int attackDelayMs;
|
||||||
|
// 자원
|
||||||
|
/// 최대 HP
|
||||||
|
@override
|
||||||
|
final int hpMax;
|
||||||
|
|
||||||
|
/// 현재 HP
|
||||||
|
@override
|
||||||
|
final int hpCurrent;
|
||||||
|
|
||||||
|
/// 최대 MP
|
||||||
|
@override
|
||||||
|
final int mpMax;
|
||||||
|
|
||||||
|
/// 현재 MP
|
||||||
|
@override
|
||||||
|
final int mpCurrent;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'CombatStats(str: $str, con: $con, dex: $dex, intelligence: $intelligence, wis: $wis, cha: $cha, atk: $atk, def: $def, magAtk: $magAtk, magDef: $magDef, criRate: $criRate, criDamage: $criDamage, evasion: $evasion, accuracy: $accuracy, blockRate: $blockRate, parryRate: $parryRate, attackDelayMs: $attackDelayMs, hpMax: $hpMax, hpCurrent: $hpCurrent, mpMax: $mpMax, mpCurrent: $mpCurrent)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$CombatStatsImpl &&
|
||||||
|
(identical(other.str, str) || other.str == str) &&
|
||||||
|
(identical(other.con, con) || other.con == con) &&
|
||||||
|
(identical(other.dex, dex) || other.dex == dex) &&
|
||||||
|
(identical(other.intelligence, intelligence) ||
|
||||||
|
other.intelligence == intelligence) &&
|
||||||
|
(identical(other.wis, wis) || other.wis == wis) &&
|
||||||
|
(identical(other.cha, cha) || other.cha == cha) &&
|
||||||
|
(identical(other.atk, atk) || other.atk == atk) &&
|
||||||
|
(identical(other.def, def) || other.def == def) &&
|
||||||
|
(identical(other.magAtk, magAtk) || other.magAtk == magAtk) &&
|
||||||
|
(identical(other.magDef, magDef) || other.magDef == magDef) &&
|
||||||
|
(identical(other.criRate, criRate) || other.criRate == criRate) &&
|
||||||
|
(identical(other.criDamage, criDamage) ||
|
||||||
|
other.criDamage == criDamage) &&
|
||||||
|
(identical(other.evasion, evasion) || other.evasion == evasion) &&
|
||||||
|
(identical(other.accuracy, accuracy) ||
|
||||||
|
other.accuracy == accuracy) &&
|
||||||
|
(identical(other.blockRate, blockRate) ||
|
||||||
|
other.blockRate == blockRate) &&
|
||||||
|
(identical(other.parryRate, parryRate) ||
|
||||||
|
other.parryRate == parryRate) &&
|
||||||
|
(identical(other.attackDelayMs, attackDelayMs) ||
|
||||||
|
other.attackDelayMs == attackDelayMs) &&
|
||||||
|
(identical(other.hpMax, hpMax) || other.hpMax == hpMax) &&
|
||||||
|
(identical(other.hpCurrent, hpCurrent) ||
|
||||||
|
other.hpCurrent == hpCurrent) &&
|
||||||
|
(identical(other.mpMax, mpMax) || other.mpMax == mpMax) &&
|
||||||
|
(identical(other.mpCurrent, mpCurrent) ||
|
||||||
|
other.mpCurrent == mpCurrent));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hashAll([
|
||||||
|
runtimeType,
|
||||||
|
str,
|
||||||
|
con,
|
||||||
|
dex,
|
||||||
|
intelligence,
|
||||||
|
wis,
|
||||||
|
cha,
|
||||||
|
atk,
|
||||||
|
def,
|
||||||
|
magAtk,
|
||||||
|
magDef,
|
||||||
|
criRate,
|
||||||
|
criDamage,
|
||||||
|
evasion,
|
||||||
|
accuracy,
|
||||||
|
blockRate,
|
||||||
|
parryRate,
|
||||||
|
attackDelayMs,
|
||||||
|
hpMax,
|
||||||
|
hpCurrent,
|
||||||
|
mpMax,
|
||||||
|
mpCurrent,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/// Create a copy of CombatStats
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$CombatStatsImplCopyWith<_$CombatStatsImpl> get copyWith =>
|
||||||
|
__$$CombatStatsImplCopyWithImpl<_$CombatStatsImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$CombatStatsImplToJson(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _CombatStats extends CombatStats {
|
||||||
|
const factory _CombatStats({
|
||||||
|
required final int str,
|
||||||
|
required final int con,
|
||||||
|
required final int dex,
|
||||||
|
required final int intelligence,
|
||||||
|
required final int wis,
|
||||||
|
required final int cha,
|
||||||
|
required final int atk,
|
||||||
|
required final int def,
|
||||||
|
required final int magAtk,
|
||||||
|
required final int magDef,
|
||||||
|
required final double criRate,
|
||||||
|
required final double criDamage,
|
||||||
|
required final double evasion,
|
||||||
|
required final double accuracy,
|
||||||
|
required final double blockRate,
|
||||||
|
required final double parryRate,
|
||||||
|
required final int attackDelayMs,
|
||||||
|
required final int hpMax,
|
||||||
|
required final int hpCurrent,
|
||||||
|
required final int mpMax,
|
||||||
|
required final int mpCurrent,
|
||||||
|
}) = _$CombatStatsImpl;
|
||||||
|
const _CombatStats._() : super._();
|
||||||
|
|
||||||
|
factory _CombatStats.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$CombatStatsImpl.fromJson;
|
||||||
|
|
||||||
|
// 기본 스탯
|
||||||
|
/// 힘: 물리 공격력 보정
|
||||||
|
@override
|
||||||
|
int get str;
|
||||||
|
|
||||||
|
/// 체력: HP, 방어력 보정
|
||||||
|
@override
|
||||||
|
int get con;
|
||||||
|
|
||||||
|
/// 민첩: 회피율, 크리티컬율, 명중률, 공격 속도
|
||||||
|
@override
|
||||||
|
int get dex;
|
||||||
|
|
||||||
|
/// 지능: 마법 공격력, MP
|
||||||
|
@override
|
||||||
|
int get intelligence;
|
||||||
|
|
||||||
|
/// 지혜: 마법 방어력, MP 회복
|
||||||
|
@override
|
||||||
|
int get wis;
|
||||||
|
|
||||||
|
/// 매력: 상점 가격, 드롭률 보정
|
||||||
|
@override
|
||||||
|
int get cha; // 파생 스탯 (전투용)
|
||||||
|
/// 물리 공격력
|
||||||
|
@override
|
||||||
|
int get atk;
|
||||||
|
|
||||||
|
/// 물리 방어력
|
||||||
|
@override
|
||||||
|
int get def;
|
||||||
|
|
||||||
|
/// 마법 공격력
|
||||||
|
@override
|
||||||
|
int get magAtk;
|
||||||
|
|
||||||
|
/// 마법 방어력
|
||||||
|
@override
|
||||||
|
int get magDef;
|
||||||
|
|
||||||
|
/// 크리티컬 확률 (0.0 ~ 1.0)
|
||||||
|
@override
|
||||||
|
double get criRate;
|
||||||
|
|
||||||
|
/// 크리티컬 데미지 배율 (1.5 ~ 3.0)
|
||||||
|
@override
|
||||||
|
double get criDamage;
|
||||||
|
|
||||||
|
/// 회피율 (0.0 ~ 0.5)
|
||||||
|
@override
|
||||||
|
double get evasion;
|
||||||
|
|
||||||
|
/// 명중률 (0.8 ~ 1.0)
|
||||||
|
@override
|
||||||
|
double get accuracy;
|
||||||
|
|
||||||
|
/// 방패 방어율 (0.0 ~ 0.4)
|
||||||
|
@override
|
||||||
|
double get blockRate;
|
||||||
|
|
||||||
|
/// 무기로 쳐내기 확률 (0.0 ~ 0.3)
|
||||||
|
@override
|
||||||
|
double get parryRate;
|
||||||
|
|
||||||
|
/// 공격 딜레이 (밀리초)
|
||||||
|
@override
|
||||||
|
int get attackDelayMs; // 자원
|
||||||
|
/// 최대 HP
|
||||||
|
@override
|
||||||
|
int get hpMax;
|
||||||
|
|
||||||
|
/// 현재 HP
|
||||||
|
@override
|
||||||
|
int get hpCurrent;
|
||||||
|
|
||||||
|
/// 최대 MP
|
||||||
|
@override
|
||||||
|
int get mpMax;
|
||||||
|
|
||||||
|
/// 현재 MP
|
||||||
|
@override
|
||||||
|
int get mpCurrent;
|
||||||
|
|
||||||
|
/// Create a copy of CombatStats
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$CombatStatsImplCopyWith<_$CombatStatsImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
57
lib/src/core/model/combat_stats.g.dart
Normal file
57
lib/src/core/model/combat_stats.g.dart
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'combat_stats.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$CombatStatsImpl _$$CombatStatsImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$CombatStatsImpl(
|
||||||
|
str: (json['str'] as num).toInt(),
|
||||||
|
con: (json['con'] as num).toInt(),
|
||||||
|
dex: (json['dex'] as num).toInt(),
|
||||||
|
intelligence: (json['intelligence'] as num).toInt(),
|
||||||
|
wis: (json['wis'] as num).toInt(),
|
||||||
|
cha: (json['cha'] as num).toInt(),
|
||||||
|
atk: (json['atk'] as num).toInt(),
|
||||||
|
def: (json['def'] as num).toInt(),
|
||||||
|
magAtk: (json['magAtk'] as num).toInt(),
|
||||||
|
magDef: (json['magDef'] as num).toInt(),
|
||||||
|
criRate: (json['criRate'] as num).toDouble(),
|
||||||
|
criDamage: (json['criDamage'] as num).toDouble(),
|
||||||
|
evasion: (json['evasion'] as num).toDouble(),
|
||||||
|
accuracy: (json['accuracy'] as num).toDouble(),
|
||||||
|
blockRate: (json['blockRate'] as num).toDouble(),
|
||||||
|
parryRate: (json['parryRate'] as num).toDouble(),
|
||||||
|
attackDelayMs: (json['attackDelayMs'] as num).toInt(),
|
||||||
|
hpMax: (json['hpMax'] as num).toInt(),
|
||||||
|
hpCurrent: (json['hpCurrent'] as num).toInt(),
|
||||||
|
mpMax: (json['mpMax'] as num).toInt(),
|
||||||
|
mpCurrent: (json['mpCurrent'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$CombatStatsImplToJson(_$CombatStatsImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'str': instance.str,
|
||||||
|
'con': instance.con,
|
||||||
|
'dex': instance.dex,
|
||||||
|
'intelligence': instance.intelligence,
|
||||||
|
'wis': instance.wis,
|
||||||
|
'cha': instance.cha,
|
||||||
|
'atk': instance.atk,
|
||||||
|
'def': instance.def,
|
||||||
|
'magAtk': instance.magAtk,
|
||||||
|
'magDef': instance.magDef,
|
||||||
|
'criRate': instance.criRate,
|
||||||
|
'criDamage': instance.criDamage,
|
||||||
|
'evasion': instance.evasion,
|
||||||
|
'accuracy': instance.accuracy,
|
||||||
|
'blockRate': instance.blockRate,
|
||||||
|
'parryRate': instance.parryRate,
|
||||||
|
'attackDelayMs': instance.attackDelayMs,
|
||||||
|
'hpMax': instance.hpMax,
|
||||||
|
'hpCurrent': instance.hpCurrent,
|
||||||
|
'mpMax': instance.mpMax,
|
||||||
|
'mpCurrent': instance.mpCurrent,
|
||||||
|
};
|
||||||
@@ -1,51 +1,38 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
||||||
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
||||||
|
|
||||||
|
part 'equipment_item.freezed.dart';
|
||||||
|
part 'equipment_item.g.dart';
|
||||||
|
|
||||||
/// 장비 아이템
|
/// 장비 아이템
|
||||||
///
|
///
|
||||||
/// 개별 장비의 이름, 슬롯, 레벨, 스탯 등을 포함하는 클래스.
|
/// 개별 장비의 이름, 슬롯, 레벨, 스탯 등을 포함하는 클래스.
|
||||||
/// 불변(immutable) 객체로 설계됨.
|
/// 불변(immutable) 객체로 설계됨.
|
||||||
class EquipmentItem {
|
@freezed
|
||||||
const EquipmentItem({
|
class EquipmentItem with _$EquipmentItem {
|
||||||
required this.name,
|
const EquipmentItem._();
|
||||||
required this.slot,
|
|
||||||
required this.level,
|
|
||||||
required this.weight,
|
|
||||||
required this.stats,
|
|
||||||
required this.rarity,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const factory EquipmentItem({
|
||||||
/// 아이템 이름 (예: "Flaming Sword of Doom")
|
/// 아이템 이름 (예: "Flaming Sword of Doom")
|
||||||
final String name;
|
required String name,
|
||||||
|
|
||||||
/// 장착 슬롯
|
/// 장착 슬롯
|
||||||
final EquipmentSlot slot;
|
@JsonKey(fromJson: _slotFromJson, toJson: _slotToJson)
|
||||||
|
required EquipmentSlot slot,
|
||||||
/// 아이템 레벨
|
/// 아이템 레벨
|
||||||
final int level;
|
required int level,
|
||||||
|
|
||||||
/// 무게 (STR 기반 휴대 제한용)
|
/// 무게 (STR 기반 휴대 제한용)
|
||||||
final int weight;
|
required int weight,
|
||||||
|
|
||||||
/// 아이템 스탯 보정치
|
/// 아이템 스탯 보정치
|
||||||
final ItemStats stats;
|
required ItemStats stats,
|
||||||
|
|
||||||
/// 희귀도
|
/// 희귀도
|
||||||
final ItemRarity rarity;
|
@JsonKey(fromJson: _rarityFromJson, toJson: _rarityToJson)
|
||||||
|
required ItemRarity rarity,
|
||||||
|
}) = _EquipmentItem;
|
||||||
|
|
||||||
/// 가중치 (자동 장착 비교용)
|
factory EquipmentItem.fromJson(Map<String, dynamic> json) =>
|
||||||
///
|
_$EquipmentItemFromJson(json);
|
||||||
/// 가중치 = 기본값 + (레벨 * 10) + (희귀도 보너스) + (스탯 합계)
|
|
||||||
int get itemWeight {
|
|
||||||
const baseValue = 10;
|
|
||||||
return baseValue + (level * 10) + rarity.weightBonus + stats.totalStatValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 빈 아이템 여부
|
|
||||||
bool get isEmpty => name.isEmpty;
|
|
||||||
|
|
||||||
/// 유효한 아이템 여부
|
|
||||||
bool get isNotEmpty => name.isNotEmpty;
|
|
||||||
|
|
||||||
/// 빈 아이템 생성 (특정 슬롯)
|
/// 빈 아이템 생성 (특정 슬롯)
|
||||||
factory EquipmentItem.empty(EquipmentSlot slot) {
|
factory EquipmentItem.empty(EquipmentSlot slot) {
|
||||||
@@ -54,7 +41,7 @@ class EquipmentItem {
|
|||||||
slot: slot,
|
slot: slot,
|
||||||
level: 0,
|
level: 0,
|
||||||
weight: 0,
|
weight: 0,
|
||||||
stats: ItemStats.empty,
|
stats: const ItemStats(),
|
||||||
rarity: ItemRarity.common,
|
rarity: ItemRarity.common,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -71,59 +58,39 @@ class EquipmentItem {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
EquipmentItem copyWith({
|
/// 가중치 (자동 장착 비교용)
|
||||||
String? name,
|
///
|
||||||
EquipmentSlot? slot,
|
/// 가중치 = 기본값 + (레벨 * 10) + (희귀도 보너스) + (스탯 합계)
|
||||||
int? level,
|
int get itemWeight {
|
||||||
int? weight,
|
const baseValue = 10;
|
||||||
ItemStats? stats,
|
return baseValue + (level * 10) + rarity.weightBonus + stats.totalStatValue;
|
||||||
ItemRarity? rarity,
|
|
||||||
}) {
|
|
||||||
return EquipmentItem(
|
|
||||||
name: name ?? this.name,
|
|
||||||
slot: slot ?? this.slot,
|
|
||||||
level: level ?? this.level,
|
|
||||||
weight: weight ?? this.weight,
|
|
||||||
stats: stats ?? this.stats,
|
|
||||||
rarity: rarity ?? this.rarity,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JSON으로 직렬화
|
/// 빈 아이템 여부
|
||||||
Map<String, dynamic> toJson() {
|
bool get isEmpty => name.isEmpty;
|
||||||
return {
|
|
||||||
'name': name,
|
|
||||||
'slot': slot.name,
|
|
||||||
'level': level,
|
|
||||||
'weight': weight,
|
|
||||||
'stats': stats.toJson(),
|
|
||||||
'rarity': rarity.name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// JSON에서 역직렬화
|
/// 유효한 아이템 여부
|
||||||
factory EquipmentItem.fromJson(Map<String, dynamic> json) {
|
bool get isNotEmpty => name.isNotEmpty;
|
||||||
final slotName = json['slot'] as String? ?? 'weapon';
|
|
||||||
final rarityName = json['rarity'] as String? ?? 'common';
|
|
||||||
|
|
||||||
return EquipmentItem(
|
|
||||||
name: json['name'] as String? ?? '',
|
|
||||||
slot: EquipmentSlot.values.firstWhere(
|
|
||||||
(s) => s.name == slotName,
|
|
||||||
orElse: () => EquipmentSlot.weapon,
|
|
||||||
),
|
|
||||||
level: json['level'] as int? ?? 0,
|
|
||||||
weight: json['weight'] as int? ?? 0,
|
|
||||||
stats: json['stats'] != null
|
|
||||||
? ItemStats.fromJson(json['stats'] as Map<String, dynamic>)
|
|
||||||
: ItemStats.empty,
|
|
||||||
rarity: ItemRarity.values.firstWhere(
|
|
||||||
(r) => r.name == rarityName,
|
|
||||||
orElse: () => ItemRarity.common,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => name.isEmpty ? '(empty)' : name;
|
String toString() => name.isEmpty ? '(empty)' : name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSON 변환 헬퍼 (세이브 파일 호환성 유지)
|
||||||
|
EquipmentSlot _slotFromJson(String? value) {
|
||||||
|
return EquipmentSlot.values.firstWhere(
|
||||||
|
(s) => s.name == value,
|
||||||
|
orElse: () => EquipmentSlot.weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _slotToJson(EquipmentSlot slot) => slot.name;
|
||||||
|
|
||||||
|
ItemRarity _rarityFromJson(String? value) {
|
||||||
|
return ItemRarity.values.firstWhere(
|
||||||
|
(r) => r.name == value,
|
||||||
|
orElse: () => ItemRarity.common,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _rarityToJson(ItemRarity rarity) => rarity.name;
|
||||||
|
|||||||
335
lib/src/core/model/equipment_item.freezed.dart
Normal file
335
lib/src/core/model/equipment_item.freezed.dart
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'equipment_item.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
|
||||||
|
);
|
||||||
|
|
||||||
|
EquipmentItem _$EquipmentItemFromJson(Map<String, dynamic> json) {
|
||||||
|
return _EquipmentItem.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$EquipmentItem {
|
||||||
|
/// 아이템 이름 (예: "Flaming Sword of Doom")
|
||||||
|
String get name => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 장착 슬롯
|
||||||
|
@JsonKey(fromJson: _slotFromJson, toJson: _slotToJson)
|
||||||
|
EquipmentSlot get slot => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 아이템 레벨
|
||||||
|
int get level => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 무게 (STR 기반 휴대 제한용)
|
||||||
|
int get weight => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 아이템 스탯 보정치
|
||||||
|
ItemStats get stats => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 희귀도
|
||||||
|
@JsonKey(fromJson: _rarityFromJson, toJson: _rarityToJson)
|
||||||
|
ItemRarity get rarity => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this EquipmentItem to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of EquipmentItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$EquipmentItemCopyWith<EquipmentItem> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $EquipmentItemCopyWith<$Res> {
|
||||||
|
factory $EquipmentItemCopyWith(
|
||||||
|
EquipmentItem value,
|
||||||
|
$Res Function(EquipmentItem) then,
|
||||||
|
) = _$EquipmentItemCopyWithImpl<$Res, EquipmentItem>;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String name,
|
||||||
|
@JsonKey(fromJson: _slotFromJson, toJson: _slotToJson) EquipmentSlot slot,
|
||||||
|
int level,
|
||||||
|
int weight,
|
||||||
|
ItemStats stats,
|
||||||
|
@JsonKey(fromJson: _rarityFromJson, toJson: _rarityToJson)
|
||||||
|
ItemRarity rarity,
|
||||||
|
});
|
||||||
|
|
||||||
|
$ItemStatsCopyWith<$Res> get stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$EquipmentItemCopyWithImpl<$Res, $Val extends EquipmentItem>
|
||||||
|
implements $EquipmentItemCopyWith<$Res> {
|
||||||
|
_$EquipmentItemCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of EquipmentItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? name = null,
|
||||||
|
Object? slot = null,
|
||||||
|
Object? level = null,
|
||||||
|
Object? weight = null,
|
||||||
|
Object? stats = null,
|
||||||
|
Object? rarity = null,
|
||||||
|
}) {
|
||||||
|
return _then(
|
||||||
|
_value.copyWith(
|
||||||
|
name: null == name
|
||||||
|
? _value.name
|
||||||
|
: name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
slot: null == slot
|
||||||
|
? _value.slot
|
||||||
|
: slot // ignore: cast_nullable_to_non_nullable
|
||||||
|
as EquipmentSlot,
|
||||||
|
level: null == level
|
||||||
|
? _value.level
|
||||||
|
: level // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
weight: null == weight
|
||||||
|
? _value.weight
|
||||||
|
: weight // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
stats: null == stats
|
||||||
|
? _value.stats
|
||||||
|
: stats // ignore: cast_nullable_to_non_nullable
|
||||||
|
as ItemStats,
|
||||||
|
rarity: null == rarity
|
||||||
|
? _value.rarity
|
||||||
|
: rarity // ignore: cast_nullable_to_non_nullable
|
||||||
|
as ItemRarity,
|
||||||
|
)
|
||||||
|
as $Val,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of EquipmentItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$ItemStatsCopyWith<$Res> get stats {
|
||||||
|
return $ItemStatsCopyWith<$Res>(_value.stats, (value) {
|
||||||
|
return _then(_value.copyWith(stats: value) as $Val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$EquipmentItemImplCopyWith<$Res>
|
||||||
|
implements $EquipmentItemCopyWith<$Res> {
|
||||||
|
factory _$$EquipmentItemImplCopyWith(
|
||||||
|
_$EquipmentItemImpl value,
|
||||||
|
$Res Function(_$EquipmentItemImpl) then,
|
||||||
|
) = __$$EquipmentItemImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String name,
|
||||||
|
@JsonKey(fromJson: _slotFromJson, toJson: _slotToJson) EquipmentSlot slot,
|
||||||
|
int level,
|
||||||
|
int weight,
|
||||||
|
ItemStats stats,
|
||||||
|
@JsonKey(fromJson: _rarityFromJson, toJson: _rarityToJson)
|
||||||
|
ItemRarity rarity,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
$ItemStatsCopyWith<$Res> get stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$EquipmentItemImplCopyWithImpl<$Res>
|
||||||
|
extends _$EquipmentItemCopyWithImpl<$Res, _$EquipmentItemImpl>
|
||||||
|
implements _$$EquipmentItemImplCopyWith<$Res> {
|
||||||
|
__$$EquipmentItemImplCopyWithImpl(
|
||||||
|
_$EquipmentItemImpl _value,
|
||||||
|
$Res Function(_$EquipmentItemImpl) _then,
|
||||||
|
) : super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of EquipmentItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? name = null,
|
||||||
|
Object? slot = null,
|
||||||
|
Object? level = null,
|
||||||
|
Object? weight = null,
|
||||||
|
Object? stats = null,
|
||||||
|
Object? rarity = null,
|
||||||
|
}) {
|
||||||
|
return _then(
|
||||||
|
_$EquipmentItemImpl(
|
||||||
|
name: null == name
|
||||||
|
? _value.name
|
||||||
|
: name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
slot: null == slot
|
||||||
|
? _value.slot
|
||||||
|
: slot // ignore: cast_nullable_to_non_nullable
|
||||||
|
as EquipmentSlot,
|
||||||
|
level: null == level
|
||||||
|
? _value.level
|
||||||
|
: level // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
weight: null == weight
|
||||||
|
? _value.weight
|
||||||
|
: weight // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
stats: null == stats
|
||||||
|
? _value.stats
|
||||||
|
: stats // ignore: cast_nullable_to_non_nullable
|
||||||
|
as ItemStats,
|
||||||
|
rarity: null == rarity
|
||||||
|
? _value.rarity
|
||||||
|
: rarity // ignore: cast_nullable_to_non_nullable
|
||||||
|
as ItemRarity,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$EquipmentItemImpl extends _EquipmentItem {
|
||||||
|
const _$EquipmentItemImpl({
|
||||||
|
required this.name,
|
||||||
|
@JsonKey(fromJson: _slotFromJson, toJson: _slotToJson) required this.slot,
|
||||||
|
required this.level,
|
||||||
|
required this.weight,
|
||||||
|
required this.stats,
|
||||||
|
@JsonKey(fromJson: _rarityFromJson, toJson: _rarityToJson)
|
||||||
|
required this.rarity,
|
||||||
|
}) : super._();
|
||||||
|
|
||||||
|
factory _$EquipmentItemImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$EquipmentItemImplFromJson(json);
|
||||||
|
|
||||||
|
/// 아이템 이름 (예: "Flaming Sword of Doom")
|
||||||
|
@override
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// 장착 슬롯
|
||||||
|
@override
|
||||||
|
@JsonKey(fromJson: _slotFromJson, toJson: _slotToJson)
|
||||||
|
final EquipmentSlot slot;
|
||||||
|
|
||||||
|
/// 아이템 레벨
|
||||||
|
@override
|
||||||
|
final int level;
|
||||||
|
|
||||||
|
/// 무게 (STR 기반 휴대 제한용)
|
||||||
|
@override
|
||||||
|
final int weight;
|
||||||
|
|
||||||
|
/// 아이템 스탯 보정치
|
||||||
|
@override
|
||||||
|
final ItemStats stats;
|
||||||
|
|
||||||
|
/// 희귀도
|
||||||
|
@override
|
||||||
|
@JsonKey(fromJson: _rarityFromJson, toJson: _rarityToJson)
|
||||||
|
final ItemRarity rarity;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$EquipmentItemImpl &&
|
||||||
|
(identical(other.name, name) || other.name == name) &&
|
||||||
|
(identical(other.slot, slot) || other.slot == slot) &&
|
||||||
|
(identical(other.level, level) || other.level == level) &&
|
||||||
|
(identical(other.weight, weight) || other.weight == weight) &&
|
||||||
|
(identical(other.stats, stats) || other.stats == stats) &&
|
||||||
|
(identical(other.rarity, rarity) || other.rarity == rarity));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
Object.hash(runtimeType, name, slot, level, weight, stats, rarity);
|
||||||
|
|
||||||
|
/// Create a copy of EquipmentItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$EquipmentItemImplCopyWith<_$EquipmentItemImpl> get copyWith =>
|
||||||
|
__$$EquipmentItemImplCopyWithImpl<_$EquipmentItemImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$EquipmentItemImplToJson(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _EquipmentItem extends EquipmentItem {
|
||||||
|
const factory _EquipmentItem({
|
||||||
|
required final String name,
|
||||||
|
@JsonKey(fromJson: _slotFromJson, toJson: _slotToJson)
|
||||||
|
required final EquipmentSlot slot,
|
||||||
|
required final int level,
|
||||||
|
required final int weight,
|
||||||
|
required final ItemStats stats,
|
||||||
|
@JsonKey(fromJson: _rarityFromJson, toJson: _rarityToJson)
|
||||||
|
required final ItemRarity rarity,
|
||||||
|
}) = _$EquipmentItemImpl;
|
||||||
|
const _EquipmentItem._() : super._();
|
||||||
|
|
||||||
|
factory _EquipmentItem.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$EquipmentItemImpl.fromJson;
|
||||||
|
|
||||||
|
/// 아이템 이름 (예: "Flaming Sword of Doom")
|
||||||
|
@override
|
||||||
|
String get name;
|
||||||
|
|
||||||
|
/// 장착 슬롯
|
||||||
|
@override
|
||||||
|
@JsonKey(fromJson: _slotFromJson, toJson: _slotToJson)
|
||||||
|
EquipmentSlot get slot;
|
||||||
|
|
||||||
|
/// 아이템 레벨
|
||||||
|
@override
|
||||||
|
int get level;
|
||||||
|
|
||||||
|
/// 무게 (STR 기반 휴대 제한용)
|
||||||
|
@override
|
||||||
|
int get weight;
|
||||||
|
|
||||||
|
/// 아이템 스탯 보정치
|
||||||
|
@override
|
||||||
|
ItemStats get stats;
|
||||||
|
|
||||||
|
/// 희귀도
|
||||||
|
@override
|
||||||
|
@JsonKey(fromJson: _rarityFromJson, toJson: _rarityToJson)
|
||||||
|
ItemRarity get rarity;
|
||||||
|
|
||||||
|
/// Create a copy of EquipmentItem
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$EquipmentItemImplCopyWith<_$EquipmentItemImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
27
lib/src/core/model/equipment_item.g.dart
Normal file
27
lib/src/core/model/equipment_item.g.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'equipment_item.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$EquipmentItemImpl _$$EquipmentItemImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$EquipmentItemImpl(
|
||||||
|
name: json['name'] as String,
|
||||||
|
slot: _slotFromJson(json['slot'] as String?),
|
||||||
|
level: (json['level'] as num).toInt(),
|
||||||
|
weight: (json['weight'] as num).toInt(),
|
||||||
|
stats: ItemStats.fromJson(json['stats'] as Map<String, dynamic>),
|
||||||
|
rarity: _rarityFromJson(json['rarity'] as String?),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$EquipmentItemImplToJson(_$EquipmentItemImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'name': instance.name,
|
||||||
|
'slot': _slotToJson(instance.slot),
|
||||||
|
'level': instance.level,
|
||||||
|
'weight': instance.weight,
|
||||||
|
'stats': instance.stats,
|
||||||
|
'rarity': _rarityToJson(instance.rarity),
|
||||||
|
};
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'item_stats.freezed.dart';
|
||||||
|
part 'item_stats.g.dart';
|
||||||
|
|
||||||
/// 아이템 희귀도
|
/// 아이템 희귀도
|
||||||
enum ItemRarity {
|
enum ItemRarity {
|
||||||
common,
|
common,
|
||||||
@@ -29,80 +34,55 @@ enum ItemRarity {
|
|||||||
///
|
///
|
||||||
/// 장비 아이템이 제공하는 스탯 보너스.
|
/// 장비 아이템이 제공하는 스탯 보너스.
|
||||||
/// 모든 값은 기본 0이며, 장착 시 플레이어 스탯에 가산됨.
|
/// 모든 값은 기본 0이며, 장착 시 플레이어 스탯에 가산됨.
|
||||||
class ItemStats {
|
@freezed
|
||||||
const ItemStats({
|
class ItemStats with _$ItemStats {
|
||||||
this.atk = 0,
|
const ItemStats._();
|
||||||
this.def = 0,
|
|
||||||
this.magAtk = 0,
|
|
||||||
this.magDef = 0,
|
|
||||||
this.criRate = 0.0,
|
|
||||||
this.evasion = 0.0,
|
|
||||||
this.blockRate = 0.0,
|
|
||||||
this.parryRate = 0.0,
|
|
||||||
this.hpBonus = 0,
|
|
||||||
this.mpBonus = 0,
|
|
||||||
this.strBonus = 0,
|
|
||||||
this.conBonus = 0,
|
|
||||||
this.dexBonus = 0,
|
|
||||||
this.intBonus = 0,
|
|
||||||
this.wisBonus = 0,
|
|
||||||
this.chaBonus = 0,
|
|
||||||
this.attackSpeed = 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const factory ItemStats({
|
||||||
/// 물리 공격력 보정
|
/// 물리 공격력 보정
|
||||||
final int atk;
|
@Default(0) int atk,
|
||||||
|
|
||||||
/// 물리 방어력 보정
|
/// 물리 방어력 보정
|
||||||
final int def;
|
@Default(0) int def,
|
||||||
|
|
||||||
/// 마법 공격력 보정
|
/// 마법 공격력 보정
|
||||||
final int magAtk;
|
@Default(0) int magAtk,
|
||||||
|
|
||||||
/// 마법 방어력 보정
|
/// 마법 방어력 보정
|
||||||
final int magDef;
|
@Default(0) int magDef,
|
||||||
|
|
||||||
/// 크리티컬 확률 보정 (0.0 ~ 1.0)
|
/// 크리티컬 확률 보정 (0.0 ~ 1.0)
|
||||||
final double criRate;
|
@Default(0.0) double criRate,
|
||||||
|
|
||||||
/// 회피율 보정 (0.0 ~ 1.0)
|
/// 회피율 보정 (0.0 ~ 1.0)
|
||||||
final double evasion;
|
@Default(0.0) double evasion,
|
||||||
|
|
||||||
/// 방패 방어율 (방패 전용, 0.0 ~ 1.0)
|
/// 방패 방어율 (방패 전용, 0.0 ~ 1.0)
|
||||||
final double blockRate;
|
@Default(0.0) double blockRate,
|
||||||
|
|
||||||
/// 무기 쳐내기 확률 (무기 전용, 0.0 ~ 1.0)
|
/// 무기 쳐내기 확률 (무기 전용, 0.0 ~ 1.0)
|
||||||
final double parryRate;
|
@Default(0.0) double parryRate,
|
||||||
|
|
||||||
/// HP 보너스
|
/// HP 보너스
|
||||||
final int hpBonus;
|
@Default(0) int hpBonus,
|
||||||
|
|
||||||
/// MP 보너스
|
/// MP 보너스
|
||||||
final int mpBonus;
|
@Default(0) int mpBonus,
|
||||||
|
|
||||||
/// STR 보너스
|
/// STR 보너스
|
||||||
final int strBonus;
|
@Default(0) int strBonus,
|
||||||
|
|
||||||
/// CON 보너스
|
/// CON 보너스
|
||||||
final int conBonus;
|
@Default(0) int conBonus,
|
||||||
|
|
||||||
/// DEX 보너스
|
/// DEX 보너스
|
||||||
final int dexBonus;
|
@Default(0) int dexBonus,
|
||||||
|
|
||||||
/// INT 보너스
|
/// INT 보너스
|
||||||
final int intBonus;
|
@Default(0) int intBonus,
|
||||||
|
|
||||||
/// WIS 보너스
|
/// WIS 보너스
|
||||||
final int wisBonus;
|
@Default(0) int wisBonus,
|
||||||
|
|
||||||
/// CHA 보너스
|
/// CHA 보너스
|
||||||
final int chaBonus;
|
@Default(0) int chaBonus,
|
||||||
|
|
||||||
/// 무기 공격속도 (밀리초, 무기 전용)
|
/// 무기 공격속도 (밀리초, 무기 전용)
|
||||||
///
|
///
|
||||||
/// 0이면 기본값(1000ms) 사용, 값이 클수록 느린 공격.
|
/// 0이면 기본값(1000ms) 사용, 값이 클수록 느린 공격.
|
||||||
/// 느린 무기는 높은 기본 데미지를 가짐.
|
/// 느린 무기는 높은 기본 데미지를 가짐.
|
||||||
final int attackSpeed;
|
@Default(0) int attackSpeed,
|
||||||
|
}) = _ItemStats;
|
||||||
|
|
||||||
|
factory ItemStats.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ItemStatsFromJson(json);
|
||||||
|
|
||||||
|
/// 빈 스탯 (보너스 없음)
|
||||||
|
static const empty = ItemStats();
|
||||||
|
|
||||||
/// 스탯 합계 (가중치 계산용)
|
/// 스탯 합계 (가중치 계산용)
|
||||||
int get totalStatValue {
|
int get totalStatValue {
|
||||||
@@ -124,55 +104,6 @@ class ItemStats {
|
|||||||
chaBonus * 5;
|
chaBonus * 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 빈 스탯 (보너스 없음)
|
|
||||||
static const empty = ItemStats();
|
|
||||||
|
|
||||||
/// JSON으로 직렬화
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'atk': atk,
|
|
||||||
'def': def,
|
|
||||||
'magAtk': magAtk,
|
|
||||||
'magDef': magDef,
|
|
||||||
'criRate': criRate,
|
|
||||||
'evasion': evasion,
|
|
||||||
'blockRate': blockRate,
|
|
||||||
'parryRate': parryRate,
|
|
||||||
'hpBonus': hpBonus,
|
|
||||||
'mpBonus': mpBonus,
|
|
||||||
'strBonus': strBonus,
|
|
||||||
'conBonus': conBonus,
|
|
||||||
'dexBonus': dexBonus,
|
|
||||||
'intBonus': intBonus,
|
|
||||||
'wisBonus': wisBonus,
|
|
||||||
'chaBonus': chaBonus,
|
|
||||||
'attackSpeed': attackSpeed,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// JSON에서 역직렬화
|
|
||||||
factory ItemStats.fromJson(Map<String, dynamic> json) {
|
|
||||||
return ItemStats(
|
|
||||||
atk: json['atk'] as int? ?? 0,
|
|
||||||
def: json['def'] as int? ?? 0,
|
|
||||||
magAtk: json['magAtk'] as int? ?? 0,
|
|
||||||
magDef: json['magDef'] as int? ?? 0,
|
|
||||||
criRate: (json['criRate'] as num?)?.toDouble() ?? 0.0,
|
|
||||||
evasion: (json['evasion'] as num?)?.toDouble() ?? 0.0,
|
|
||||||
blockRate: (json['blockRate'] as num?)?.toDouble() ?? 0.0,
|
|
||||||
parryRate: (json['parryRate'] as num?)?.toDouble() ?? 0.0,
|
|
||||||
hpBonus: json['hpBonus'] as int? ?? 0,
|
|
||||||
mpBonus: json['mpBonus'] as int? ?? 0,
|
|
||||||
strBonus: json['strBonus'] as int? ?? 0,
|
|
||||||
conBonus: json['conBonus'] as int? ?? 0,
|
|
||||||
dexBonus: json['dexBonus'] as int? ?? 0,
|
|
||||||
intBonus: json['intBonus'] as int? ?? 0,
|
|
||||||
wisBonus: json['wisBonus'] as int? ?? 0,
|
|
||||||
chaBonus: json['chaBonus'] as int? ?? 0,
|
|
||||||
attackSpeed: json['attackSpeed'] as int? ?? 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 두 스탯 합산
|
/// 두 스탯 합산
|
||||||
///
|
///
|
||||||
/// attackSpeed는 합산 대상 아님 (무기 슬롯 단일 값)
|
/// attackSpeed는 합산 대상 아님 (무기 슬롯 단일 값)
|
||||||
@@ -197,44 +128,4 @@ class ItemStats {
|
|||||||
// attackSpeed는 무기에서만 직접 참조
|
// attackSpeed는 무기에서만 직접 참조
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemStats copyWith({
|
|
||||||
int? atk,
|
|
||||||
int? def,
|
|
||||||
int? magAtk,
|
|
||||||
int? magDef,
|
|
||||||
double? criRate,
|
|
||||||
double? evasion,
|
|
||||||
double? blockRate,
|
|
||||||
double? parryRate,
|
|
||||||
int? hpBonus,
|
|
||||||
int? mpBonus,
|
|
||||||
int? strBonus,
|
|
||||||
int? conBonus,
|
|
||||||
int? dexBonus,
|
|
||||||
int? intBonus,
|
|
||||||
int? wisBonus,
|
|
||||||
int? chaBonus,
|
|
||||||
int? attackSpeed,
|
|
||||||
}) {
|
|
||||||
return ItemStats(
|
|
||||||
atk: atk ?? this.atk,
|
|
||||||
def: def ?? this.def,
|
|
||||||
magAtk: magAtk ?? this.magAtk,
|
|
||||||
magDef: magDef ?? this.magDef,
|
|
||||||
criRate: criRate ?? this.criRate,
|
|
||||||
evasion: evasion ?? this.evasion,
|
|
||||||
blockRate: blockRate ?? this.blockRate,
|
|
||||||
parryRate: parryRate ?? this.parryRate,
|
|
||||||
hpBonus: hpBonus ?? this.hpBonus,
|
|
||||||
mpBonus: mpBonus ?? this.mpBonus,
|
|
||||||
strBonus: strBonus ?? this.strBonus,
|
|
||||||
conBonus: conBonus ?? this.conBonus,
|
|
||||||
dexBonus: dexBonus ?? this.dexBonus,
|
|
||||||
intBonus: intBonus ?? this.intBonus,
|
|
||||||
wisBonus: wisBonus ?? this.wisBonus,
|
|
||||||
chaBonus: chaBonus ?? this.chaBonus,
|
|
||||||
attackSpeed: attackSpeed ?? this.attackSpeed,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
651
lib/src/core/model/item_stats.freezed.dart
Normal file
651
lib/src/core/model/item_stats.freezed.dart
Normal file
@@ -0,0 +1,651 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'item_stats.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
|
||||||
|
);
|
||||||
|
|
||||||
|
ItemStats _$ItemStatsFromJson(Map<String, dynamic> json) {
|
||||||
|
return _ItemStats.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$ItemStats {
|
||||||
|
/// 물리 공격력 보정
|
||||||
|
int get atk => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 물리 방어력 보정
|
||||||
|
int get def => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 마법 공격력 보정
|
||||||
|
int get magAtk => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 마법 방어력 보정
|
||||||
|
int get magDef => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 크리티컬 확률 보정 (0.0 ~ 1.0)
|
||||||
|
double get criRate => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 회피율 보정 (0.0 ~ 1.0)
|
||||||
|
double get evasion => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 방패 방어율 (방패 전용, 0.0 ~ 1.0)
|
||||||
|
double get blockRate => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 무기 쳐내기 확률 (무기 전용, 0.0 ~ 1.0)
|
||||||
|
double get parryRate => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// HP 보너스
|
||||||
|
int get hpBonus => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// MP 보너스
|
||||||
|
int get mpBonus => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// STR 보너스
|
||||||
|
int get strBonus => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// CON 보너스
|
||||||
|
int get conBonus => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// DEX 보너스
|
||||||
|
int get dexBonus => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// INT 보너스
|
||||||
|
int get intBonus => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// WIS 보너스
|
||||||
|
int get wisBonus => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// CHA 보너스
|
||||||
|
int get chaBonus => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// 무기 공격속도 (밀리초, 무기 전용)
|
||||||
|
///
|
||||||
|
/// 0이면 기본값(1000ms) 사용, 값이 클수록 느린 공격.
|
||||||
|
/// 느린 무기는 높은 기본 데미지를 가짐.
|
||||||
|
int get attackSpeed => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this ItemStats to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of ItemStats
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$ItemStatsCopyWith<ItemStats> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $ItemStatsCopyWith<$Res> {
|
||||||
|
factory $ItemStatsCopyWith(ItemStats value, $Res Function(ItemStats) then) =
|
||||||
|
_$ItemStatsCopyWithImpl<$Res, ItemStats>;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
int atk,
|
||||||
|
int def,
|
||||||
|
int magAtk,
|
||||||
|
int magDef,
|
||||||
|
double criRate,
|
||||||
|
double evasion,
|
||||||
|
double blockRate,
|
||||||
|
double parryRate,
|
||||||
|
int hpBonus,
|
||||||
|
int mpBonus,
|
||||||
|
int strBonus,
|
||||||
|
int conBonus,
|
||||||
|
int dexBonus,
|
||||||
|
int intBonus,
|
||||||
|
int wisBonus,
|
||||||
|
int chaBonus,
|
||||||
|
int attackSpeed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$ItemStatsCopyWithImpl<$Res, $Val extends ItemStats>
|
||||||
|
implements $ItemStatsCopyWith<$Res> {
|
||||||
|
_$ItemStatsCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of ItemStats
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? atk = null,
|
||||||
|
Object? def = null,
|
||||||
|
Object? magAtk = null,
|
||||||
|
Object? magDef = null,
|
||||||
|
Object? criRate = null,
|
||||||
|
Object? evasion = null,
|
||||||
|
Object? blockRate = null,
|
||||||
|
Object? parryRate = null,
|
||||||
|
Object? hpBonus = null,
|
||||||
|
Object? mpBonus = null,
|
||||||
|
Object? strBonus = null,
|
||||||
|
Object? conBonus = null,
|
||||||
|
Object? dexBonus = null,
|
||||||
|
Object? intBonus = null,
|
||||||
|
Object? wisBonus = null,
|
||||||
|
Object? chaBonus = null,
|
||||||
|
Object? attackSpeed = null,
|
||||||
|
}) {
|
||||||
|
return _then(
|
||||||
|
_value.copyWith(
|
||||||
|
atk: null == atk
|
||||||
|
? _value.atk
|
||||||
|
: atk // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
def: null == def
|
||||||
|
? _value.def
|
||||||
|
: def // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
magAtk: null == magAtk
|
||||||
|
? _value.magAtk
|
||||||
|
: magAtk // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
magDef: null == magDef
|
||||||
|
? _value.magDef
|
||||||
|
: magDef // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
criRate: null == criRate
|
||||||
|
? _value.criRate
|
||||||
|
: criRate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
evasion: null == evasion
|
||||||
|
? _value.evasion
|
||||||
|
: evasion // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
blockRate: null == blockRate
|
||||||
|
? _value.blockRate
|
||||||
|
: blockRate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
parryRate: null == parryRate
|
||||||
|
? _value.parryRate
|
||||||
|
: parryRate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
hpBonus: null == hpBonus
|
||||||
|
? _value.hpBonus
|
||||||
|
: hpBonus // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
mpBonus: null == mpBonus
|
||||||
|
? _value.mpBonus
|
||||||
|
: mpBonus // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
strBonus: null == strBonus
|
||||||
|
? _value.strBonus
|
||||||
|
: strBonus // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
conBonus: null == conBonus
|
||||||
|
? _value.conBonus
|
||||||
|
: conBonus // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
dexBonus: null == dexBonus
|
||||||
|
? _value.dexBonus
|
||||||
|
: dexBonus // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
intBonus: null == intBonus
|
||||||
|
? _value.intBonus
|
||||||
|
: intBonus // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
wisBonus: null == wisBonus
|
||||||
|
? _value.wisBonus
|
||||||
|
: wisBonus // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
chaBonus: null == chaBonus
|
||||||
|
? _value.chaBonus
|
||||||
|
: chaBonus // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
attackSpeed: null == attackSpeed
|
||||||
|
? _value.attackSpeed
|
||||||
|
: attackSpeed // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
)
|
||||||
|
as $Val,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$ItemStatsImplCopyWith<$Res>
|
||||||
|
implements $ItemStatsCopyWith<$Res> {
|
||||||
|
factory _$$ItemStatsImplCopyWith(
|
||||||
|
_$ItemStatsImpl value,
|
||||||
|
$Res Function(_$ItemStatsImpl) then,
|
||||||
|
) = __$$ItemStatsImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
int atk,
|
||||||
|
int def,
|
||||||
|
int magAtk,
|
||||||
|
int magDef,
|
||||||
|
double criRate,
|
||||||
|
double evasion,
|
||||||
|
double blockRate,
|
||||||
|
double parryRate,
|
||||||
|
int hpBonus,
|
||||||
|
int mpBonus,
|
||||||
|
int strBonus,
|
||||||
|
int conBonus,
|
||||||
|
int dexBonus,
|
||||||
|
int intBonus,
|
||||||
|
int wisBonus,
|
||||||
|
int chaBonus,
|
||||||
|
int attackSpeed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$ItemStatsImplCopyWithImpl<$Res>
|
||||||
|
extends _$ItemStatsCopyWithImpl<$Res, _$ItemStatsImpl>
|
||||||
|
implements _$$ItemStatsImplCopyWith<$Res> {
|
||||||
|
__$$ItemStatsImplCopyWithImpl(
|
||||||
|
_$ItemStatsImpl _value,
|
||||||
|
$Res Function(_$ItemStatsImpl) _then,
|
||||||
|
) : super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of ItemStats
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? atk = null,
|
||||||
|
Object? def = null,
|
||||||
|
Object? magAtk = null,
|
||||||
|
Object? magDef = null,
|
||||||
|
Object? criRate = null,
|
||||||
|
Object? evasion = null,
|
||||||
|
Object? blockRate = null,
|
||||||
|
Object? parryRate = null,
|
||||||
|
Object? hpBonus = null,
|
||||||
|
Object? mpBonus = null,
|
||||||
|
Object? strBonus = null,
|
||||||
|
Object? conBonus = null,
|
||||||
|
Object? dexBonus = null,
|
||||||
|
Object? intBonus = null,
|
||||||
|
Object? wisBonus = null,
|
||||||
|
Object? chaBonus = null,
|
||||||
|
Object? attackSpeed = null,
|
||||||
|
}) {
|
||||||
|
return _then(
|
||||||
|
_$ItemStatsImpl(
|
||||||
|
atk: null == atk
|
||||||
|
? _value.atk
|
||||||
|
: atk // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
def: null == def
|
||||||
|
? _value.def
|
||||||
|
: def // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
magAtk: null == magAtk
|
||||||
|
? _value.magAtk
|
||||||
|
: magAtk // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
magDef: null == magDef
|
||||||
|
? _value.magDef
|
||||||
|
: magDef // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
criRate: null == criRate
|
||||||
|
? _value.criRate
|
||||||
|
: criRate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
evasion: null == evasion
|
||||||
|
? _value.evasion
|
||||||
|
: evasion // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
blockRate: null == blockRate
|
||||||
|
? _value.blockRate
|
||||||
|
: blockRate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
parryRate: null == parryRate
|
||||||
|
? _value.parryRate
|
||||||
|
: parryRate // ignore: cast_nullable_to_non_nullable
|
||||||
|
as double,
|
||||||
|
hpBonus: null == hpBonus
|
||||||
|
? _value.hpBonus
|
||||||
|
: hpBonus // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
mpBonus: null == mpBonus
|
||||||
|
? _value.mpBonus
|
||||||
|
: mpBonus // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
strBonus: null == strBonus
|
||||||
|
? _value.strBonus
|
||||||
|
: strBonus // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
conBonus: null == conBonus
|
||||||
|
? _value.conBonus
|
||||||
|
: conBonus // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
dexBonus: null == dexBonus
|
||||||
|
? _value.dexBonus
|
||||||
|
: dexBonus // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
intBonus: null == intBonus
|
||||||
|
? _value.intBonus
|
||||||
|
: intBonus // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
wisBonus: null == wisBonus
|
||||||
|
? _value.wisBonus
|
||||||
|
: wisBonus // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
chaBonus: null == chaBonus
|
||||||
|
? _value.chaBonus
|
||||||
|
: chaBonus // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
attackSpeed: null == attackSpeed
|
||||||
|
? _value.attackSpeed
|
||||||
|
: attackSpeed // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$ItemStatsImpl extends _ItemStats {
|
||||||
|
const _$ItemStatsImpl({
|
||||||
|
this.atk = 0,
|
||||||
|
this.def = 0,
|
||||||
|
this.magAtk = 0,
|
||||||
|
this.magDef = 0,
|
||||||
|
this.criRate = 0.0,
|
||||||
|
this.evasion = 0.0,
|
||||||
|
this.blockRate = 0.0,
|
||||||
|
this.parryRate = 0.0,
|
||||||
|
this.hpBonus = 0,
|
||||||
|
this.mpBonus = 0,
|
||||||
|
this.strBonus = 0,
|
||||||
|
this.conBonus = 0,
|
||||||
|
this.dexBonus = 0,
|
||||||
|
this.intBonus = 0,
|
||||||
|
this.wisBonus = 0,
|
||||||
|
this.chaBonus = 0,
|
||||||
|
this.attackSpeed = 0,
|
||||||
|
}) : super._();
|
||||||
|
|
||||||
|
factory _$ItemStatsImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$ItemStatsImplFromJson(json);
|
||||||
|
|
||||||
|
/// 물리 공격력 보정
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int atk;
|
||||||
|
|
||||||
|
/// 물리 방어력 보정
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int def;
|
||||||
|
|
||||||
|
/// 마법 공격력 보정
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int magAtk;
|
||||||
|
|
||||||
|
/// 마법 방어력 보정
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int magDef;
|
||||||
|
|
||||||
|
/// 크리티컬 확률 보정 (0.0 ~ 1.0)
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final double criRate;
|
||||||
|
|
||||||
|
/// 회피율 보정 (0.0 ~ 1.0)
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final double evasion;
|
||||||
|
|
||||||
|
/// 방패 방어율 (방패 전용, 0.0 ~ 1.0)
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final double blockRate;
|
||||||
|
|
||||||
|
/// 무기 쳐내기 확률 (무기 전용, 0.0 ~ 1.0)
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final double parryRate;
|
||||||
|
|
||||||
|
/// HP 보너스
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int hpBonus;
|
||||||
|
|
||||||
|
/// MP 보너스
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int mpBonus;
|
||||||
|
|
||||||
|
/// STR 보너스
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int strBonus;
|
||||||
|
|
||||||
|
/// CON 보너스
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int conBonus;
|
||||||
|
|
||||||
|
/// DEX 보너스
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int dexBonus;
|
||||||
|
|
||||||
|
/// INT 보너스
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int intBonus;
|
||||||
|
|
||||||
|
/// WIS 보너스
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int wisBonus;
|
||||||
|
|
||||||
|
/// CHA 보너스
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int chaBonus;
|
||||||
|
|
||||||
|
/// 무기 공격속도 (밀리초, 무기 전용)
|
||||||
|
///
|
||||||
|
/// 0이면 기본값(1000ms) 사용, 값이 클수록 느린 공격.
|
||||||
|
/// 느린 무기는 높은 기본 데미지를 가짐.
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int attackSpeed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ItemStats(atk: $atk, def: $def, magAtk: $magAtk, magDef: $magDef, criRate: $criRate, evasion: $evasion, blockRate: $blockRate, parryRate: $parryRate, hpBonus: $hpBonus, mpBonus: $mpBonus, strBonus: $strBonus, conBonus: $conBonus, dexBonus: $dexBonus, intBonus: $intBonus, wisBonus: $wisBonus, chaBonus: $chaBonus, attackSpeed: $attackSpeed)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$ItemStatsImpl &&
|
||||||
|
(identical(other.atk, atk) || other.atk == atk) &&
|
||||||
|
(identical(other.def, def) || other.def == def) &&
|
||||||
|
(identical(other.magAtk, magAtk) || other.magAtk == magAtk) &&
|
||||||
|
(identical(other.magDef, magDef) || other.magDef == magDef) &&
|
||||||
|
(identical(other.criRate, criRate) || other.criRate == criRate) &&
|
||||||
|
(identical(other.evasion, evasion) || other.evasion == evasion) &&
|
||||||
|
(identical(other.blockRate, blockRate) ||
|
||||||
|
other.blockRate == blockRate) &&
|
||||||
|
(identical(other.parryRate, parryRate) ||
|
||||||
|
other.parryRate == parryRate) &&
|
||||||
|
(identical(other.hpBonus, hpBonus) || other.hpBonus == hpBonus) &&
|
||||||
|
(identical(other.mpBonus, mpBonus) || other.mpBonus == mpBonus) &&
|
||||||
|
(identical(other.strBonus, strBonus) ||
|
||||||
|
other.strBonus == strBonus) &&
|
||||||
|
(identical(other.conBonus, conBonus) ||
|
||||||
|
other.conBonus == conBonus) &&
|
||||||
|
(identical(other.dexBonus, dexBonus) ||
|
||||||
|
other.dexBonus == dexBonus) &&
|
||||||
|
(identical(other.intBonus, intBonus) ||
|
||||||
|
other.intBonus == intBonus) &&
|
||||||
|
(identical(other.wisBonus, wisBonus) ||
|
||||||
|
other.wisBonus == wisBonus) &&
|
||||||
|
(identical(other.chaBonus, chaBonus) ||
|
||||||
|
other.chaBonus == chaBonus) &&
|
||||||
|
(identical(other.attackSpeed, attackSpeed) ||
|
||||||
|
other.attackSpeed == attackSpeed));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType,
|
||||||
|
atk,
|
||||||
|
def,
|
||||||
|
magAtk,
|
||||||
|
magDef,
|
||||||
|
criRate,
|
||||||
|
evasion,
|
||||||
|
blockRate,
|
||||||
|
parryRate,
|
||||||
|
hpBonus,
|
||||||
|
mpBonus,
|
||||||
|
strBonus,
|
||||||
|
conBonus,
|
||||||
|
dexBonus,
|
||||||
|
intBonus,
|
||||||
|
wisBonus,
|
||||||
|
chaBonus,
|
||||||
|
attackSpeed,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Create a copy of ItemStats
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$ItemStatsImplCopyWith<_$ItemStatsImpl> get copyWith =>
|
||||||
|
__$$ItemStatsImplCopyWithImpl<_$ItemStatsImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$ItemStatsImplToJson(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _ItemStats extends ItemStats {
|
||||||
|
const factory _ItemStats({
|
||||||
|
final int atk,
|
||||||
|
final int def,
|
||||||
|
final int magAtk,
|
||||||
|
final int magDef,
|
||||||
|
final double criRate,
|
||||||
|
final double evasion,
|
||||||
|
final double blockRate,
|
||||||
|
final double parryRate,
|
||||||
|
final int hpBonus,
|
||||||
|
final int mpBonus,
|
||||||
|
final int strBonus,
|
||||||
|
final int conBonus,
|
||||||
|
final int dexBonus,
|
||||||
|
final int intBonus,
|
||||||
|
final int wisBonus,
|
||||||
|
final int chaBonus,
|
||||||
|
final int attackSpeed,
|
||||||
|
}) = _$ItemStatsImpl;
|
||||||
|
const _ItemStats._() : super._();
|
||||||
|
|
||||||
|
factory _ItemStats.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$ItemStatsImpl.fromJson;
|
||||||
|
|
||||||
|
/// 물리 공격력 보정
|
||||||
|
@override
|
||||||
|
int get atk;
|
||||||
|
|
||||||
|
/// 물리 방어력 보정
|
||||||
|
@override
|
||||||
|
int get def;
|
||||||
|
|
||||||
|
/// 마법 공격력 보정
|
||||||
|
@override
|
||||||
|
int get magAtk;
|
||||||
|
|
||||||
|
/// 마법 방어력 보정
|
||||||
|
@override
|
||||||
|
int get magDef;
|
||||||
|
|
||||||
|
/// 크리티컬 확률 보정 (0.0 ~ 1.0)
|
||||||
|
@override
|
||||||
|
double get criRate;
|
||||||
|
|
||||||
|
/// 회피율 보정 (0.0 ~ 1.0)
|
||||||
|
@override
|
||||||
|
double get evasion;
|
||||||
|
|
||||||
|
/// 방패 방어율 (방패 전용, 0.0 ~ 1.0)
|
||||||
|
@override
|
||||||
|
double get blockRate;
|
||||||
|
|
||||||
|
/// 무기 쳐내기 확률 (무기 전용, 0.0 ~ 1.0)
|
||||||
|
@override
|
||||||
|
double get parryRate;
|
||||||
|
|
||||||
|
/// HP 보너스
|
||||||
|
@override
|
||||||
|
int get hpBonus;
|
||||||
|
|
||||||
|
/// MP 보너스
|
||||||
|
@override
|
||||||
|
int get mpBonus;
|
||||||
|
|
||||||
|
/// STR 보너스
|
||||||
|
@override
|
||||||
|
int get strBonus;
|
||||||
|
|
||||||
|
/// CON 보너스
|
||||||
|
@override
|
||||||
|
int get conBonus;
|
||||||
|
|
||||||
|
/// DEX 보너스
|
||||||
|
@override
|
||||||
|
int get dexBonus;
|
||||||
|
|
||||||
|
/// INT 보너스
|
||||||
|
@override
|
||||||
|
int get intBonus;
|
||||||
|
|
||||||
|
/// WIS 보너스
|
||||||
|
@override
|
||||||
|
int get wisBonus;
|
||||||
|
|
||||||
|
/// CHA 보너스
|
||||||
|
@override
|
||||||
|
int get chaBonus;
|
||||||
|
|
||||||
|
/// 무기 공격속도 (밀리초, 무기 전용)
|
||||||
|
///
|
||||||
|
/// 0이면 기본값(1000ms) 사용, 값이 클수록 느린 공격.
|
||||||
|
/// 느린 무기는 높은 기본 데미지를 가짐.
|
||||||
|
@override
|
||||||
|
int get attackSpeed;
|
||||||
|
|
||||||
|
/// Create a copy of ItemStats
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$ItemStatsImplCopyWith<_$ItemStatsImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
49
lib/src/core/model/item_stats.g.dart
Normal file
49
lib/src/core/model/item_stats.g.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'item_stats.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$ItemStatsImpl _$$ItemStatsImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ItemStatsImpl(
|
||||||
|
atk: (json['atk'] as num?)?.toInt() ?? 0,
|
||||||
|
def: (json['def'] as num?)?.toInt() ?? 0,
|
||||||
|
magAtk: (json['magAtk'] as num?)?.toInt() ?? 0,
|
||||||
|
magDef: (json['magDef'] as num?)?.toInt() ?? 0,
|
||||||
|
criRate: (json['criRate'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
evasion: (json['evasion'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
blockRate: (json['blockRate'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
parryRate: (json['parryRate'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
hpBonus: (json['hpBonus'] as num?)?.toInt() ?? 0,
|
||||||
|
mpBonus: (json['mpBonus'] as num?)?.toInt() ?? 0,
|
||||||
|
strBonus: (json['strBonus'] as num?)?.toInt() ?? 0,
|
||||||
|
conBonus: (json['conBonus'] as num?)?.toInt() ?? 0,
|
||||||
|
dexBonus: (json['dexBonus'] as num?)?.toInt() ?? 0,
|
||||||
|
intBonus: (json['intBonus'] as num?)?.toInt() ?? 0,
|
||||||
|
wisBonus: (json['wisBonus'] as num?)?.toInt() ?? 0,
|
||||||
|
chaBonus: (json['chaBonus'] as num?)?.toInt() ?? 0,
|
||||||
|
attackSpeed: (json['attackSpeed'] as num?)?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$ItemStatsImplToJson(_$ItemStatsImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'atk': instance.atk,
|
||||||
|
'def': instance.def,
|
||||||
|
'magAtk': instance.magAtk,
|
||||||
|
'magDef': instance.magDef,
|
||||||
|
'criRate': instance.criRate,
|
||||||
|
'evasion': instance.evasion,
|
||||||
|
'blockRate': instance.blockRate,
|
||||||
|
'parryRate': instance.parryRate,
|
||||||
|
'hpBonus': instance.hpBonus,
|
||||||
|
'mpBonus': instance.mpBonus,
|
||||||
|
'strBonus': instance.strBonus,
|
||||||
|
'conBonus': instance.conBonus,
|
||||||
|
'dexBonus': instance.dexBonus,
|
||||||
|
'intBonus': instance.intBonus,
|
||||||
|
'wisBonus': instance.wisBonus,
|
||||||
|
'chaBonus': instance.chaBonus,
|
||||||
|
'attackSpeed': instance.attackSpeed,
|
||||||
|
};
|
||||||
327
lib/src/core/util/pq_item.dart
Normal file
327
lib/src/core/util/pq_item.dart
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/pq_random.dart';
|
||||||
|
|
||||||
|
/// 아이템/장비 생성 관련 함수들
|
||||||
|
/// 원본 Main.pas의 아이템 생성 로직을 포팅
|
||||||
|
|
||||||
|
/// 장비 생성 결과 (구조화된 데이터로 l10n 지원)
|
||||||
|
class EquipResult {
|
||||||
|
const EquipResult({
|
||||||
|
required this.baseName,
|
||||||
|
this.modifiers = const [],
|
||||||
|
this.plusValue = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 기본 장비 이름 (예: "VPN Cloak")
|
||||||
|
final String baseName;
|
||||||
|
|
||||||
|
/// 수식어 목록 (예: ["Holey", "Deprecated"])
|
||||||
|
final List<String> modifiers;
|
||||||
|
|
||||||
|
/// +/- 수치 (예: -1, +2)
|
||||||
|
final int plusValue;
|
||||||
|
|
||||||
|
/// 영문 전체 이름 생성 (기존 방식)
|
||||||
|
String get displayName {
|
||||||
|
var name = baseName;
|
||||||
|
for (final mod in modifiers) {
|
||||||
|
name = '$mod $name';
|
||||||
|
}
|
||||||
|
if (plusValue != 0) {
|
||||||
|
name = '${plusValue > 0 ? '+' : ''}$plusValue $name';
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 아이템 생성 결과 (구조화된 데이터로 l10n 지원)
|
||||||
|
class ItemResult {
|
||||||
|
const ItemResult({this.attrib, this.special, this.itemOf, this.boringItem});
|
||||||
|
|
||||||
|
/// 아이템 속성 (예: "Golden")
|
||||||
|
final String? attrib;
|
||||||
|
|
||||||
|
/// 특수 아이템 (예: "Iterator")
|
||||||
|
final String? special;
|
||||||
|
|
||||||
|
/// "~의" 접미사 (예: "Monitoring")
|
||||||
|
final String? itemOf;
|
||||||
|
|
||||||
|
/// 단순 아이템 (보링 아이템용)
|
||||||
|
final String? boringItem;
|
||||||
|
|
||||||
|
/// 영문 전체 이름 생성 (기존 방식)
|
||||||
|
String get displayName {
|
||||||
|
if (boringItem != null) return boringItem!;
|
||||||
|
if (attrib != null && special != null && itemOf != null) {
|
||||||
|
return '$attrib $special of $itemOf';
|
||||||
|
}
|
||||||
|
if (attrib != null && special != null) {
|
||||||
|
return '$attrib $special';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 아이템 생성 함수들
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// 단순 아이템 (Boring Item)
|
||||||
|
String boringItem(PqConfig config, DeterministicRandom rng) {
|
||||||
|
return pick(config.boringItems, rng);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 흥미로운 아이템 (Interesting Item)
|
||||||
|
String interestingItem(PqConfig config, DeterministicRandom rng) {
|
||||||
|
final attr = pick(config.itemAttrib, rng);
|
||||||
|
final special = pick(config.specials, rng);
|
||||||
|
return '$attr $special';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 특수 아이템 (Special Item)
|
||||||
|
String specialItem(PqConfig config, DeterministicRandom rng) {
|
||||||
|
return '${interestingItem(config, rng)} of ${pick(config.itemOfs, rng)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 구조화된 단순 아이템 결과 반환 (l10n 지원)
|
||||||
|
ItemResult boringItemStructured(PqConfig config, DeterministicRandom rng) {
|
||||||
|
return ItemResult(boringItem: pick(config.boringItems, rng));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 구조화된 흥미로운 아이템 결과 반환 (l10n 지원)
|
||||||
|
ItemResult interestingItemStructured(PqConfig config, DeterministicRandom rng) {
|
||||||
|
return ItemResult(
|
||||||
|
attrib: pick(config.itemAttrib, rng),
|
||||||
|
special: pick(config.specials, rng),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 구조화된 특수 아이템 결과 반환 (l10n 지원)
|
||||||
|
ItemResult specialItemStructured(PqConfig config, DeterministicRandom rng) {
|
||||||
|
return ItemResult(
|
||||||
|
attrib: pick(config.itemAttrib, rng),
|
||||||
|
special: pick(config.specials, rng),
|
||||||
|
itemOf: pick(config.itemOfs, rng),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 구조화된 승리 아이템 결과 반환 (l10n 지원)
|
||||||
|
ItemResult winItemStructured(
|
||||||
|
PqConfig config,
|
||||||
|
DeterministicRandom rng,
|
||||||
|
int inventoryCount,
|
||||||
|
) {
|
||||||
|
final threshold = math.max(250, rng.nextInt(999));
|
||||||
|
if (inventoryCount > threshold) {
|
||||||
|
return const ItemResult(); // 빈 결과
|
||||||
|
}
|
||||||
|
return specialItemStructured(config, rng);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 승리 아이템 (문자열 반환)
|
||||||
|
String winItem(PqConfig config, DeterministicRandom rng, int inventoryCount) {
|
||||||
|
final threshold = math.max(250, rng.nextInt(999));
|
||||||
|
if (inventoryCount > threshold) return '';
|
||||||
|
return specialItem(config, rng);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 장비 선택 함수들
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// 무기 선택
|
||||||
|
String pickWeapon(PqConfig config, DeterministicRandom rng, int level) {
|
||||||
|
return _lPick(config.weapons, rng, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 방패 선택
|
||||||
|
String pickShield(PqConfig config, DeterministicRandom rng, int level) {
|
||||||
|
return _lPick(config.shields, rng, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 방어구 선택
|
||||||
|
String pickArmor(PqConfig config, DeterministicRandom rng, int level) {
|
||||||
|
return _lPick(config.armors, rng, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 주문 선택
|
||||||
|
String pickSpell(PqConfig config, DeterministicRandom rng, int goalLevel) {
|
||||||
|
return _lPick(config.spells, rng, goalLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 원본 Main.pas:776-789 LPick: 6회 시도하여 목표 레벨에 가장 가까운 아이템 선택
|
||||||
|
String _lPick(List<String> items, DeterministicRandom rng, int goal) {
|
||||||
|
if (items.isEmpty) return '';
|
||||||
|
var result = pick(items, rng);
|
||||||
|
var bestLevel = _parseLevel(result);
|
||||||
|
|
||||||
|
for (var i = 0; i < 5; i++) {
|
||||||
|
final candidate = pick(items, rng);
|
||||||
|
final candLevel = _parseLevel(candidate);
|
||||||
|
if ((goal - candLevel).abs() < (goal - bestLevel).abs()) {
|
||||||
|
result = candidate;
|
||||||
|
bestLevel = candLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 아이템 문자열에서 레벨 파싱
|
||||||
|
int _parseLevel(String entry) {
|
||||||
|
final parts = entry.split('|');
|
||||||
|
if (parts.length < 2) return 0;
|
||||||
|
return int.tryParse(parts[1].replaceAll('+', '')) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 장비 생성 함수들
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// 수식어 추가
|
||||||
|
String addModifier(
|
||||||
|
DeterministicRandom rng,
|
||||||
|
String baseName,
|
||||||
|
List<String> modifiers,
|
||||||
|
int plus,
|
||||||
|
) {
|
||||||
|
var name = baseName;
|
||||||
|
var remaining = plus;
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
while (count < 2 && remaining != 0) {
|
||||||
|
final modifier = pick(modifiers, rng);
|
||||||
|
final parts = modifier.split('|');
|
||||||
|
if (parts.isEmpty) break;
|
||||||
|
final label = parts[0];
|
||||||
|
final qual = parts.length > 1 ? int.tryParse(parts[1]) ?? 0 : 0;
|
||||||
|
if (name.contains(label)) break; // avoid repeats
|
||||||
|
if (remaining.abs() < qual.abs()) break;
|
||||||
|
name = '$label $name';
|
||||||
|
remaining -= qual;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining != 0) {
|
||||||
|
name = '${remaining > 0 ? '+' : ''}$remaining $name';
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 장비 생성 (원본 Main.pas:791-830 WinEquip)
|
||||||
|
/// [slotIndex]: 0=Weapon, 1=Shield, 2-10=Armor 계열
|
||||||
|
String winEquip(
|
||||||
|
PqConfig config,
|
||||||
|
DeterministicRandom rng,
|
||||||
|
int level,
|
||||||
|
int slotIndex,
|
||||||
|
) {
|
||||||
|
final bool isWeapon = slotIndex == 0;
|
||||||
|
final List<String> items;
|
||||||
|
if (slotIndex == 0) {
|
||||||
|
items = config.weapons;
|
||||||
|
} else if (slotIndex == 1) {
|
||||||
|
items = config.shields;
|
||||||
|
} else {
|
||||||
|
items = config.armors;
|
||||||
|
}
|
||||||
|
final better = isWeapon ? config.offenseAttrib : config.defenseAttrib;
|
||||||
|
final worse = isWeapon ? config.offenseBad : config.defenseBad;
|
||||||
|
|
||||||
|
final base = _lPick(items, rng, level);
|
||||||
|
final parts = base.split('|');
|
||||||
|
final baseName = parts[0];
|
||||||
|
final qual = parts.length > 1
|
||||||
|
? int.tryParse(parts[1].replaceAll('+', '')) ?? 0
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
final plus = level - qual;
|
||||||
|
final modifierList = plus >= 0 ? better : worse;
|
||||||
|
return addModifier(rng, baseName, modifierList, plus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// EquipmentSlot enum을 사용하는 편의 함수
|
||||||
|
String winEquipBySlot(
|
||||||
|
PqConfig config,
|
||||||
|
DeterministicRandom rng,
|
||||||
|
int level,
|
||||||
|
EquipmentSlot slot,
|
||||||
|
) {
|
||||||
|
return winEquip(config, rng, level, slot.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 구조화된 장비 생성 결과 반환 (l10n 지원)
|
||||||
|
EquipResult winEquipStructured(
|
||||||
|
PqConfig config,
|
||||||
|
DeterministicRandom rng,
|
||||||
|
int level,
|
||||||
|
int slotIndex,
|
||||||
|
) {
|
||||||
|
final bool isWeapon = slotIndex == 0;
|
||||||
|
final List<String> items;
|
||||||
|
if (slotIndex == 0) {
|
||||||
|
items = config.weapons;
|
||||||
|
} else if (slotIndex == 1) {
|
||||||
|
items = config.shields;
|
||||||
|
} else {
|
||||||
|
items = config.armors;
|
||||||
|
}
|
||||||
|
final better = isWeapon ? config.offenseAttrib : config.defenseAttrib;
|
||||||
|
final worse = isWeapon ? config.offenseBad : config.defenseBad;
|
||||||
|
|
||||||
|
final base = _lPick(items, rng, level);
|
||||||
|
final parts = base.split('|');
|
||||||
|
final baseName = parts[0];
|
||||||
|
final qual = parts.length > 1
|
||||||
|
? int.tryParse(parts[1].replaceAll('+', '')) ?? 0
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
final plus = level - qual;
|
||||||
|
final modifierList = plus >= 0 ? better : worse;
|
||||||
|
return _addModifierStructured(rng, baseName, modifierList, plus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 구조화된 장비 결과 반환 (내부 함수)
|
||||||
|
EquipResult _addModifierStructured(
|
||||||
|
DeterministicRandom rng,
|
||||||
|
String baseName,
|
||||||
|
List<String> modifiers,
|
||||||
|
int plus,
|
||||||
|
) {
|
||||||
|
final collectedModifiers = <String>[];
|
||||||
|
var remaining = plus;
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
while (count < 2 && remaining != 0) {
|
||||||
|
final modifier = pick(modifiers, rng);
|
||||||
|
final parts = modifier.split('|');
|
||||||
|
if (parts.isEmpty) break;
|
||||||
|
final label = parts[0];
|
||||||
|
final qual = parts.length > 1 ? int.tryParse(parts[1]) ?? 0 : 0;
|
||||||
|
if (collectedModifiers.contains(label)) break; // avoid repeats
|
||||||
|
if (remaining.abs() < qual.abs()) break;
|
||||||
|
collectedModifiers.add(label);
|
||||||
|
remaining -= qual;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EquipResult(
|
||||||
|
baseName: baseName,
|
||||||
|
modifiers: collectedModifiers,
|
||||||
|
plusValue: remaining,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// EquipmentSlot enum을 사용하는 구조화된 버전
|
||||||
|
EquipResult winEquipBySlotStructured(
|
||||||
|
PqConfig config,
|
||||||
|
DeterministicRandom rng,
|
||||||
|
int level,
|
||||||
|
EquipmentSlot slot,
|
||||||
|
) {
|
||||||
|
return winEquipStructured(config, rng, level, slot.index);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
283
lib/src/core/util/pq_monster.dart
Normal file
283
lib/src/core/util/pq_monster.dart
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
|
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/pq_random.dart';
|
||||||
|
|
||||||
|
/// 몬스터 생성 관련 함수들
|
||||||
|
/// 원본 Main.pas의 몬스터 생성 로직을 포팅
|
||||||
|
|
||||||
|
/// monsterTask의 반환 타입 (원본 fTask.Caption 정보 포함)
|
||||||
|
class MonsterTaskResult {
|
||||||
|
const MonsterTaskResult({
|
||||||
|
required this.displayName,
|
||||||
|
required this.baseName,
|
||||||
|
required this.level,
|
||||||
|
required this.part,
|
||||||
|
required this.grade,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 화면에 표시할 몬스터 이름 (형용사 포함, 예: "a sick Goblin")
|
||||||
|
final String displayName;
|
||||||
|
|
||||||
|
/// 기본 몬스터 이름 (형용사 제외, 예: "Goblin")
|
||||||
|
final String baseName;
|
||||||
|
|
||||||
|
/// 몬스터 레벨
|
||||||
|
final int level;
|
||||||
|
|
||||||
|
/// 전리품 부위 (예: "claw", "tail", "*"는 WinItem 호출)
|
||||||
|
final String part;
|
||||||
|
|
||||||
|
/// 몬스터 등급 (Normal/Elite/Boss)
|
||||||
|
final MonsterGrade grade;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 몬스터 등급 결정 (Normal 85%, Elite 12%, Boss 3%)
|
||||||
|
/// 몬스터 레벨이 플레이어 레벨보다 높으면 상위 등급 확률 증가
|
||||||
|
MonsterGrade _determineGrade(
|
||||||
|
int monsterLevel,
|
||||||
|
int playerLevel,
|
||||||
|
DeterministicRandom rng,
|
||||||
|
) {
|
||||||
|
final levelDiff = monsterLevel - playerLevel;
|
||||||
|
final eliteBonus = (levelDiff * 2).clamp(0, 10);
|
||||||
|
final bossBonus = (levelDiff * 0.5).clamp(0, 3).toInt();
|
||||||
|
|
||||||
|
final roll = rng.nextInt(100);
|
||||||
|
if (roll < 3 + bossBonus) return MonsterGrade.boss;
|
||||||
|
if (roll < 15 + eliteBonus) return MonsterGrade.elite;
|
||||||
|
return MonsterGrade.normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 몬스터 문자열에서 레벨 파싱
|
||||||
|
int _monsterLevel(String entry) {
|
||||||
|
final parts = entry.split('|');
|
||||||
|
if (parts.length < 2) return 0;
|
||||||
|
return int.tryParse(parts[1]) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 몬스터 태스크 생성 (원본 Main.pas:523-640)
|
||||||
|
MonsterTaskResult monsterTask(
|
||||||
|
PqConfig config,
|
||||||
|
DeterministicRandom rng,
|
||||||
|
int level,
|
||||||
|
String? questMonster,
|
||||||
|
int? questLevel,
|
||||||
|
) {
|
||||||
|
var targetLevel = level;
|
||||||
|
|
||||||
|
for (var i = level; i > 0; i--) {
|
||||||
|
if (rng.nextInt(5) < 2) {
|
||||||
|
targetLevel += rng.nextInt(2) * 2 - 1; // RandSign
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetLevel < 1) targetLevel = 1;
|
||||||
|
|
||||||
|
String monster;
|
||||||
|
int monsterLevel;
|
||||||
|
String part;
|
||||||
|
bool definite = false;
|
||||||
|
|
||||||
|
// 원본 Main.pas:537-547: 가끔 NPC를 몬스터로 사용
|
||||||
|
if (rng.nextInt(25) == 0) {
|
||||||
|
final raceEn = pick(config.races, rng).split('|').first;
|
||||||
|
final race = l10n.translateRace(raceEn);
|
||||||
|
if (rng.nextInt(2) == 0) {
|
||||||
|
final klassEn = pick(config.klasses, rng).split('|').first;
|
||||||
|
final klass = l10n.translateKlass(klassEn);
|
||||||
|
monster = l10n.modifierPassing('$race $klass');
|
||||||
|
} else {
|
||||||
|
final titleEn = pickLow(config.titles, rng);
|
||||||
|
final title = l10n.translateTitle(titleEn);
|
||||||
|
monster = l10n.namedMonsterFormat(generateName(rng), '$title $race');
|
||||||
|
definite = true;
|
||||||
|
}
|
||||||
|
monsterLevel = targetLevel;
|
||||||
|
part = '*';
|
||||||
|
} else if (questMonster != null && rng.nextInt(4) == 0) {
|
||||||
|
monster = questMonster;
|
||||||
|
final parts = questMonster.split('|');
|
||||||
|
monsterLevel = questLevel ?? targetLevel;
|
||||||
|
part = parts.length > 2 ? parts[2] : '';
|
||||||
|
} else {
|
||||||
|
monster = pick(config.monsters, rng);
|
||||||
|
monsterLevel = _monsterLevel(monster);
|
||||||
|
for (var i = 0; i < 5; i++) {
|
||||||
|
final candidate = pick(config.monsters, rng);
|
||||||
|
final candLevel = _monsterLevel(candidate);
|
||||||
|
if ((targetLevel - candLevel).abs() <
|
||||||
|
(targetLevel - monsterLevel).abs()) {
|
||||||
|
monster = candidate;
|
||||||
|
monsterLevel = candLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final monsterParts = monster.split('|');
|
||||||
|
part = monsterParts.length > 2 ? monsterParts[2] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
final baseName = monster.split('|').first;
|
||||||
|
|
||||||
|
var qty = 1;
|
||||||
|
final levelDiff = targetLevel - monsterLevel;
|
||||||
|
var name = l10n.translateMonster(baseName);
|
||||||
|
|
||||||
|
if (levelDiff > 10) {
|
||||||
|
qty =
|
||||||
|
(targetLevel + rng.nextInt(monsterLevel == 0 ? 1 : monsterLevel)) ~/
|
||||||
|
(monsterLevel == 0 ? 1 : monsterLevel);
|
||||||
|
if (qty < 1) qty = 1;
|
||||||
|
targetLevel ~/= qty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (levelDiff <= -10) {
|
||||||
|
name = l10n.modifierImaginary(name);
|
||||||
|
} else if (levelDiff < -5) {
|
||||||
|
final i = 5 - rng.nextInt(10 + levelDiff + 1);
|
||||||
|
name = _sick(i, _young((monsterLevel - targetLevel) - i, name));
|
||||||
|
} else if (levelDiff < 0) {
|
||||||
|
if (rng.nextInt(2) == 1) {
|
||||||
|
name = _sick(levelDiff, name);
|
||||||
|
} else {
|
||||||
|
name = _young(levelDiff, name);
|
||||||
|
}
|
||||||
|
} else if (levelDiff >= 10) {
|
||||||
|
name = l10n.modifierMessianic(name);
|
||||||
|
} else if (levelDiff > 5) {
|
||||||
|
final i = 5 - rng.nextInt(10 - levelDiff + 1);
|
||||||
|
name = _big(i, _special((levelDiff) - i, name));
|
||||||
|
} else if (levelDiff > 0) {
|
||||||
|
if (rng.nextInt(2) == 1) {
|
||||||
|
name = _big(levelDiff, name);
|
||||||
|
} else {
|
||||||
|
name = _special(levelDiff, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!definite) {
|
||||||
|
name = l10n.indefiniteL10n(name, qty);
|
||||||
|
}
|
||||||
|
|
||||||
|
final grade = _determineGrade(monsterLevel, level, rng);
|
||||||
|
|
||||||
|
return MonsterTaskResult(
|
||||||
|
displayName: name,
|
||||||
|
baseName: baseName,
|
||||||
|
level: monsterLevel * qty,
|
||||||
|
part: part,
|
||||||
|
grade: grade,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 이름 있는 몬스터 생성 (NamedMonster, Main.pas:498-512)
|
||||||
|
String namedMonster(PqConfig config, DeterministicRandom rng, int level) {
|
||||||
|
String best = '';
|
||||||
|
int bestLevel = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < 5; i++) {
|
||||||
|
final m = pick(config.monsters, rng);
|
||||||
|
final parts = m.split('|');
|
||||||
|
final name = parts.first;
|
||||||
|
final lev = parts.length > 1 ? (int.tryParse(parts[1]) ?? 0) : 0;
|
||||||
|
|
||||||
|
if (best.isEmpty || (level - lev).abs() < (level - bestLevel).abs()) {
|
||||||
|
best = name;
|
||||||
|
bestLevel = lev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final translatedMonster = l10n.translateMonster(best);
|
||||||
|
return l10n.namedMonsterFormat(generateName(rng), translatedMonster);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 몬스터 수식어 함수들
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
String _sick(int m, String s) {
|
||||||
|
switch (m) {
|
||||||
|
case -5:
|
||||||
|
case 5:
|
||||||
|
return l10n.modifierDead(s);
|
||||||
|
case -4:
|
||||||
|
case 4:
|
||||||
|
return l10n.modifierComatose(s);
|
||||||
|
case -3:
|
||||||
|
case 3:
|
||||||
|
return l10n.modifierCrippled(s);
|
||||||
|
case -2:
|
||||||
|
case 2:
|
||||||
|
return l10n.modifierSick(s);
|
||||||
|
case -1:
|
||||||
|
case 1:
|
||||||
|
return l10n.modifierUndernourished(s);
|
||||||
|
default:
|
||||||
|
return '$m$s';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _young(int m, String s) {
|
||||||
|
switch (-m) {
|
||||||
|
case -5:
|
||||||
|
case 5:
|
||||||
|
return l10n.modifierFoetal(s);
|
||||||
|
case -4:
|
||||||
|
case 4:
|
||||||
|
return l10n.modifierBaby(s);
|
||||||
|
case -3:
|
||||||
|
case 3:
|
||||||
|
return l10n.modifierPreadolescent(s);
|
||||||
|
case -2:
|
||||||
|
case 2:
|
||||||
|
return l10n.modifierTeenage(s);
|
||||||
|
case -1:
|
||||||
|
case 1:
|
||||||
|
return l10n.modifierUnderage(s);
|
||||||
|
default:
|
||||||
|
return '$m$s';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _big(int m, String s) {
|
||||||
|
switch (m) {
|
||||||
|
case 1:
|
||||||
|
case -1:
|
||||||
|
return l10n.modifierGreater(s);
|
||||||
|
case 2:
|
||||||
|
case -2:
|
||||||
|
return l10n.modifierMassive(s);
|
||||||
|
case 3:
|
||||||
|
case -3:
|
||||||
|
return l10n.modifierEnormous(s);
|
||||||
|
case 4:
|
||||||
|
case -4:
|
||||||
|
return l10n.modifierGiant(s);
|
||||||
|
case 5:
|
||||||
|
case -5:
|
||||||
|
return l10n.modifierTitanic(s);
|
||||||
|
default:
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _special(int m, String s) {
|
||||||
|
switch (-m) {
|
||||||
|
case 1:
|
||||||
|
case -1:
|
||||||
|
return s.contains(' ') ? l10n.modifierVeteran(s) : l10n.modifierBattle(s);
|
||||||
|
case 2:
|
||||||
|
case -2:
|
||||||
|
return l10n.modifierCursed(s);
|
||||||
|
case 3:
|
||||||
|
case -3:
|
||||||
|
return s.contains(' ') ? l10n.modifierWarrior(s) : l10n.modifierWere(s);
|
||||||
|
case 4:
|
||||||
|
case -4:
|
||||||
|
return l10n.modifierUndead(s);
|
||||||
|
case 5:
|
||||||
|
case -5:
|
||||||
|
return l10n.modifierDemon(s);
|
||||||
|
default:
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
283
lib/src/core/util/pq_quest.dart
Normal file
283
lib/src/core/util/pq_quest.dart
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/pq_config.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/pq_item.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/pq_monster.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/pq_random.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/roman.dart';
|
||||||
|
|
||||||
|
/// 퀘스트/Act/시네마틱 관련 함수들
|
||||||
|
/// 원본 Main.pas의 퀘스트 로직을 포팅
|
||||||
|
|
||||||
|
/// 보상 종류
|
||||||
|
enum RewardKind { spell, equip, stat, item }
|
||||||
|
|
||||||
|
/// 퀘스트 결과
|
||||||
|
class QuestResult {
|
||||||
|
const QuestResult({
|
||||||
|
required this.caption,
|
||||||
|
required this.reward,
|
||||||
|
this.monsterName,
|
||||||
|
this.monsterLevel,
|
||||||
|
this.monsterIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String caption;
|
||||||
|
final RewardKind reward;
|
||||||
|
|
||||||
|
/// 몬스터 전체 데이터 (예: "Rat|5|tail") - 원본 fQuest.Caption
|
||||||
|
final String? monsterName;
|
||||||
|
|
||||||
|
/// 몬스터 레벨 (파싱된 값)
|
||||||
|
final int? monsterLevel;
|
||||||
|
|
||||||
|
/// 몬스터 인덱스 (config.monsters에서의 위치) - 원본 fQuest.Tag
|
||||||
|
final int? monsterIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Act 결과
|
||||||
|
class ActResult {
|
||||||
|
const ActResult({
|
||||||
|
required this.actTitle,
|
||||||
|
required this.plotBarMaxSeconds,
|
||||||
|
required this.rewards,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String actTitle;
|
||||||
|
final int plotBarMaxSeconds;
|
||||||
|
final List<RewardKind> rewards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 몬스터 문자열에서 레벨 파싱
|
||||||
|
int _monsterLevel(String entry) {
|
||||||
|
final parts = entry.split('|');
|
||||||
|
if (parts.length < 2) return 0;
|
||||||
|
return int.tryParse(parts[1]) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 퀘스트 완료 처리 (원본 Main.pas:930-984)
|
||||||
|
QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
|
||||||
|
final rewardRoll = rng.nextInt(4);
|
||||||
|
final reward = switch (rewardRoll) {
|
||||||
|
0 => RewardKind.spell,
|
||||||
|
1 => RewardKind.equip,
|
||||||
|
2 => RewardKind.stat,
|
||||||
|
_ => RewardKind.item,
|
||||||
|
};
|
||||||
|
|
||||||
|
final questRoll = rng.nextInt(5);
|
||||||
|
switch (questRoll) {
|
||||||
|
case 0:
|
||||||
|
// Exterminate: 4번 시도하여 레벨에 가장 가까운 몬스터 선택
|
||||||
|
var best = '';
|
||||||
|
var bestLevel = 0;
|
||||||
|
var bestIndex = 0;
|
||||||
|
for (var i = 0; i < 4; i++) {
|
||||||
|
final monsterIndex = rng.nextInt(config.monsters.length);
|
||||||
|
final m = config.monsters[monsterIndex];
|
||||||
|
final l = _monsterLevel(m);
|
||||||
|
if (i == 0 || (l - level).abs() < (bestLevel - level).abs()) {
|
||||||
|
best = m;
|
||||||
|
bestLevel = l;
|
||||||
|
bestIndex = monsterIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final nameEn = best.split('|').first;
|
||||||
|
final name = l10n.translateMonster(nameEn);
|
||||||
|
return QuestResult(
|
||||||
|
caption: l10n.questPatch(l10n.definiteL10n(name, 2)),
|
||||||
|
reward: reward,
|
||||||
|
monsterName: best,
|
||||||
|
monsterLevel: bestLevel,
|
||||||
|
monsterIndex: bestIndex,
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
// interestingItem: attrib + special 조합 후 번역
|
||||||
|
final attr = pick(config.itemAttrib, rng);
|
||||||
|
final special = pick(config.specials, rng);
|
||||||
|
final item = l10n.translateInterestingItem(attr, special);
|
||||||
|
return QuestResult(
|
||||||
|
caption: l10n.questLocate(l10n.definiteL10n(item, 1)),
|
||||||
|
reward: reward,
|
||||||
|
);
|
||||||
|
case 2:
|
||||||
|
final itemEn = boringItem(config, rng);
|
||||||
|
final item = l10n.translateBoringItem(itemEn);
|
||||||
|
return QuestResult(caption: l10n.questTransfer(item), reward: reward);
|
||||||
|
case 3:
|
||||||
|
final itemEn = boringItem(config, rng);
|
||||||
|
final item = l10n.translateBoringItem(itemEn);
|
||||||
|
return QuestResult(
|
||||||
|
caption: l10n.questDownload(l10n.indefiniteL10n(item, 1)),
|
||||||
|
reward: reward,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
// Stabilize: 2번 시도하여 레벨에 가장 가까운 몬스터 선택
|
||||||
|
var best = '';
|
||||||
|
var bestLevel = 0;
|
||||||
|
for (var i = 0; i < 2; i++) {
|
||||||
|
final m = pick(config.monsters, rng);
|
||||||
|
final l = _monsterLevel(m);
|
||||||
|
if (i == 0 || (l - level).abs() < (bestLevel - level).abs()) {
|
||||||
|
best = m;
|
||||||
|
bestLevel = l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final nameEn = best.split('|').first;
|
||||||
|
final name = l10n.translateMonster(nameEn);
|
||||||
|
return QuestResult(
|
||||||
|
caption: l10n.questStabilize(l10n.definiteL10n(name, 2)),
|
||||||
|
reward: reward,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Act별 Plot Bar 최대값 (초) - 10시간 완주 목표
|
||||||
|
const _actPlotBarSeconds = [
|
||||||
|
300, // Prologue: 5분
|
||||||
|
7200, // Act I: 2시간
|
||||||
|
10800, // Act II: 3시간
|
||||||
|
10800, // Act III: 3시간
|
||||||
|
5400, // Act IV: 1.5시간
|
||||||
|
1800, // Act V: 30분
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Act 완료 처리
|
||||||
|
ActResult completeAct(int existingActCount) {
|
||||||
|
final nextActIndex = existingActCount;
|
||||||
|
final title = l10n.actTitle(intToRoman(nextActIndex));
|
||||||
|
final plotBarMax = existingActCount < _actPlotBarSeconds.length
|
||||||
|
? _actPlotBarSeconds[existingActCount]
|
||||||
|
: 3600;
|
||||||
|
|
||||||
|
final rewards = <RewardKind>[];
|
||||||
|
if (existingActCount >= 1) {
|
||||||
|
rewards.add(RewardKind.equip);
|
||||||
|
}
|
||||||
|
if (existingActCount > 1) {
|
||||||
|
rewards.add(RewardKind.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ActResult(
|
||||||
|
actTitle: title,
|
||||||
|
plotBarMaxSeconds: plotBarMax,
|
||||||
|
rewards: rewards,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 시네마틱 관련 함수들 (Main.pas:456-521)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// NPC 이름 생성 (ImpressiveGuy, Main.pas:514-521)
|
||||||
|
String impressiveGuy(PqConfig config, DeterministicRandom rng) {
|
||||||
|
final titleEn = pick(config.impressiveTitles, rng);
|
||||||
|
final title = l10n.translateImpressiveTitle(titleEn);
|
||||||
|
switch (rng.nextInt(2)) {
|
||||||
|
case 0:
|
||||||
|
final raceEn = pick(config.races, rng).split('|').first;
|
||||||
|
final race = l10n.translateRace(raceEn);
|
||||||
|
return l10n.impressiveGuyPattern1(title, race);
|
||||||
|
case 1:
|
||||||
|
final name1 = generateName(rng);
|
||||||
|
final name2 = generateName(rng);
|
||||||
|
return l10n.impressiveGuyPattern2(title, name1, name2);
|
||||||
|
default:
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 플롯 간 시네마틱 이벤트 생성 (InterplotCinematic, Main.pas:456-495)
|
||||||
|
List<QueueEntry> interplotCinematic(
|
||||||
|
PqConfig config,
|
||||||
|
DeterministicRandom rng,
|
||||||
|
int level,
|
||||||
|
int plotCount,
|
||||||
|
) {
|
||||||
|
final entries = <QueueEntry>[];
|
||||||
|
|
||||||
|
void q(QueueKind kind, int seconds, String caption) {
|
||||||
|
entries.add(
|
||||||
|
QueueEntry(kind: kind, durationMillis: seconds * 1000, caption: caption),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (rng.nextInt(3)) {
|
||||||
|
case 0:
|
||||||
|
// 시나리오 1: 안전한 캐시 영역 도착
|
||||||
|
q(QueueKind.task, 1, l10n.cinematicCacheZone1());
|
||||||
|
q(QueueKind.task, 2, l10n.cinematicCacheZone2());
|
||||||
|
q(QueueKind.task, 2, l10n.cinematicCacheZone3());
|
||||||
|
q(QueueKind.task, 1, l10n.cinematicCacheZone4());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
// 시나리오 2: 강력한 버그와의 전투
|
||||||
|
q(QueueKind.task, 1, l10n.cinematicCombat1());
|
||||||
|
final nemesis = namedMonster(config, rng, level + 3);
|
||||||
|
q(QueueKind.task, 4, l10n.cinematicCombat2(nemesis));
|
||||||
|
|
||||||
|
var s = rng.nextInt(3);
|
||||||
|
final combatRounds = rng.nextInt(1 + plotCount);
|
||||||
|
for (var i = 0; i < combatRounds; i++) {
|
||||||
|
s += 1 + rng.nextInt(2);
|
||||||
|
switch (s % 3) {
|
||||||
|
case 0:
|
||||||
|
q(QueueKind.task, 2, l10n.cinematicCombatLocked(nemesis));
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
q(QueueKind.task, 2, l10n.cinematicCombatCorrupts(nemesis));
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
q(QueueKind.task, 2, l10n.cinematicCombatWorking(nemesis));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
q(QueueKind.task, 3, l10n.cinematicCombatVictory(nemesis));
|
||||||
|
q(QueueKind.task, 2, l10n.cinematicCombatWakeUp());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
// 시나리오 3: 내부자 위협 발견
|
||||||
|
final guy = impressiveGuy(config, rng);
|
||||||
|
final itemEn = boringItem(config, rng);
|
||||||
|
final item = l10n.translateBoringItem(itemEn);
|
||||||
|
q(QueueKind.task, 2, l10n.cinematicBetrayal1(guy));
|
||||||
|
q(QueueKind.task, 3, l10n.cinematicBetrayal2(guy));
|
||||||
|
q(QueueKind.task, 2, l10n.cinematicBetrayal3(item));
|
||||||
|
q(QueueKind.task, 2, l10n.cinematicBetrayal4());
|
||||||
|
q(QueueKind.task, 2, l10n.cinematicBetrayal5(guy));
|
||||||
|
q(QueueKind.task, 3, l10n.cinematicBetrayal6());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
q(QueueKind.plot, 2, l10n.taskCompiling);
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 스펠 관련 함수
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// 스펠 획득 (원본 Main.pas:770-774)
|
||||||
|
String winSpell(
|
||||||
|
PqConfig config,
|
||||||
|
DeterministicRandom rng,
|
||||||
|
int wisdom,
|
||||||
|
int level,
|
||||||
|
) {
|
||||||
|
final maxIndex = math.min(wisdom + level, config.spells.length);
|
||||||
|
if (maxIndex <= 0) return '';
|
||||||
|
final index = randomLow(rng, maxIndex);
|
||||||
|
final entry = config.spells[index];
|
||||||
|
final parts = entry.split('|');
|
||||||
|
final name = parts[0];
|
||||||
|
final currentRank = romanToInt(parts.length > 1 ? parts[1] : 'I');
|
||||||
|
final nextRank = math.max(1, currentRank + 1);
|
||||||
|
return '$name|${intToRoman(nextRank)}';
|
||||||
|
}
|
||||||
61
lib/src/core/util/pq_random.dart
Normal file
61
lib/src/core/util/pq_random.dart
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
|
||||||
|
/// 랜덤/확률 관련 유틸리티 함수들
|
||||||
|
/// 원본 Main.pas의 랜덤 헬퍼 함수들을 포팅
|
||||||
|
|
||||||
|
/// 두 번의 랜덤 중 작은 값 반환 (낮은 인덱스 선호)
|
||||||
|
int randomLow(DeterministicRandom rng, int below) {
|
||||||
|
return math.min(rng.nextInt(below), rng.nextInt(below));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 리스트에서 랜덤 선택
|
||||||
|
String pick(List<String> values, DeterministicRandom rng) {
|
||||||
|
if (values.isEmpty) return '';
|
||||||
|
return values[rng.nextInt(values.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 리스트에서 낮은 인덱스 선호하여 선택
|
||||||
|
String pickLow(List<String> values, DeterministicRandom rng) {
|
||||||
|
if (values.isEmpty) return '';
|
||||||
|
return values[randomLow(rng, values.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 64비트 범위 랜덤 (큰 범위용)
|
||||||
|
int random64Below(DeterministicRandom rng, int below) {
|
||||||
|
if (below <= 0) return 0;
|
||||||
|
final hi = rng.nextUint32();
|
||||||
|
final lo = rng.nextUint32();
|
||||||
|
final combined = (hi << 32) | lo;
|
||||||
|
return (combined % below).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 3d6 스탯 롤
|
||||||
|
int rollStat(DeterministicRandom rng) {
|
||||||
|
return 3 + rng.nextInt(6) + rng.nextInt(6) + rng.nextInt(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 캐릭터 이름 생성 (원본 NewGuy.pas 기반)
|
||||||
|
String generateName(DeterministicRandom rng) {
|
||||||
|
const kParts = [
|
||||||
|
'br|cr|dr|fr|gr|j|kr|l|m|n|pr||||r|sh|tr|v|wh|x|y|z',
|
||||||
|
'a|a|e|e|i|i|o|o|u|u|ae|ie|oo|ou',
|
||||||
|
'b|ck|d|g|k|m|n|p|t|v|x|z',
|
||||||
|
];
|
||||||
|
|
||||||
|
var result = '';
|
||||||
|
for (var i = 0; i <= 5; i++) {
|
||||||
|
result += _pickFromPipe(kParts[i % 3], rng);
|
||||||
|
}
|
||||||
|
if (result.isEmpty) return result;
|
||||||
|
return '${result[0].toUpperCase()}${result.substring(1)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 파이프(|)로 구분된 문자열에서 랜덤 선택
|
||||||
|
String _pickFromPipe(String pipeSeparated, DeterministicRandom rng) {
|
||||||
|
final parts = pipeSeparated.split('|');
|
||||||
|
if (parts.isEmpty) return '';
|
||||||
|
final idx = rng.nextInt(parts.length);
|
||||||
|
return parts[idx];
|
||||||
|
}
|
||||||
64
lib/src/core/util/pq_stat.dart
Normal file
64
lib/src/core/util/pq_stat.dart
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
import 'package:asciineverdie/src/core/util/pq_random.dart';
|
||||||
|
|
||||||
|
/// 스탯 관련 함수들
|
||||||
|
/// 원본 Main.pas의 스탯 처리 로직을 포팅
|
||||||
|
|
||||||
|
/// 스탯 증가 인덱스 결정 (원본 Main.pas:870-883)
|
||||||
|
int winStatIndex(DeterministicRandom rng, List<int> statValues) {
|
||||||
|
// 50%: 모든 8개 스탯 중 랜덤
|
||||||
|
// 50%: 첫 6개(STR~CHA)만 제곱 가중치로 선택
|
||||||
|
if (rng.nextInt(2) == 0) {
|
||||||
|
return rng.nextInt(statValues.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 첫 6개(STR, CON, DEX, INT, WIS, CHA)만 제곱 가중치 적용
|
||||||
|
const firstSixCount = 6;
|
||||||
|
var total = 0;
|
||||||
|
for (var i = 0; i < firstSixCount && i < statValues.length; i++) {
|
||||||
|
total += statValues[i] * statValues[i];
|
||||||
|
}
|
||||||
|
if (total == 0) return rng.nextInt(firstSixCount);
|
||||||
|
var pickValue = random64Below(rng, total);
|
||||||
|
for (var i = 0; i < firstSixCount; i++) {
|
||||||
|
pickValue -= statValues[i] * statValues[i];
|
||||||
|
if (pickValue < 0) return i;
|
||||||
|
}
|
||||||
|
return firstSixCount - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 스탯 증가 적용
|
||||||
|
Stats winStat(Stats stats, DeterministicRandom rng) {
|
||||||
|
final values = <int>[
|
||||||
|
stats.str,
|
||||||
|
stats.con,
|
||||||
|
stats.dex,
|
||||||
|
stats.intelligence,
|
||||||
|
stats.wis,
|
||||||
|
stats.cha,
|
||||||
|
stats.hpMax,
|
||||||
|
stats.mpMax,
|
||||||
|
];
|
||||||
|
final idx = winStatIndex(rng, values);
|
||||||
|
switch (idx) {
|
||||||
|
case 0:
|
||||||
|
return stats.copyWith(str: stats.str + 1);
|
||||||
|
case 1:
|
||||||
|
return stats.copyWith(con: stats.con + 1);
|
||||||
|
case 2:
|
||||||
|
return stats.copyWith(dex: stats.dex + 1);
|
||||||
|
case 3:
|
||||||
|
return stats.copyWith(intelligence: stats.intelligence + 1);
|
||||||
|
case 4:
|
||||||
|
return stats.copyWith(wis: stats.wis + 1);
|
||||||
|
case 5:
|
||||||
|
return stats.copyWith(cha: stats.cha + 1);
|
||||||
|
case 6:
|
||||||
|
return stats.copyWith(hpMax: stats.hpMax + 1);
|
||||||
|
case 7:
|
||||||
|
return stats.copyWith(mpMax: stats.mpMax + 1);
|
||||||
|
default:
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
55
lib/src/core/util/pq_string.dart
Normal file
55
lib/src/core/util/pq_string.dart
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
|
|
||||||
|
/// 문자열 유틸리티 함수들
|
||||||
|
/// 원본 Main.pas의 문자열 처리 함수들을 포팅
|
||||||
|
|
||||||
|
/// 단어를 복수형으로 변환 (영문)
|
||||||
|
String pluralize(String s) {
|
||||||
|
if (_ends(s, 'y')) return '${s.substring(0, s.length - 1)}ies';
|
||||||
|
if (_ends(s, 'us')) return '${s.substring(0, s.length - 2)}i';
|
||||||
|
if (_ends(s, 'ch') || _ends(s, 'x') || _ends(s, 's')) return '${s}es';
|
||||||
|
if (_ends(s, 'f')) return '${s.substring(0, s.length - 1)}ves';
|
||||||
|
if (_ends(s, 'man') || _ends(s, 'Man')) {
|
||||||
|
return '${s.substring(0, s.length - 2)}en';
|
||||||
|
}
|
||||||
|
return '${s}s';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 부정관사 + 명사 (a/an + 단수 또는 수량 + 복수)
|
||||||
|
String indefinite(String s, int qty) {
|
||||||
|
if (qty == 1) {
|
||||||
|
const vowels = 'AEIOUÜaeiouü';
|
||||||
|
final first = s.isNotEmpty ? s[0] : 'a';
|
||||||
|
final article = vowels.contains(first) ? 'an' : 'a';
|
||||||
|
return '$article $s';
|
||||||
|
}
|
||||||
|
return '$qty ${pluralize(s)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 정관사 + 명사 (the + 명사)
|
||||||
|
String definite(String s, int qty) {
|
||||||
|
if (qty > 1) {
|
||||||
|
s = pluralize(s);
|
||||||
|
}
|
||||||
|
return 'the $s';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 초 단위 시간을 사람이 읽기 쉬운 형태로 변환 (원본 Main.pas:1265-1271)
|
||||||
|
/// l10n 지원
|
||||||
|
String roughTime(int seconds) {
|
||||||
|
if (seconds < 120) {
|
||||||
|
return l10n.roughTimeSeconds(seconds);
|
||||||
|
} else if (seconds < 60 * 120) {
|
||||||
|
return l10n.roughTimeMinutes(seconds ~/ 60);
|
||||||
|
} else if (seconds < 60 * 60 * 48) {
|
||||||
|
return l10n.roughTimeHours(seconds ~/ 3600);
|
||||||
|
} else {
|
||||||
|
return l10n.roughTimeDays(seconds ~/ (3600 * 24));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 문자열이 특정 접미사로 끝나는지 확인
|
||||||
|
bool _ends(String s, String suffix) {
|
||||||
|
return s.length >= suffix.length &&
|
||||||
|
s.substring(s.length - suffix.length) == suffix;
|
||||||
|
}
|
||||||
97
lib/src/core/util/pq_task.dart
Normal file
97
lib/src/core/util/pq_task.dart
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
|
|
||||||
|
/// 태스크/큐/진행 관련 함수들
|
||||||
|
/// 원본 Main.pas의 태스크 처리 로직을 포팅
|
||||||
|
|
||||||
|
/// 레벨업에 필요한 시간 (초)
|
||||||
|
int levelUpTimeSeconds(int level) {
|
||||||
|
// Act 진행과 레벨업 동기화 (10시간 완주 목표)
|
||||||
|
if (level <= 20) {
|
||||||
|
// Act I: 레벨 1-20, 2시간 (7200초) → 평균 360초/레벨
|
||||||
|
return 300 + (level * 6);
|
||||||
|
} else if (level <= 40) {
|
||||||
|
// Act II: 레벨 21-40, 3시간 (10800초) → 평균 540초/레벨
|
||||||
|
return 400 + ((level - 20) * 10);
|
||||||
|
} else if (level <= 60) {
|
||||||
|
// Act III: 레벨 41-60, 3시간 (10800초) → 평균 540초/레벨
|
||||||
|
return 400 + ((level - 40) * 10);
|
||||||
|
} else if (level <= 80) {
|
||||||
|
// Act IV: 레벨 61-80, 1.5시간 (5400초) → 평균 270초/레벨
|
||||||
|
return 200 + ((level - 60) * 5);
|
||||||
|
} else {
|
||||||
|
// Act V: 레벨 81-100, 30분 (1800초) → 평균 90초/레벨
|
||||||
|
return 60 + ((level - 80) * 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// levelUpTimeSeconds의 별칭 (호환성 유지)
|
||||||
|
int levelUpTime(int level) => levelUpTimeSeconds(level);
|
||||||
|
|
||||||
|
/// 태스크 결과
|
||||||
|
class TaskResult {
|
||||||
|
const TaskResult({
|
||||||
|
required this.caption,
|
||||||
|
required this.durationMillis,
|
||||||
|
required this.progress,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String caption;
|
||||||
|
final int durationMillis;
|
||||||
|
final ProgressState progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 태스크 시작: 태스크 바 리셋 및 캡션 설정
|
||||||
|
TaskResult startTask(
|
||||||
|
ProgressState progress,
|
||||||
|
String caption,
|
||||||
|
int durationMillis,
|
||||||
|
) {
|
||||||
|
final updated = progress.copyWith(
|
||||||
|
task: ProgressBarState(position: 0, max: durationMillis),
|
||||||
|
);
|
||||||
|
return TaskResult(
|
||||||
|
caption: '$caption...',
|
||||||
|
durationMillis: durationMillis,
|
||||||
|
progress: updated,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 큐 처리 결과
|
||||||
|
class DequeueResult {
|
||||||
|
const DequeueResult({
|
||||||
|
required this.progress,
|
||||||
|
required this.queue,
|
||||||
|
required this.caption,
|
||||||
|
required this.taskType,
|
||||||
|
required this.kind,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ProgressState progress;
|
||||||
|
final QueueState queue;
|
||||||
|
final String caption;
|
||||||
|
final TaskType taskType;
|
||||||
|
final QueueKind kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 큐에서 다음 태스크 꺼내기
|
||||||
|
/// 현재 태스크가 완료되면 큐에서 다음 태스크를 가져옴
|
||||||
|
DequeueResult? dequeue(ProgressState progress, QueueState queue) {
|
||||||
|
// 태스크 바가 완료되지 않으면 null 반환
|
||||||
|
if (progress.task.position < progress.task.max) return null;
|
||||||
|
if (queue.entries.isEmpty) return null;
|
||||||
|
|
||||||
|
final entries = Queue<QueueEntry>.from(queue.entries);
|
||||||
|
if (entries.isEmpty) return null;
|
||||||
|
|
||||||
|
final next = entries.removeFirst();
|
||||||
|
final taskResult = startTask(progress, next.caption, next.durationMillis);
|
||||||
|
return DequeueResult(
|
||||||
|
progress: taskResult.progress,
|
||||||
|
queue: QueueState(entries: entries.toList()),
|
||||||
|
caption: taskResult.caption,
|
||||||
|
taskType: next.taskType,
|
||||||
|
kind: next.kind,
|
||||||
|
);
|
||||||
|
}
|
||||||
184
lib/src/features/game/controllers/combat_log_controller.dart
Normal file
184
lib/src/features/game/controllers/combat_log_controller.dart
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||||
|
import 'package:asciineverdie/src/core/model/combat_event.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/combat_state.dart';
|
||||||
|
import 'package:asciineverdie/src/features/game/widgets/combat_log.dart';
|
||||||
|
|
||||||
|
/// 전투 로그 컨트롤러
|
||||||
|
///
|
||||||
|
/// GamePlayScreen에서 추출된 전투 로그 관련 로직 담당:
|
||||||
|
/// - 로그 엔트리 관리 (최대 50개 유지)
|
||||||
|
/// - 전투 이벤트 → 로그 메시지 변환
|
||||||
|
/// - 태스크 변경 시 로그 추가
|
||||||
|
class CombatLogController extends ChangeNotifier {
|
||||||
|
CombatLogController({
|
||||||
|
this.onCombatEvent,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 전투 이벤트 발생 시 호출되는 콜백 (SFX 재생 등에 사용)
|
||||||
|
final void Function(CombatEvent event)? onCombatEvent;
|
||||||
|
|
||||||
|
// 로그 엔트리 목록
|
||||||
|
final List<CombatLogEntry> _entries = [];
|
||||||
|
|
||||||
|
// 이벤트 처리 추적
|
||||||
|
int _lastProcessedEventCount = 0;
|
||||||
|
String _lastTaskCaption = '';
|
||||||
|
|
||||||
|
/// 로그 엔트리 목록 (읽기 전용)
|
||||||
|
List<CombatLogEntry> get entries => List.unmodifiable(_entries);
|
||||||
|
|
||||||
|
/// 로그 엔트리 추가
|
||||||
|
void addLog(String message, CombatLogType type) {
|
||||||
|
_entries.add(
|
||||||
|
CombatLogEntry(message: message, timestamp: DateTime.now(), type: type),
|
||||||
|
);
|
||||||
|
// 최대 50개 유지
|
||||||
|
if (_entries.length > 50) {
|
||||||
|
_entries.removeAt(0);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 태스크 변경 처리
|
||||||
|
///
|
||||||
|
/// 새 태스크 시작 시 로그에 추가하고 이벤트 카운터 리셋
|
||||||
|
void onTaskChanged(String caption) {
|
||||||
|
if (caption.isNotEmpty && caption != _lastTaskCaption) {
|
||||||
|
addLog(caption, CombatLogType.normal);
|
||||||
|
_lastTaskCaption = caption;
|
||||||
|
// 새 태스크 시작 시 이벤트 카운터 리셋
|
||||||
|
_lastProcessedEventCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 전투 이벤트 처리
|
||||||
|
///
|
||||||
|
/// 새 전투 이벤트를 로그로 변환하고 콜백 호출
|
||||||
|
void processCombatEvents(CombatState? combat) {
|
||||||
|
if (combat == null || !combat.isActive) {
|
||||||
|
_lastProcessedEventCount = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final events = combat.recentEvents;
|
||||||
|
if (events.isEmpty || events.length <= _lastProcessedEventCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 새 이벤트만 처리
|
||||||
|
final newEvents = events.skip(_lastProcessedEventCount);
|
||||||
|
for (final event in newEvents) {
|
||||||
|
final (message, type) = _formatCombatEvent(event);
|
||||||
|
addLog(message, type);
|
||||||
|
|
||||||
|
// 오디오 콜백 호출 (SFX 재생)
|
||||||
|
onCombatEvent?.call(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastProcessedEventCount = events.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 레벨업 로그 추가
|
||||||
|
void addLevelUpLog(int level) {
|
||||||
|
addLog(
|
||||||
|
'${game_l10n.uiLevelUp} Lv.$level',
|
||||||
|
CombatLogType.levelUp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 퀘스트 완료 로그 추가
|
||||||
|
void addQuestCompleteLog(String questCaption) {
|
||||||
|
addLog(
|
||||||
|
game_l10n.uiQuestComplete(questCaption),
|
||||||
|
CombatLogType.questComplete,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 전투 이벤트를 메시지와 타입으로 변환
|
||||||
|
(String, CombatLogType) _formatCombatEvent(CombatEvent event) {
|
||||||
|
final target = event.targetName ?? '';
|
||||||
|
// 스킬/포션 이름 번역 (전역 로케일 사용)
|
||||||
|
final skillName = event.skillName != null
|
||||||
|
? game_l10n.translateSpell(event.skillName!)
|
||||||
|
: '';
|
||||||
|
return switch (event.type) {
|
||||||
|
CombatEventType.playerAttack =>
|
||||||
|
event.isCritical
|
||||||
|
? (
|
||||||
|
game_l10n.combatCritical(event.damage, target),
|
||||||
|
CombatLogType.critical,
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
game_l10n.combatYouHit(target, event.damage),
|
||||||
|
CombatLogType.damage,
|
||||||
|
),
|
||||||
|
CombatEventType.monsterAttack => (
|
||||||
|
game_l10n.combatMonsterHitsYou(target, event.damage),
|
||||||
|
CombatLogType.monsterAttack,
|
||||||
|
),
|
||||||
|
CombatEventType.playerEvade => (
|
||||||
|
game_l10n.combatYouEvaded(target),
|
||||||
|
CombatLogType.evade,
|
||||||
|
),
|
||||||
|
CombatEventType.monsterEvade => (
|
||||||
|
game_l10n.combatMonsterEvaded(target),
|
||||||
|
CombatLogType.evade,
|
||||||
|
),
|
||||||
|
CombatEventType.playerBlock => (
|
||||||
|
game_l10n.combatBlocked(event.damage),
|
||||||
|
CombatLogType.block,
|
||||||
|
),
|
||||||
|
CombatEventType.playerParry => (
|
||||||
|
game_l10n.combatParried(event.damage),
|
||||||
|
CombatLogType.parry,
|
||||||
|
),
|
||||||
|
CombatEventType.playerSkill =>
|
||||||
|
event.isCritical
|
||||||
|
? (
|
||||||
|
game_l10n.combatSkillCritical(skillName, event.damage),
|
||||||
|
CombatLogType.critical,
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
game_l10n.combatSkillDamage(skillName, event.damage),
|
||||||
|
CombatLogType.skill,
|
||||||
|
),
|
||||||
|
CombatEventType.playerHeal => (
|
||||||
|
game_l10n.combatSkillHeal(
|
||||||
|
skillName.isNotEmpty ? skillName : game_l10n.uiHeal,
|
||||||
|
event.healAmount,
|
||||||
|
),
|
||||||
|
CombatLogType.heal,
|
||||||
|
),
|
||||||
|
CombatEventType.playerBuff => (
|
||||||
|
game_l10n.combatBuffActivated(skillName),
|
||||||
|
CombatLogType.buff,
|
||||||
|
),
|
||||||
|
CombatEventType.playerDebuff => (
|
||||||
|
game_l10n.combatDebuffApplied(skillName, target),
|
||||||
|
CombatLogType.debuff,
|
||||||
|
),
|
||||||
|
CombatEventType.dotTick => (
|
||||||
|
game_l10n.combatDotTick(skillName, event.damage),
|
||||||
|
CombatLogType.dotTick,
|
||||||
|
),
|
||||||
|
CombatEventType.playerPotion => (
|
||||||
|
game_l10n.combatPotionUsed(skillName, event.healAmount, target),
|
||||||
|
CombatLogType.potion,
|
||||||
|
),
|
||||||
|
CombatEventType.potionDrop => (
|
||||||
|
game_l10n.combatPotionDrop(skillName),
|
||||||
|
CombatLogType.potionDrop,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 로그 초기화
|
||||||
|
void reset() {
|
||||||
|
_entries.clear();
|
||||||
|
_lastProcessedEventCount = 0;
|
||||||
|
_lastTaskCaption = '';
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
251
lib/src/features/game/controllers/game_audio_controller.dart
Normal file
251
lib/src/features/game/controllers/game_audio_controller.dart
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/data/story_data.dart';
|
||||||
|
import 'package:asciineverdie/src/core/audio/audio_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/combat_event.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||||
|
|
||||||
|
/// 게임 오디오 컨트롤러
|
||||||
|
///
|
||||||
|
/// GamePlayScreen에서 추출된 오디오 관련 로직 담당:
|
||||||
|
/// - BGM 전환 (전투/마을/사망/엔딩)
|
||||||
|
/// - 전투 이벤트 SFX 재생
|
||||||
|
/// - 볼륨 관리
|
||||||
|
class GameAudioController extends ChangeNotifier {
|
||||||
|
GameAudioController({
|
||||||
|
required this.audioService,
|
||||||
|
this.getSpeedMultiplier,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AudioService? audioService;
|
||||||
|
|
||||||
|
/// 현재 배속 값을 가져오는 콜백 (디바운스 계산용)
|
||||||
|
final int Function()? getSpeedMultiplier;
|
||||||
|
|
||||||
|
// 오디오 상태 추적
|
||||||
|
bool _wasInBattleTask = false;
|
||||||
|
bool _wasDead = false;
|
||||||
|
bool _wasComplete = false;
|
||||||
|
|
||||||
|
// 볼륨 상태
|
||||||
|
double _bgmVolume = 0.7;
|
||||||
|
double _sfxVolume = 0.8;
|
||||||
|
|
||||||
|
// SFX 디바운스 추적
|
||||||
|
final Map<String, int> _lastSfxPlayTime = {};
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
double get bgmVolume => _bgmVolume;
|
||||||
|
double get sfxVolume => _sfxVolume;
|
||||||
|
|
||||||
|
/// 오디오 볼륨 초기화 (AudioService에서 로드)
|
||||||
|
Future<void> initVolumes() async {
|
||||||
|
final audio = audioService;
|
||||||
|
if (audio != null) {
|
||||||
|
_bgmVolume = audio.bgmVolume;
|
||||||
|
_sfxVolume = audio.sfxVolume;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BGM 볼륨 설정
|
||||||
|
void setBgmVolume(double volume) {
|
||||||
|
_bgmVolume = volume;
|
||||||
|
audioService?.setBgmVolume(volume);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SFX 볼륨 설정
|
||||||
|
void setSfxVolume(double volume) {
|
||||||
|
_sfxVolume = volume;
|
||||||
|
audioService?.setSfxVolume(volume);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 일시정지
|
||||||
|
void pauseAll() {
|
||||||
|
audioService?.pauseAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 재개
|
||||||
|
void resumeAll() {
|
||||||
|
audioService?.resumeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 초기 BGM 재생 (게임 시작/로드 시)
|
||||||
|
void playInitialBgm(GameState state) {
|
||||||
|
final audio = audioService;
|
||||||
|
if (audio == null) return;
|
||||||
|
|
||||||
|
final taskType = state.progress.currentTask.type;
|
||||||
|
final isInBattleTask = taskType == TaskType.kill;
|
||||||
|
|
||||||
|
if (isInBattleTask) {
|
||||||
|
audio.playBgm(_getBattleBgm(state));
|
||||||
|
} else {
|
||||||
|
// 비전투 태스크: 마을 BGM
|
||||||
|
audio.playBgm('town');
|
||||||
|
}
|
||||||
|
|
||||||
|
_wasInBattleTask = isInBattleTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TaskType 기반 BGM 전환 (애니메이션과 동기화)
|
||||||
|
void updateBgmForTaskType(GameState state) {
|
||||||
|
final audio = audioService;
|
||||||
|
if (audio == null) return;
|
||||||
|
|
||||||
|
final taskType = state.progress.currentTask.type;
|
||||||
|
final isInBattleTask = taskType == TaskType.kill;
|
||||||
|
|
||||||
|
if (isInBattleTask) {
|
||||||
|
final expectedBgm = _getBattleBgm(state);
|
||||||
|
|
||||||
|
// 전환 시점이거나 현재 BGM이 일치하지 않으면 재생
|
||||||
|
if (!_wasInBattleTask || audio.currentBgm != expectedBgm) {
|
||||||
|
audio.playBgm(expectedBgm);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 비전투 태스크: 항상 마을 BGM 유지 (이미 town이면 스킵)
|
||||||
|
if (audio.currentBgm != 'town') {
|
||||||
|
audio.playBgm('town');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_wasInBattleTask = isInBattleTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 사망/엔딩 BGM 전환 처리
|
||||||
|
void updateDeathEndingBgm(GameState state, {required bool isGameComplete}) {
|
||||||
|
final audio = audioService;
|
||||||
|
if (audio == null) return;
|
||||||
|
|
||||||
|
final isDead = state.isDead;
|
||||||
|
|
||||||
|
// 엔딩 BGM (게임 클리어 시)
|
||||||
|
if (isGameComplete && !_wasComplete) {
|
||||||
|
audio.playBgm('ending');
|
||||||
|
_wasComplete = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사망 BGM (isDead 상태 진입 시)
|
||||||
|
if (isDead && !_wasDead) {
|
||||||
|
audio.playBgm('death');
|
||||||
|
_wasDead = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 부활 시 사망 상태 리셋 (다음 사망 감지 가능하도록)
|
||||||
|
if (!isDead && _wasDead) {
|
||||||
|
_wasDead = false;
|
||||||
|
// 부활 후 BGM은 updateBgmForTaskType에서 처리됨
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 시네마틱 BGM 재생
|
||||||
|
void playCinematicBgm() {
|
||||||
|
audioService?.playBgm('act_cinemetic');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 레벨업 SFX 재생
|
||||||
|
void playLevelUpSfx() {
|
||||||
|
audioService?.playPlayerSfx('level_up');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 퀘스트 완료 SFX 재생
|
||||||
|
void playQuestCompleteSfx() {
|
||||||
|
audioService?.playPlayerSfx('quest_complete');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 전투 이벤트별 SFX 재생 (채널 분리 + 디바운스)
|
||||||
|
void playCombatEventSfx(CombatEvent event) {
|
||||||
|
final audio = audioService;
|
||||||
|
if (audio == null) return;
|
||||||
|
|
||||||
|
// 사운드 이름 결정
|
||||||
|
final sfxName = switch (event.type) {
|
||||||
|
CombatEventType.playerAttack => 'attack',
|
||||||
|
CombatEventType.playerSkill => 'skill',
|
||||||
|
CombatEventType.playerHeal => 'item',
|
||||||
|
CombatEventType.playerPotion => 'item',
|
||||||
|
CombatEventType.potionDrop => 'item',
|
||||||
|
CombatEventType.playerBuff => 'skill',
|
||||||
|
CombatEventType.playerDebuff => 'skill',
|
||||||
|
CombatEventType.monsterAttack => 'hit',
|
||||||
|
CombatEventType.playerEvade => 'evade',
|
||||||
|
CombatEventType.monsterEvade => 'evade',
|
||||||
|
CombatEventType.playerBlock => 'block',
|
||||||
|
CombatEventType.playerParry => 'parry',
|
||||||
|
CombatEventType.dotTick => null, // DOT 틱은 SFX 없음
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sfxName == null) return;
|
||||||
|
|
||||||
|
// 디바운스 체크 (배속 시 같은 사운드 중복 재생 방지)
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
final lastTime = _lastSfxPlayTime[sfxName] ?? 0;
|
||||||
|
final speedMultiplier = getSpeedMultiplier?.call() ?? 1;
|
||||||
|
|
||||||
|
// 배속이 높을수록 디바운스 간격 증가 (1x=50ms, 8x=150ms)
|
||||||
|
final debounceMs = 50 + (speedMultiplier - 1) * 15;
|
||||||
|
|
||||||
|
if (now - lastTime < debounceMs) {
|
||||||
|
return; // 디바운스 기간 내 → 스킵
|
||||||
|
}
|
||||||
|
_lastSfxPlayTime[sfxName] = now;
|
||||||
|
|
||||||
|
// 채널별 재생
|
||||||
|
final isMonsterSfx = event.type == CombatEventType.monsterAttack;
|
||||||
|
if (isMonsterSfx) {
|
||||||
|
audio.playMonsterSfx(sfxName);
|
||||||
|
} else {
|
||||||
|
audio.playPlayerSfx(sfxName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 전투 BGM 결정 (몬스터 등급 + 레벨 + Act 고려)
|
||||||
|
String _getBattleBgm(GameState state) {
|
||||||
|
final task = state.progress.currentTask;
|
||||||
|
final monsterGrade = task.monsterGrade;
|
||||||
|
final monsterLevel = task.monsterLevel ?? 0;
|
||||||
|
final playerLevel = state.traits.level;
|
||||||
|
|
||||||
|
// 1. 등급 보스 (3% 확률로 등장하는 특수 보스)
|
||||||
|
if (monsterGrade == MonsterGrade.boss) {
|
||||||
|
return 'act_boss';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 레벨 기반 보스 (강적)
|
||||||
|
if (monsterLevel >= playerLevel + 5) {
|
||||||
|
return 'boss';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 엘리트 몬스터 (12% 확률)
|
||||||
|
if (monsterGrade == MonsterGrade.elite) {
|
||||||
|
return 'elite';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 일반 전투 (Act별 분기)
|
||||||
|
return _getBattleBgmForLevel(playerLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 레벨에 따른 전투 BGM 파일명 반환 (Act별 분기)
|
||||||
|
String _getBattleBgmForLevel(int playerLevel) {
|
||||||
|
final act = getActForLevel(playerLevel);
|
||||||
|
return switch (act) {
|
||||||
|
StoryAct.act4 => 'battle_act4',
|
||||||
|
StoryAct.act5 => 'battle_act5',
|
||||||
|
_ => 'battle',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 상태 리셋 (새 게임 시작 시)
|
||||||
|
void reset() {
|
||||||
|
_wasInBattleTask = false;
|
||||||
|
_wasDead = false;
|
||||||
|
_wasComplete = false;
|
||||||
|
_lastSfxPlayTime.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,10 +10,8 @@ import 'package:asciineverdie/data/story_data.dart';
|
|||||||
import 'package:asciineverdie/l10n/app_localizations.dart';
|
import 'package:asciineverdie/l10n/app_localizations.dart';
|
||||||
import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
|
import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/story_service.dart';
|
import 'package:asciineverdie/src/core/engine/story_service.dart';
|
||||||
import 'package:asciineverdie/src/core/model/combat_event.dart';
|
|
||||||
import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart';
|
import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart';
|
||||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
|
||||||
import 'package:asciineverdie/src/core/model/skill.dart';
|
import 'package:asciineverdie/src/core/model/skill.dart';
|
||||||
import 'package:asciineverdie/src/core/notification/notification_service.dart';
|
import 'package:asciineverdie/src/core/notification/notification_service.dart';
|
||||||
import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||||
@@ -36,6 +34,8 @@ import 'package:asciineverdie/src/features/game/widgets/statistics_dialog.dart';
|
|||||||
import 'package:asciineverdie/src/features/game/widgets/help_dialog.dart';
|
import 'package:asciineverdie/src/features/game/widgets/help_dialog.dart';
|
||||||
import 'package:asciineverdie/src/core/storage/settings_repository.dart';
|
import 'package:asciineverdie/src/core/storage/settings_repository.dart';
|
||||||
import 'package:asciineverdie/src/core/audio/audio_service.dart';
|
import 'package:asciineverdie/src/core/audio/audio_service.dart';
|
||||||
|
import 'package:asciineverdie/src/features/game/controllers/combat_log_controller.dart';
|
||||||
|
import 'package:asciineverdie/src/features/game/controllers/game_audio_controller.dart';
|
||||||
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
|
||||||
/// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃)
|
/// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃)
|
||||||
@@ -85,58 +85,34 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
StoryAct _lastAct = StoryAct.prologue;
|
StoryAct _lastAct = StoryAct.prologue;
|
||||||
bool _showingCinematic = false;
|
bool _showingCinematic = false;
|
||||||
|
|
||||||
// Phase 8: 전투 로그 (Combat Log)
|
|
||||||
final List<CombatLogEntry> _combatLogEntries = [];
|
|
||||||
String _lastTaskCaption = '';
|
|
||||||
|
|
||||||
// 이전 상태 추적 (레벨업/퀘스트/Act 완료 감지용)
|
// 이전 상태 추적 (레벨업/퀘스트/Act 완료 감지용)
|
||||||
int _lastLevel = 0;
|
int _lastLevel = 0;
|
||||||
int _lastQuestCount = 0;
|
int _lastQuestCount = 0;
|
||||||
int _lastPlotStageCount = 0;
|
int _lastPlotStageCount = 0;
|
||||||
|
|
||||||
// 전투 이벤트 추적 (마지막 처리된 이벤트 수)
|
// Phase 2.4: 오디오 컨트롤러
|
||||||
int _lastProcessedEventCount = 0;
|
late final GameAudioController _audioController;
|
||||||
|
|
||||||
// 오디오 상태 추적 (TaskType 기반)
|
// Phase 2.5: 전투 로그 컨트롤러
|
||||||
bool _wasInBattleTask = false;
|
late final CombatLogController _combatLogController;
|
||||||
|
|
||||||
// 사망/엔딩 상태 추적 (BGM 전환용)
|
|
||||||
bool _wasDead = false;
|
|
||||||
|
|
||||||
// 사운드 디바운스 추적 (배속 시 사운드 누락 방지)
|
|
||||||
final Map<String, int> _lastSfxPlayTime = {};
|
|
||||||
bool _wasComplete = false;
|
|
||||||
|
|
||||||
// 사운드 볼륨 상태 (모바일 설정 UI용)
|
|
||||||
double _bgmVolume = 0.7;
|
|
||||||
double _sfxVolume = 0.8;
|
|
||||||
|
|
||||||
void _checkSpecialEvents(GameState state) {
|
void _checkSpecialEvents(GameState state) {
|
||||||
// Phase 8: 태스크 변경 시 로그 추가
|
// Phase 8: 태스크 변경 시 로그 추가
|
||||||
final currentCaption = state.progress.currentTask.caption;
|
_combatLogController.onTaskChanged(state.progress.currentTask.caption);
|
||||||
if (currentCaption.isNotEmpty && currentCaption != _lastTaskCaption) {
|
|
||||||
_addCombatLog(currentCaption, CombatLogType.normal);
|
|
||||||
_lastTaskCaption = currentCaption;
|
|
||||||
// 새 태스크 시작 시 이벤트 카운터 리셋
|
|
||||||
_lastProcessedEventCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 전투 이벤트 처리 (Combat Events)
|
// 전투 이벤트 처리 (Combat Events)
|
||||||
_processCombatEvents(state);
|
_combatLogController.processCombatEvents(state.progress.currentCombat);
|
||||||
|
|
||||||
// 오디오: TaskType 변경 시 BGM 전환 (애니메이션과 동기화)
|
// 오디오: TaskType 변경 시 BGM 전환 (애니메이션과 동기화)
|
||||||
_updateBgmForTaskType(state);
|
_audioController.updateBgmForTaskType(state);
|
||||||
|
|
||||||
// 레벨업 감지
|
// 레벨업 감지
|
||||||
if (state.traits.level > _lastLevel && _lastLevel > 0) {
|
if (state.traits.level > _lastLevel && _lastLevel > 0) {
|
||||||
_specialAnimation = AsciiAnimationType.levelUp;
|
_specialAnimation = AsciiAnimationType.levelUp;
|
||||||
_notificationService.showLevelUp(state.traits.level);
|
_notificationService.showLevelUp(state.traits.level);
|
||||||
_addCombatLog(
|
_combatLogController.addLevelUpLog(state.traits.level);
|
||||||
'${game_l10n.uiLevelUp} Lv.${state.traits.level}',
|
|
||||||
CombatLogType.levelUp,
|
|
||||||
);
|
|
||||||
// 오디오: 레벨업 SFX (플레이어 채널)
|
// 오디오: 레벨업 SFX (플레이어 채널)
|
||||||
widget.audioService?.playPlayerSfx('level_up');
|
_audioController.playLevelUpSfx();
|
||||||
_resetSpecialAnimationAfterFrame();
|
_resetSpecialAnimationAfterFrame();
|
||||||
|
|
||||||
// Phase 9: Act 변경 감지 (레벨 기반)
|
// Phase 9: Act 변경 감지 (레벨 기반)
|
||||||
@@ -164,13 +140,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
.lastOrNull;
|
.lastOrNull;
|
||||||
if (completedQuest != null) {
|
if (completedQuest != null) {
|
||||||
_notificationService.showQuestComplete(completedQuest.caption);
|
_notificationService.showQuestComplete(completedQuest.caption);
|
||||||
_addCombatLog(
|
_combatLogController.addQuestCompleteLog(completedQuest.caption);
|
||||||
game_l10n.uiQuestComplete(completedQuest.caption),
|
|
||||||
CombatLogType.questComplete,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// 오디오: 퀘스트 완료 SFX (플레이어 채널)
|
// 오디오: 퀘스트 완료 SFX (플레이어 채널)
|
||||||
widget.audioService?.playPlayerSfx('quest_complete');
|
_audioController.playQuestCompleteSfx();
|
||||||
_resetSpecialAnimationAfterFrame();
|
_resetSpecialAnimationAfterFrame();
|
||||||
}
|
}
|
||||||
_lastQuestCount = state.progress.questCount;
|
_lastQuestCount = state.progress.questCount;
|
||||||
@@ -187,292 +160,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
_lastPlotStageCount = state.progress.plotStageCount;
|
_lastPlotStageCount = state.progress.plotStageCount;
|
||||||
|
|
||||||
// 사망/엔딩 BGM 전환 (Death/Ending BGM Transition)
|
// 사망/엔딩 BGM 전환 (Death/Ending BGM Transition)
|
||||||
_updateDeathEndingBgm(state);
|
_audioController.updateDeathEndingBgm(
|
||||||
}
|
state,
|
||||||
|
isGameComplete: widget.controller.isComplete,
|
||||||
/// 사망/엔딩 BGM 전환 처리
|
|
||||||
void _updateDeathEndingBgm(GameState state) {
|
|
||||||
final audio = widget.audioService;
|
|
||||||
if (audio == null) return;
|
|
||||||
|
|
||||||
final isDead = state.isDead;
|
|
||||||
final isComplete = widget.controller.isComplete;
|
|
||||||
|
|
||||||
// 엔딩 BGM (게임 클리어 시)
|
|
||||||
if (isComplete && !_wasComplete) {
|
|
||||||
audio.playBgm('ending');
|
|
||||||
_wasComplete = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 사망 BGM (isDead 상태 진입 시)
|
|
||||||
if (isDead && !_wasDead) {
|
|
||||||
audio.playBgm('death');
|
|
||||||
_wasDead = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 부활 시 사망 상태 리셋 (다음 사망 감지 가능하도록)
|
|
||||||
if (!isDead && _wasDead) {
|
|
||||||
_wasDead = false;
|
|
||||||
// 부활 후 BGM은 _updateBgmForTaskType에서 처리됨
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Phase 8: 전투 로그 추가 (Add Combat Log Entry)
|
|
||||||
void _addCombatLog(String message, CombatLogType type) {
|
|
||||||
_combatLogEntries.add(
|
|
||||||
CombatLogEntry(message: message, timestamp: DateTime.now(), type: type),
|
|
||||||
);
|
);
|
||||||
// 최대 50개 유지
|
|
||||||
if (_combatLogEntries.length > 50) {
|
|
||||||
_combatLogEntries.removeAt(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 전투 이벤트를 로그로 변환 (Convert Combat Events to Log)
|
|
||||||
void _processCombatEvents(GameState state) {
|
|
||||||
final combat = state.progress.currentCombat;
|
|
||||||
if (combat == null || !combat.isActive) {
|
|
||||||
_lastProcessedEventCount = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final events = combat.recentEvents;
|
|
||||||
if (events.isEmpty || events.length <= _lastProcessedEventCount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 새 이벤트만 처리
|
|
||||||
final newEvents = events.skip(_lastProcessedEventCount);
|
|
||||||
for (final event in newEvents) {
|
|
||||||
final (message, type) = _formatCombatEvent(event);
|
|
||||||
_addCombatLog(message, type);
|
|
||||||
|
|
||||||
// 오디오: 전투 이벤트에 따른 SFX 재생
|
|
||||||
_playCombatEventSfx(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastProcessedEventCount = events.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 초기 BGM 재생 (게임 시작/로드 시)
|
|
||||||
///
|
|
||||||
/// TaskType 기반으로 BGM 결정 (애니메이션과 동기화)
|
|
||||||
void _playInitialBgm(GameState state) {
|
|
||||||
final audio = widget.audioService;
|
|
||||||
if (audio == null) return;
|
|
||||||
|
|
||||||
final taskType = state.progress.currentTask.type;
|
|
||||||
final isInBattleTask = taskType == TaskType.kill;
|
|
||||||
|
|
||||||
if (isInBattleTask) {
|
|
||||||
audio.playBgm(_getBattleBgm(state));
|
|
||||||
} else {
|
|
||||||
// 비전투 태스크: 마을 BGM
|
|
||||||
audio.playBgm('town');
|
|
||||||
}
|
|
||||||
|
|
||||||
_wasInBattleTask = isInBattleTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 전투 BGM 결정 (몬스터 등급 + 레벨 + Act 고려)
|
|
||||||
///
|
|
||||||
/// 우선순위:
|
|
||||||
/// 1. MonsterGrade.boss → 'act_boss'
|
|
||||||
/// 2. 레벨 기반 보스 (monsterLevel >= playerLevel + 5) → 'boss'
|
|
||||||
/// 3. MonsterGrade.elite → 'elite'
|
|
||||||
/// 4. Act별 일반 전투 → 'battle', 'battle_act4', 'battle_act5'
|
|
||||||
String _getBattleBgm(GameState state) {
|
|
||||||
final task = state.progress.currentTask;
|
|
||||||
final monsterGrade = task.monsterGrade;
|
|
||||||
final monsterLevel = task.monsterLevel ?? 0;
|
|
||||||
final playerLevel = state.traits.level;
|
|
||||||
|
|
||||||
// 1. 등급 보스 (3% 확률로 등장하는 특수 보스)
|
|
||||||
if (monsterGrade == MonsterGrade.boss) {
|
|
||||||
return 'act_boss';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 레벨 기반 보스 (강적)
|
|
||||||
if (monsterLevel >= playerLevel + 5) {
|
|
||||||
return 'boss';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 엘리트 몬스터 (12% 확률)
|
|
||||||
if (monsterGrade == MonsterGrade.elite) {
|
|
||||||
return 'elite';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 일반 전투 (Act별 분기)
|
|
||||||
return _getBattleBgmForLevel(playerLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 레벨에 따른 전투 BGM 파일명 반환 (Act별 분기)
|
|
||||||
String _getBattleBgmForLevel(int playerLevel) {
|
|
||||||
final act = getActForLevel(playerLevel);
|
|
||||||
return switch (act) {
|
|
||||||
StoryAct.act4 => 'battle_act4',
|
|
||||||
StoryAct.act5 => 'battle_act5',
|
|
||||||
_ => 'battle',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// TaskType 기반 BGM 전환 (애니메이션과 동기화)
|
|
||||||
///
|
|
||||||
/// 애니메이션은 TaskType으로 결정되므로, BGM도 동일한 기준 사용
|
|
||||||
/// 전환 감지 외에도 현재 BGM이 TaskType과 일치하는지 검증
|
|
||||||
void _updateBgmForTaskType(GameState state) {
|
|
||||||
final audio = widget.audioService;
|
|
||||||
if (audio == null) return;
|
|
||||||
|
|
||||||
final taskType = state.progress.currentTask.type;
|
|
||||||
final isInBattleTask = taskType == TaskType.kill;
|
|
||||||
|
|
||||||
// 전투 태스크 상태 결정
|
|
||||||
if (isInBattleTask) {
|
|
||||||
final expectedBgm = _getBattleBgm(state);
|
|
||||||
|
|
||||||
// 전환 시점이거나 현재 BGM이 일치하지 않으면 재생
|
|
||||||
if (!_wasInBattleTask || audio.currentBgm != expectedBgm) {
|
|
||||||
audio.playBgm(expectedBgm);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 비전투 태스크: 항상 마을 BGM 유지 (이미 town이면 스킵)
|
|
||||||
if (audio.currentBgm != 'town') {
|
|
||||||
audio.playBgm('town');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_wasInBattleTask = isInBattleTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 전투 이벤트별 SFX 재생 (채널 분리 + 디바운스)
|
|
||||||
///
|
|
||||||
/// 플레이어 이펙트와 몬스터 이펙트를 별도 채널에서 재생하여
|
|
||||||
/// 사운드 충돌을 방지하고 완료를 보장합니다.
|
|
||||||
/// 배속 모드에서는 디바운스를 적용하여 사운드 누락을 방지합니다.
|
|
||||||
void _playCombatEventSfx(CombatEvent event) {
|
|
||||||
final audio = widget.audioService;
|
|
||||||
if (audio == null) return;
|
|
||||||
|
|
||||||
// 사운드 이름 결정
|
|
||||||
final sfxName = switch (event.type) {
|
|
||||||
CombatEventType.playerAttack => 'attack',
|
|
||||||
CombatEventType.playerSkill => 'skill',
|
|
||||||
CombatEventType.playerHeal => 'item',
|
|
||||||
CombatEventType.playerPotion => 'item',
|
|
||||||
CombatEventType.potionDrop => 'item',
|
|
||||||
CombatEventType.playerBuff => 'skill',
|
|
||||||
CombatEventType.playerDebuff => 'skill',
|
|
||||||
CombatEventType.monsterAttack => 'hit',
|
|
||||||
CombatEventType.playerEvade => 'evade',
|
|
||||||
CombatEventType.monsterEvade => 'evade',
|
|
||||||
CombatEventType.playerBlock => 'block',
|
|
||||||
CombatEventType.playerParry => 'parry',
|
|
||||||
CombatEventType.dotTick => null, // DOT 틱은 SFX 없음
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sfxName == null) return;
|
|
||||||
|
|
||||||
// 디바운스 체크 (배속 시 같은 사운드 100ms 내 중복 재생 방지)
|
|
||||||
final now = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
final lastTime = _lastSfxPlayTime[sfxName] ?? 0;
|
|
||||||
final speedMultiplier = widget.controller.loop?.speedMultiplier ?? 1;
|
|
||||||
|
|
||||||
// 배속이 높을수록 디바운스 간격 증가 (1x=50ms, 8x=150ms)
|
|
||||||
final debounceMs = 50 + (speedMultiplier - 1) * 15;
|
|
||||||
|
|
||||||
if (now - lastTime < debounceMs) {
|
|
||||||
return; // 디바운스 기간 내 → 스킵
|
|
||||||
}
|
|
||||||
_lastSfxPlayTime[sfxName] = now;
|
|
||||||
|
|
||||||
// 채널별 재생
|
|
||||||
final isMonsterSfx = event.type == CombatEventType.monsterAttack;
|
|
||||||
if (isMonsterSfx) {
|
|
||||||
audio.playMonsterSfx(sfxName);
|
|
||||||
} else {
|
|
||||||
audio.playPlayerSfx(sfxName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 전투 이벤트를 메시지와 타입으로 변환
|
|
||||||
(String, CombatLogType) _formatCombatEvent(CombatEvent event) {
|
|
||||||
final target = event.targetName ?? '';
|
|
||||||
// 스킬/포션 이름 번역 (전역 로케일 사용)
|
|
||||||
final skillName = event.skillName != null
|
|
||||||
? game_l10n.translateSpell(event.skillName!)
|
|
||||||
: '';
|
|
||||||
return switch (event.type) {
|
|
||||||
CombatEventType.playerAttack =>
|
|
||||||
event.isCritical
|
|
||||||
? (
|
|
||||||
game_l10n.combatCritical(event.damage, target),
|
|
||||||
CombatLogType.critical,
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
game_l10n.combatYouHit(target, event.damage),
|
|
||||||
CombatLogType.damage,
|
|
||||||
),
|
|
||||||
CombatEventType.monsterAttack => (
|
|
||||||
game_l10n.combatMonsterHitsYou(target, event.damage),
|
|
||||||
CombatLogType.monsterAttack,
|
|
||||||
),
|
|
||||||
CombatEventType.playerEvade => (
|
|
||||||
game_l10n.combatYouEvaded(target),
|
|
||||||
CombatLogType.evade,
|
|
||||||
),
|
|
||||||
CombatEventType.monsterEvade => (
|
|
||||||
game_l10n.combatMonsterEvaded(target),
|
|
||||||
CombatLogType.evade,
|
|
||||||
),
|
|
||||||
CombatEventType.playerBlock => (
|
|
||||||
game_l10n.combatBlocked(event.damage),
|
|
||||||
CombatLogType.block,
|
|
||||||
),
|
|
||||||
CombatEventType.playerParry => (
|
|
||||||
game_l10n.combatParried(event.damage),
|
|
||||||
CombatLogType.parry,
|
|
||||||
),
|
|
||||||
CombatEventType.playerSkill =>
|
|
||||||
event.isCritical
|
|
||||||
? (
|
|
||||||
game_l10n.combatSkillCritical(skillName, event.damage),
|
|
||||||
CombatLogType.critical,
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
game_l10n.combatSkillDamage(skillName, event.damage),
|
|
||||||
CombatLogType.skill,
|
|
||||||
),
|
|
||||||
CombatEventType.playerHeal => (
|
|
||||||
game_l10n.combatSkillHeal(
|
|
||||||
skillName.isNotEmpty ? skillName : game_l10n.uiHeal,
|
|
||||||
event.healAmount,
|
|
||||||
),
|
|
||||||
CombatLogType.heal,
|
|
||||||
),
|
|
||||||
CombatEventType.playerBuff => (
|
|
||||||
game_l10n.combatBuffActivated(skillName),
|
|
||||||
CombatLogType.buff,
|
|
||||||
),
|
|
||||||
CombatEventType.playerDebuff => (
|
|
||||||
game_l10n.combatDebuffApplied(skillName, target),
|
|
||||||
CombatLogType.debuff,
|
|
||||||
),
|
|
||||||
CombatEventType.dotTick => (
|
|
||||||
game_l10n.combatDotTick(skillName, event.damage),
|
|
||||||
CombatLogType.dotTick,
|
|
||||||
),
|
|
||||||
CombatEventType.playerPotion => (
|
|
||||||
game_l10n.combatPotionUsed(skillName, event.healAmount, target),
|
|
||||||
CombatLogType.potion,
|
|
||||||
),
|
|
||||||
CombatEventType.potionDrop => (
|
|
||||||
game_l10n.combatPotionDrop(skillName),
|
|
||||||
CombatLogType.potionDrop,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Phase 9: Act 시네마틱 표시 (Show Act Cinematic)
|
/// Phase 9: Act 시네마틱 표시 (Show Act Cinematic)
|
||||||
@@ -484,7 +175,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
await widget.controller.pause(saveOnStop: false);
|
await widget.controller.pause(saveOnStop: false);
|
||||||
|
|
||||||
// 시네마틱 BGM 재생
|
// 시네마틱 BGM 재생
|
||||||
widget.audioService?.playBgm('act_cinemetic');
|
_audioController.playCinematicBgm();
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
await showActCinematic(context, act);
|
await showActCinematic(context, act);
|
||||||
@@ -520,6 +211,18 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
super.initState();
|
super.initState();
|
||||||
_notificationService = NotificationService();
|
_notificationService = NotificationService();
|
||||||
_storyService = StoryService();
|
_storyService = StoryService();
|
||||||
|
|
||||||
|
// 오디오 컨트롤러 초기화
|
||||||
|
_audioController = GameAudioController(
|
||||||
|
audioService: widget.audioService,
|
||||||
|
getSpeedMultiplier: () => widget.controller.loop?.speedMultiplier ?? 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 전투 로그 컨트롤러 초기화
|
||||||
|
_combatLogController = CombatLogController(
|
||||||
|
onCombatEvent: (event) => _audioController.playCombatEventSfx(event),
|
||||||
|
);
|
||||||
|
|
||||||
widget.controller.addListener(_onControllerChanged);
|
widget.controller.addListener(_onControllerChanged);
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
@@ -531,8 +234,8 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
_lastPlotStageCount = state.progress.plotStageCount;
|
_lastPlotStageCount = state.progress.plotStageCount;
|
||||||
_lastAct = getActForLevel(state.traits.level);
|
_lastAct = getActForLevel(state.traits.level);
|
||||||
|
|
||||||
// 초기 BGM 재생 (TaskType 기반, _wasInBattleTask도 함께 설정)
|
// 초기 BGM 재생 (TaskType 기반)
|
||||||
_playInitialBgm(state);
|
_audioController.playInitialBgm(state);
|
||||||
} else {
|
} else {
|
||||||
// 상태가 없으면 기본 마을 BGM
|
// 상태가 없으면 기본 마을 BGM
|
||||||
widget.audioService?.playBgm('town');
|
widget.audioService?.playBgm('town');
|
||||||
@@ -542,17 +245,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
widget.controller.loadCumulativeStats();
|
widget.controller.loadCumulativeStats();
|
||||||
|
|
||||||
// 오디오 볼륨 초기화
|
// 오디오 볼륨 초기화
|
||||||
_initAudioVolumes();
|
_audioController.initVolumes();
|
||||||
}
|
|
||||||
|
|
||||||
/// 오디오 볼륨 초기화 (설정에서 로드)
|
|
||||||
Future<void> _initAudioVolumes() async {
|
|
||||||
final audio = widget.audioService;
|
|
||||||
if (audio != null) {
|
|
||||||
_bgmVolume = audio.bgmVolume;
|
|
||||||
_sfxVolume = audio.sfxVolume;
|
|
||||||
if (mounted) setState(() {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -592,13 +285,13 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
// 모바일: 게임 일시정지 + 전체 오디오 정지
|
// 모바일: 게임 일시정지 + 전체 오디오 정지
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
widget.controller.pause(saveOnStop: false);
|
widget.controller.pause(saveOnStop: false);
|
||||||
widget.audioService?.pauseAll();
|
_audioController.pauseAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 모바일: 앱이 포그라운드로 돌아올 때 전체 재로드
|
// 모바일: 앱이 포그라운드로 돌아올 때 전체 재로드
|
||||||
if (appState == AppLifecycleState.resumed && isMobile) {
|
if (appState == AppLifecycleState.resumed && isMobile) {
|
||||||
widget.audioService?.resumeAll();
|
_audioController.resumeAll();
|
||||||
_reloadGameScreen();
|
_reloadGameScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -745,12 +438,12 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onBgmVolumeChange: (volume) {
|
onBgmVolumeChange: (volume) {
|
||||||
setState(() => _bgmVolume = volume);
|
_audioController.setBgmVolume(volume);
|
||||||
widget.audioService?.setBgmVolume(volume);
|
setState(() {});
|
||||||
},
|
},
|
||||||
onSfxVolumeChange: (volume) {
|
onSfxVolumeChange: (volume) {
|
||||||
setState(() => _sfxVolume = volume);
|
_audioController.setSfxVolume(volume);
|
||||||
widget.audioService?.setSfxVolume(volume);
|
setState(() {});
|
||||||
},
|
},
|
||||||
onCreateTestCharacter: () async {
|
onCreateTestCharacter: () async {
|
||||||
final navigator = Navigator.of(context);
|
final navigator = Navigator.of(context);
|
||||||
@@ -824,7 +517,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
children: [
|
children: [
|
||||||
MobileCarouselLayout(
|
MobileCarouselLayout(
|
||||||
state: state,
|
state: state,
|
||||||
combatLogEntries: _combatLogEntries,
|
combatLogEntries: _combatLogController.entries,
|
||||||
speedMultiplier: widget.controller.loop?.speedMultiplier ?? 1,
|
speedMultiplier: widget.controller.loop?.speedMultiplier ?? 1,
|
||||||
onSpeedCycle: () {
|
onSpeedCycle: () {
|
||||||
widget.controller.loop?.cycleSpeed();
|
widget.controller.loop?.cycleSpeed();
|
||||||
@@ -884,15 +577,15 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
currentThemeMode: widget.currentThemeMode,
|
currentThemeMode: widget.currentThemeMode,
|
||||||
onThemeModeChange: widget.onThemeModeChange,
|
onThemeModeChange: widget.onThemeModeChange,
|
||||||
// 사운드 설정
|
// 사운드 설정
|
||||||
bgmVolume: _bgmVolume,
|
bgmVolume: _audioController.bgmVolume,
|
||||||
sfxVolume: _sfxVolume,
|
sfxVolume: _audioController.sfxVolume,
|
||||||
onBgmVolumeChange: (volume) {
|
onBgmVolumeChange: (volume) {
|
||||||
setState(() => _bgmVolume = volume);
|
_audioController.setBgmVolume(volume);
|
||||||
widget.audioService?.setBgmVolume(volume);
|
setState(() {});
|
||||||
},
|
},
|
||||||
onSfxVolumeChange: (volume) {
|
onSfxVolumeChange: (volume) {
|
||||||
setState(() => _sfxVolume = volume);
|
_audioController.setSfxVolume(volume);
|
||||||
widget.audioService?.setSfxVolume(volume);
|
setState(() {});
|
||||||
},
|
},
|
||||||
// 통계 및 도움말
|
// 통계 및 도움말
|
||||||
onShowStatistics: () => _showStatisticsDialog(context),
|
onShowStatistics: () => _showStatisticsDialog(context),
|
||||||
@@ -1256,7 +949,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
|
|
||||||
// Phase 8: 전투 로그 (Combat Log)
|
// Phase 8: 전투 로그 (Combat Log)
|
||||||
_buildPanelHeader(l10n.combatLog),
|
_buildPanelHeader(l10n.combatLog),
|
||||||
Expanded(flex: 2, child: CombatLog(entries: _combatLogEntries)),
|
Expanded(flex: 2, child: CombatLog(entries: _combatLogController.entries)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
255
lib/src/features/hall_of_fame/game_clear_dialog.dart
Normal file
255
lib/src/features/hall_of_fame/game_clear_dialog.dart
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
|
import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
||||||
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
|
||||||
|
/// 게임 클리어 축하 다이얼로그 표시 함수
|
||||||
|
Future<void> showGameClearDialog(
|
||||||
|
BuildContext context, {
|
||||||
|
required HallOfFameEntry entry,
|
||||||
|
required VoidCallback onNewGame,
|
||||||
|
required VoidCallback onViewHallOfFame,
|
||||||
|
}) {
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => GameClearDialog(
|
||||||
|
entry: entry,
|
||||||
|
onNewGame: onNewGame,
|
||||||
|
onViewHallOfFame: onViewHallOfFame,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 게임 클리어 다이얼로그 위젯
|
||||||
|
class GameClearDialog extends StatelessWidget {
|
||||||
|
const GameClearDialog({
|
||||||
|
super.key,
|
||||||
|
required this.entry,
|
||||||
|
required this.onNewGame,
|
||||||
|
required this.onViewHallOfFame,
|
||||||
|
});
|
||||||
|
|
||||||
|
final HallOfFameEntry entry;
|
||||||
|
final VoidCallback onNewGame;
|
||||||
|
final VoidCallback onViewHallOfFame;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final goldColor = RetroColors.goldOf(context);
|
||||||
|
final panelBg = RetroColors.panelBgOf(context);
|
||||||
|
final borderColor = RetroColors.borderOf(context);
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: panelBg,
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(color: goldColor, width: 3),
|
||||||
|
left: BorderSide(color: goldColor, width: 3),
|
||||||
|
bottom: BorderSide(color: borderColor, width: 3),
|
||||||
|
right: BorderSide(color: borderColor, width: 3),
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: goldColor.withValues(alpha: 0.4),
|
||||||
|
blurRadius: 24,
|
||||||
|
spreadRadius: 4,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// 헤더
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: goldColor.withValues(alpha: 0.2),
|
||||||
|
border: Border(bottom: BorderSide(color: goldColor, width: 2)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.emoji_events, color: goldColor, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
l10n.hofVictory.toUpperCase(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 12,
|
||||||
|
color: goldColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Icon(Icons.emoji_events, color: goldColor, size: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 컨텐츠
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.hofDefeatedGlitchGod,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 7,
|
||||||
|
color: RetroColors.textPrimaryOf(context),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Container(height: 2, color: borderColor),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// 캐릭터 정보
|
||||||
|
Text(
|
||||||
|
entry.characterName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 10,
|
||||||
|
color: goldColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
'${GameDataL10n.getRaceName(context, entry.race)} '
|
||||||
|
'${GameDataL10n.getKlassName(context, entry.klass)}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 6,
|
||||||
|
color: RetroColors.textSecondaryOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// 통계
|
||||||
|
Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 8,
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
children: [
|
||||||
|
_buildStat(context, l10n.hofLevel, '${entry.level}'),
|
||||||
|
_buildStat(
|
||||||
|
context,
|
||||||
|
l10n.hofTime,
|
||||||
|
entry.formattedPlayTime,
|
||||||
|
),
|
||||||
|
_buildStat(
|
||||||
|
context,
|
||||||
|
l10n.hofDeaths,
|
||||||
|
'${entry.totalDeaths}',
|
||||||
|
),
|
||||||
|
_buildStat(
|
||||||
|
context,
|
||||||
|
l10n.hofQuests,
|
||||||
|
'${entry.questsCompleted}',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
l10n.hofLegendEnshrined,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 6,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
color: goldColor,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 버튼
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
onViewHallOfFame();
|
||||||
|
},
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
side: BorderSide(color: borderColor, width: 2),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
l10n.hofViewHallOfFame.toUpperCase(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 6,
|
||||||
|
color: RetroColors.textSecondaryOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
onNewGame();
|
||||||
|
},
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: goldColor,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
l10n.hofNewGame.toUpperCase(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 6,
|
||||||
|
color: RetroColors.backgroundOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStat(BuildContext context, String label, String value) {
|
||||||
|
final goldColor = RetroColors.goldOf(context);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: goldColor.withValues(alpha: 0.1),
|
||||||
|
border: Border.all(color: goldColor.withValues(alpha: 0.3), width: 1),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 8,
|
||||||
|
color: goldColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 5,
|
||||||
|
color: RetroColors.textMutedOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
263
lib/src/features/hall_of_fame/hall_of_fame_entry_card.dart
Normal file
263
lib/src/features/hall_of_fame/hall_of_fame_entry_card.dart
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
||||||
|
import 'package:asciineverdie/src/features/hall_of_fame/hero_detail_dialog.dart';
|
||||||
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
|
||||||
|
/// 명예의 전당 엔트리 카드
|
||||||
|
class HallOfFameEntryCard extends StatelessWidget {
|
||||||
|
const HallOfFameEntryCard({
|
||||||
|
super.key,
|
||||||
|
required this.entry,
|
||||||
|
required this.rank,
|
||||||
|
required this.onDeleteRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
final HallOfFameEntry entry;
|
||||||
|
final int rank;
|
||||||
|
final VoidCallback onDeleteRequest;
|
||||||
|
|
||||||
|
void _showDetailDialog(BuildContext context) {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => HeroDetailDialog(entry: entry),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final rankColor = _getRankColor(context, rank);
|
||||||
|
final rankIcon = _getRankIcon(rank);
|
||||||
|
final borderColor = RetroColors.borderOf(context);
|
||||||
|
final panelBg = RetroColors.panelBgOf(context);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: panelBg,
|
||||||
|
border: Border.all(color: borderColor, width: 1),
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => _showDetailDialog(context),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// 순위
|
||||||
|
Container(
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: rankColor.withValues(alpha: 0.2),
|
||||||
|
border: Border.all(color: rankColor, width: 2),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: rankIcon != null
|
||||||
|
? Icon(rankIcon, color: rankColor, size: 18)
|
||||||
|
: Text(
|
||||||
|
'$rank',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
color: rankColor,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
// 캐릭터 정보
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 이름 + 레벨
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
entry.characterName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 8,
|
||||||
|
color: RetroColors.goldOf(context),
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: RetroColors.mpOf(
|
||||||
|
context,
|
||||||
|
).withValues(alpha: 0.2),
|
||||||
|
border: Border.all(
|
||||||
|
color: RetroColors.mpOf(context),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Lv.${entry.level}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
color: RetroColors.mpOf(context),
|
||||||
|
fontSize: 6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
// 종족/클래스
|
||||||
|
Text(
|
||||||
|
'${GameDataL10n.getRaceName(context, entry.race)} '
|
||||||
|
'${GameDataL10n.getKlassName(context, entry.klass)}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 6,
|
||||||
|
color: RetroColors.textSecondaryOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
// 통계
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildStatChip(
|
||||||
|
context,
|
||||||
|
Icons.timer,
|
||||||
|
entry.formattedPlayTime,
|
||||||
|
RetroColors.expOf(context),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
_buildStatChip(
|
||||||
|
context,
|
||||||
|
Icons.heart_broken,
|
||||||
|
'${entry.totalDeaths}',
|
||||||
|
RetroColors.hpOf(context),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
_buildStatChip(
|
||||||
|
context,
|
||||||
|
Icons.check_circle,
|
||||||
|
'${entry.questsCompleted}Q',
|
||||||
|
RetroColors.warningOf(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 클리어 날짜
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.calendar_today,
|
||||||
|
size: 12,
|
||||||
|
color: RetroColors.textMutedOf(context),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
entry.formattedClearedDate,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 5,
|
||||||
|
color: RetroColors.textMutedOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// 삭제 버튼 (디버그 모드 전용)
|
||||||
|
if (kDebugMode) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
// 이벤트 전파 중지 (카드 클릭 방지)
|
||||||
|
onDeleteRequest();
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: RetroColors.hpOf(
|
||||||
|
context,
|
||||||
|
).withValues(alpha: 0.2),
|
||||||
|
border: Border.all(
|
||||||
|
color: RetroColors.hpOf(context),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.delete_outline,
|
||||||
|
size: 16,
|
||||||
|
color: RetroColors.hpOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatChip(
|
||||||
|
BuildContext context,
|
||||||
|
IconData icon,
|
||||||
|
String value,
|
||||||
|
Color color,
|
||||||
|
) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 10, color: color),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 5,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getRankColor(BuildContext context, int rank) {
|
||||||
|
switch (rank) {
|
||||||
|
case 1:
|
||||||
|
return RetroColors.goldOf(context);
|
||||||
|
case 2:
|
||||||
|
return Colors.grey.shade400;
|
||||||
|
case 3:
|
||||||
|
return Colors.brown.shade400;
|
||||||
|
default:
|
||||||
|
return RetroColors.mpOf(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData? _getRankIcon(int rank) {
|
||||||
|
switch (rank) {
|
||||||
|
case 1:
|
||||||
|
return Icons.emoji_events;
|
||||||
|
case 2:
|
||||||
|
return Icons.workspace_premium;
|
||||||
|
case 3:
|
||||||
|
return Icons.military_tech;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
690
lib/src/features/hall_of_fame/hero_detail_dialog.dart
Normal file
690
lib/src/features/hall_of_fame/hero_detail_dialog.dart
Normal file
@@ -0,0 +1,690 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
|
import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/equipment_slot.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
||||||
|
import 'package:asciineverdie/src/features/game/widgets/ascii_animation_card.dart';
|
||||||
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
import 'package:asciineverdie/src/shared/widgets/retro_dialog.dart';
|
||||||
|
|
||||||
|
/// 명예의 전당 상세 정보 다이얼로그
|
||||||
|
class HeroDetailDialog extends StatelessWidget {
|
||||||
|
const HeroDetailDialog({super.key, required this.entry});
|
||||||
|
|
||||||
|
final HallOfFameEntry entry;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RetroDialog(
|
||||||
|
title: entry.characterName,
|
||||||
|
titleIcon: '👑',
|
||||||
|
maxWidth: 420,
|
||||||
|
maxHeight: 600,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// 서브 타이틀
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
child: Text(
|
||||||
|
'${GameDataL10n.getRaceName(context, entry.race)} '
|
||||||
|
'${GameDataL10n.getKlassName(context, entry.klass)} - '
|
||||||
|
'Lv.${entry.level}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 6,
|
||||||
|
color: RetroColors.textSecondaryOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 스크롤 가능한 컨텐츠
|
||||||
|
Flexible(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// 캐릭터 애니메이션 섹션
|
||||||
|
_buildSection(
|
||||||
|
context,
|
||||||
|
icon: Icons.movie,
|
||||||
|
title: l10n.hofCharacterPreview,
|
||||||
|
child: _buildAnimationPreview(context),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// 통계 섹션
|
||||||
|
_buildSection(
|
||||||
|
context,
|
||||||
|
icon: Icons.analytics,
|
||||||
|
title: l10n.hofStats,
|
||||||
|
child: _buildStatsGrid(context),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// 전투 스탯 섹션
|
||||||
|
if (entry.finalStats != null) ...[
|
||||||
|
_buildSection(
|
||||||
|
context,
|
||||||
|
icon: Icons.sports_mma,
|
||||||
|
title: l10n.hofCombatStats,
|
||||||
|
child: _buildCombatStatsGrid(context),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
// 장비 섹션
|
||||||
|
if (entry.finalEquipment != null) ...[
|
||||||
|
_buildSection(
|
||||||
|
context,
|
||||||
|
icon: Icons.shield,
|
||||||
|
title: l10n.navEquipment,
|
||||||
|
child: _buildEquipmentList(context),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
// 스킬 섹션 (스킬이 없어도 표시)
|
||||||
|
_buildSection(
|
||||||
|
context,
|
||||||
|
icon: Icons.auto_fix_high,
|
||||||
|
title: l10n.hofSkills,
|
||||||
|
child: _buildSkillList(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSection(
|
||||||
|
BuildContext context, {
|
||||||
|
required IconData icon,
|
||||||
|
required String title,
|
||||||
|
required Widget child,
|
||||||
|
}) {
|
||||||
|
final goldColor = RetroColors.goldOf(context);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 14, color: goldColor),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
title.toUpperCase(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 7,
|
||||||
|
color: goldColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 1,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
goldColor,
|
||||||
|
goldColor.withValues(alpha: 0.3),
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 캐릭터 애니메이션 미리보기 위젯
|
||||||
|
Widget _buildAnimationPreview(BuildContext context) {
|
||||||
|
// 장비에서 무기와 방패 이름 추출
|
||||||
|
String? weaponName;
|
||||||
|
String? shieldName;
|
||||||
|
|
||||||
|
if (entry.finalEquipment != null) {
|
||||||
|
for (final item in entry.finalEquipment!) {
|
||||||
|
if (item.slot == EquipmentSlot.weapon && item.isNotEmpty) {
|
||||||
|
weaponName = item.name;
|
||||||
|
} else if (item.slot == EquipmentSlot.shield && item.isNotEmpty) {
|
||||||
|
shieldName = item.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final borderColor = RetroColors.borderOf(context);
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: borderColor, width: 1),
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 360,
|
||||||
|
height: 80,
|
||||||
|
child: AsciiAnimationCard(
|
||||||
|
taskType: TaskType.kill,
|
||||||
|
raceId: entry.race,
|
||||||
|
weaponName: weaponName,
|
||||||
|
shieldName: shieldName,
|
||||||
|
characterLevel: entry.level,
|
||||||
|
monsterLevel: entry.level,
|
||||||
|
monsterBaseName: 'Glitch God',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatsGrid(BuildContext context) {
|
||||||
|
return Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: [
|
||||||
|
_buildStatItem(
|
||||||
|
context,
|
||||||
|
Icons.timer,
|
||||||
|
l10n.hofTime,
|
||||||
|
entry.formattedPlayTime,
|
||||||
|
),
|
||||||
|
_buildStatItem(
|
||||||
|
context,
|
||||||
|
Icons.pest_control,
|
||||||
|
l10n.hofMonsters,
|
||||||
|
'${entry.monstersKilled}',
|
||||||
|
),
|
||||||
|
_buildStatItem(
|
||||||
|
context,
|
||||||
|
Icons.heart_broken,
|
||||||
|
l10n.hofDeaths,
|
||||||
|
'${entry.totalDeaths}',
|
||||||
|
),
|
||||||
|
_buildStatItem(
|
||||||
|
context,
|
||||||
|
Icons.check_circle,
|
||||||
|
l10n.hofQuests,
|
||||||
|
'${entry.questsCompleted}',
|
||||||
|
),
|
||||||
|
_buildStatItem(
|
||||||
|
context,
|
||||||
|
Icons.calendar_today,
|
||||||
|
l10n.hofCleared,
|
||||||
|
entry.formattedClearedDate,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCombatStatsGrid(BuildContext context) {
|
||||||
|
final stats = entry.finalStats!;
|
||||||
|
final borderColor = RetroColors.borderOf(context);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// 기본 스탯 행
|
||||||
|
Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
_buildCombatStatChip(l10n.statStr, '${stats.str}', Colors.red),
|
||||||
|
_buildCombatStatChip(l10n.statCon, '${stats.con}', Colors.orange),
|
||||||
|
_buildCombatStatChip(l10n.statDex, '${stats.dex}', Colors.green),
|
||||||
|
_buildCombatStatChip(
|
||||||
|
l10n.statInt,
|
||||||
|
'${stats.intelligence}',
|
||||||
|
Colors.blue,
|
||||||
|
),
|
||||||
|
_buildCombatStatChip(l10n.statWis, '${stats.wis}', Colors.purple),
|
||||||
|
_buildCombatStatChip(l10n.statCha, '${stats.cha}', Colors.pink),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Container(height: 1, color: borderColor),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
// 공격 스탯 행
|
||||||
|
Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
_buildCombatStatChip(
|
||||||
|
l10n.statAtk,
|
||||||
|
'${stats.atk}',
|
||||||
|
Colors.red.shade700,
|
||||||
|
),
|
||||||
|
_buildCombatStatChip(
|
||||||
|
l10n.statMAtk,
|
||||||
|
'${stats.magAtk}',
|
||||||
|
Colors.blue.shade700,
|
||||||
|
),
|
||||||
|
_buildCombatStatChip(
|
||||||
|
l10n.statCri,
|
||||||
|
'${(stats.criRate * 100).toStringAsFixed(1)}%',
|
||||||
|
Colors.amber.shade700,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
// 방어 스탯 행
|
||||||
|
Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
_buildCombatStatChip(l10n.statDef, '${stats.def}', Colors.brown),
|
||||||
|
_buildCombatStatChip(
|
||||||
|
l10n.statMDef,
|
||||||
|
'${stats.magDef}',
|
||||||
|
Colors.indigo,
|
||||||
|
),
|
||||||
|
_buildCombatStatChip(
|
||||||
|
l10n.statEva,
|
||||||
|
'${(stats.evasion * 100).toStringAsFixed(1)}%',
|
||||||
|
Colors.teal,
|
||||||
|
),
|
||||||
|
_buildCombatStatChip(
|
||||||
|
l10n.statBlock,
|
||||||
|
'${(stats.blockRate * 100).toStringAsFixed(1)}%',
|
||||||
|
Colors.blueGrey,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
// HP/MP 행
|
||||||
|
Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
_buildCombatStatChip(
|
||||||
|
l10n.statHp,
|
||||||
|
'${stats.hpMax}',
|
||||||
|
Colors.red.shade400,
|
||||||
|
),
|
||||||
|
_buildCombatStatChip(
|
||||||
|
l10n.statMp,
|
||||||
|
'${stats.mpMax}',
|
||||||
|
Colors.blue.shade400,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCombatStatChip(String label, String value, Color color) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.1),
|
||||||
|
border: Border.all(color: color.withValues(alpha: 0.3), width: 1),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'$label ',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 5,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 5,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatItem(
|
||||||
|
BuildContext context,
|
||||||
|
IconData icon,
|
||||||
|
String label,
|
||||||
|
String value,
|
||||||
|
) {
|
||||||
|
final goldColor = RetroColors.goldOf(context);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: goldColor.withValues(alpha: 0.1),
|
||||||
|
border: Border.all(color: goldColor.withValues(alpha: 0.3), width: 1),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 12, color: goldColor),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 6,
|
||||||
|
color: goldColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 4,
|
||||||
|
color: RetroColors.textMutedOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEquipmentList(BuildContext context) {
|
||||||
|
final equipment = entry.finalEquipment!;
|
||||||
|
final mutedColor = RetroColors.textMutedOf(context);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: equipment.map((item) {
|
||||||
|
if (item.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
final slotLabel = _getSlotLabel(item.slot);
|
||||||
|
final slotIcon = _getSlotIcon(item.slot);
|
||||||
|
final slotIndex = _getSlotIndex(item.slot);
|
||||||
|
final rarityColor = _getRarityColor(item.rarity);
|
||||||
|
|
||||||
|
// 장비 이름 번역
|
||||||
|
final translatedName = GameDataL10n.translateEquipString(
|
||||||
|
context,
|
||||||
|
item.name,
|
||||||
|
slotIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 주요 스탯 요약
|
||||||
|
final statSummary = _buildStatSummary(item.stats, item.slot);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: rarityColor.withValues(alpha: 0.1),
|
||||||
|
border: Border.all(
|
||||||
|
color: rarityColor.withValues(alpha: 0.3),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 슬롯 + 희귀도 표시
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(slotIcon, size: 12, color: rarityColor),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
slotLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 5,
|
||||||
|
color: mutedColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 1,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: rarityColor.withValues(alpha: 0.2),
|
||||||
|
border: Border.all(
|
||||||
|
color: rarityColor.withValues(alpha: 0.4),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_getRarityLabel(item.rarity),
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 4,
|
||||||
|
color: rarityColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
// 장비 이름
|
||||||
|
Text(
|
||||||
|
translatedName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 6,
|
||||||
|
color: rarityColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 스탯 요약
|
||||||
|
if (statSummary.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Wrap(spacing: 6, runSpacing: 2, children: statSummary),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getSlotLabel(EquipmentSlot slot) {
|
||||||
|
return switch (slot) {
|
||||||
|
EquipmentSlot.weapon => l10n.slotWeapon,
|
||||||
|
EquipmentSlot.shield => l10n.slotShield,
|
||||||
|
EquipmentSlot.helm => l10n.slotHelm,
|
||||||
|
EquipmentSlot.hauberk => l10n.slotHauberk,
|
||||||
|
EquipmentSlot.brassairts => l10n.slotBrassairts,
|
||||||
|
EquipmentSlot.vambraces => l10n.slotVambraces,
|
||||||
|
EquipmentSlot.gauntlets => l10n.slotGauntlets,
|
||||||
|
EquipmentSlot.gambeson => l10n.slotGambeson,
|
||||||
|
EquipmentSlot.cuisses => l10n.slotCuisses,
|
||||||
|
EquipmentSlot.greaves => l10n.slotGreaves,
|
||||||
|
EquipmentSlot.sollerets => l10n.slotSollerets,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getSlotIcon(EquipmentSlot slot) {
|
||||||
|
return switch (slot) {
|
||||||
|
EquipmentSlot.weapon => Icons.gavel,
|
||||||
|
EquipmentSlot.shield => Icons.shield,
|
||||||
|
EquipmentSlot.helm => Icons.sports_mma,
|
||||||
|
EquipmentSlot.hauberk => Icons.checkroom,
|
||||||
|
EquipmentSlot.brassairts => Icons.front_hand,
|
||||||
|
EquipmentSlot.vambraces => Icons.back_hand,
|
||||||
|
EquipmentSlot.gauntlets => Icons.sports_handball,
|
||||||
|
EquipmentSlot.gambeson => Icons.dry_cleaning,
|
||||||
|
EquipmentSlot.cuisses => Icons.airline_seat_legroom_normal,
|
||||||
|
EquipmentSlot.greaves => Icons.snowshoeing,
|
||||||
|
EquipmentSlot.sollerets => Icons.do_not_step,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
int _getSlotIndex(EquipmentSlot slot) {
|
||||||
|
return switch (slot) {
|
||||||
|
EquipmentSlot.weapon => 0,
|
||||||
|
EquipmentSlot.shield => 1,
|
||||||
|
_ => 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getRarityColor(ItemRarity rarity) {
|
||||||
|
return switch (rarity) {
|
||||||
|
ItemRarity.common => Colors.grey.shade600,
|
||||||
|
ItemRarity.uncommon => Colors.green.shade600,
|
||||||
|
ItemRarity.rare => Colors.blue.shade600,
|
||||||
|
ItemRarity.epic => Colors.purple.shade600,
|
||||||
|
ItemRarity.legendary => Colors.orange.shade700,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getRarityLabel(ItemRarity rarity) {
|
||||||
|
return switch (rarity) {
|
||||||
|
ItemRarity.common => l10n.rarityCommon,
|
||||||
|
ItemRarity.uncommon => l10n.rarityUncommon,
|
||||||
|
ItemRarity.rare => l10n.rarityRare,
|
||||||
|
ItemRarity.epic => l10n.rarityEpic,
|
||||||
|
ItemRarity.legendary => l10n.rarityLegendary,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildStatSummary(ItemStats stats, EquipmentSlot slot) {
|
||||||
|
final widgets = <Widget>[];
|
||||||
|
|
||||||
|
void addStat(String label, String value, Color color) {
|
||||||
|
widgets.add(
|
||||||
|
Text(
|
||||||
|
'$label $value',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 5,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 공격 스탯
|
||||||
|
if (stats.atk > 0) addStat(l10n.statAtk, '+${stats.atk}', Colors.red);
|
||||||
|
if (stats.magAtk > 0) {
|
||||||
|
addStat(l10n.statMAtk, '+${stats.magAtk}', Colors.blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 방어 스탯
|
||||||
|
if (stats.def > 0) addStat(l10n.statDef, '+${stats.def}', Colors.brown);
|
||||||
|
if (stats.magDef > 0) {
|
||||||
|
addStat(l10n.statMDef, '+${stats.magDef}', Colors.indigo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 확률 스탯
|
||||||
|
if (stats.criRate > 0) {
|
||||||
|
addStat(
|
||||||
|
l10n.statCri,
|
||||||
|
'+${(stats.criRate * 100).toStringAsFixed(0)}%',
|
||||||
|
Colors.amber,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (stats.blockRate > 0) {
|
||||||
|
addStat(
|
||||||
|
l10n.statBlock,
|
||||||
|
'+${(stats.blockRate * 100).toStringAsFixed(0)}%',
|
||||||
|
Colors.blueGrey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (stats.evasion > 0) {
|
||||||
|
addStat(
|
||||||
|
l10n.statEva,
|
||||||
|
'+${(stats.evasion * 100).toStringAsFixed(0)}%',
|
||||||
|
Colors.teal,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (stats.parryRate > 0) {
|
||||||
|
addStat(
|
||||||
|
l10n.statParry,
|
||||||
|
'+${(stats.parryRate * 100).toStringAsFixed(0)}%',
|
||||||
|
Colors.cyan,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 보너스 스탯
|
||||||
|
if (stats.hpBonus > 0) {
|
||||||
|
addStat(l10n.statHp, '+${stats.hpBonus}', Colors.red.shade400);
|
||||||
|
}
|
||||||
|
if (stats.mpBonus > 0) {
|
||||||
|
addStat(l10n.statMp, '+${stats.mpBonus}', Colors.blue.shade400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 능력치 보너스
|
||||||
|
if (stats.strBonus > 0) {
|
||||||
|
addStat(l10n.statStr, '+${stats.strBonus}', Colors.red.shade700);
|
||||||
|
}
|
||||||
|
if (stats.conBonus > 0) {
|
||||||
|
addStat(l10n.statCon, '+${stats.conBonus}', Colors.orange.shade700);
|
||||||
|
}
|
||||||
|
if (stats.dexBonus > 0) {
|
||||||
|
addStat(l10n.statDex, '+${stats.dexBonus}', Colors.green.shade700);
|
||||||
|
}
|
||||||
|
if (stats.intBonus > 0) {
|
||||||
|
addStat(l10n.statInt, '+${stats.intBonus}', Colors.blue.shade700);
|
||||||
|
}
|
||||||
|
|
||||||
|
return widgets;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSkillList(BuildContext context) {
|
||||||
|
final skills = entry.finalSkills;
|
||||||
|
const skillColor = Colors.purple;
|
||||||
|
|
||||||
|
// 스킬이 없는 경우 빈 상태 표시
|
||||||
|
if (skills == null || skills.isEmpty) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: RetroColors.backgroundOf(context),
|
||||||
|
border: Border.all(color: RetroColors.borderOf(context), width: 1),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
l10n.hofNoSkills.toUpperCase(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 6,
|
||||||
|
color: RetroColors.textMutedOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: skills.map((skill) {
|
||||||
|
final name = skill['name'] ?? '';
|
||||||
|
final rank = skill['rank'] ?? '';
|
||||||
|
// 스킬 이름 번역 적용
|
||||||
|
final translatedName = GameDataL10n.getSpellName(context, name);
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: skillColor.withValues(alpha: 0.1),
|
||||||
|
border: Border.all(
|
||||||
|
color: skillColor.withValues(alpha: 0.3),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$translatedName $rank',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 5,
|
||||||
|
color: skillColor.shade400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
314
pubspec.lock
314
pubspec.lock
@@ -1,6 +1,22 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
_fe_analyzer_shared:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: _fe_analyzer_shared
|
||||||
|
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "85.0.0"
|
||||||
|
analyzer:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: analyzer
|
||||||
|
sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.7.1"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -41,6 +57,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
|
build:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build
|
||||||
|
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.4"
|
||||||
|
build_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build_config
|
||||||
|
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
build_daemon:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build_daemon
|
||||||
|
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.1"
|
||||||
|
build_resolvers:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build_resolvers
|
||||||
|
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.4"
|
||||||
|
build_runner:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: build_runner
|
||||||
|
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.4"
|
||||||
|
build_runner_core:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build_runner_core
|
||||||
|
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.1.2"
|
||||||
|
built_collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: built_collection
|
||||||
|
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.1.1"
|
||||||
|
built_value:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: built_value
|
||||||
|
sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.12.3"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -73,6 +153,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
|
code_builder:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: code_builder
|
||||||
|
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.11.1"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -81,6 +169,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
|
convert:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -97,6 +193,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.8"
|
||||||
|
dart_style:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dart_style
|
||||||
|
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -165,6 +269,70 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
freezed:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: freezed
|
||||||
|
sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.8"
|
||||||
|
freezed_annotation:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: freezed_annotation
|
||||||
|
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.4"
|
||||||
|
frontend_server_client:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: frontend_server_client
|
||||||
|
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.0"
|
||||||
|
glob:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
|
graphs:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: graphs
|
||||||
|
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.0"
|
||||||
|
http_multi_server:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_multi_server
|
||||||
|
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.2"
|
||||||
image:
|
image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -181,14 +349,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.2"
|
version: "0.20.2"
|
||||||
json_annotation:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: io
|
||||||
|
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
|
js:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: js
|
||||||
|
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.2"
|
||||||
|
json_annotation:
|
||||||
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: json_annotation
|
name: json_annotation
|
||||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.9.0"
|
version: "4.9.0"
|
||||||
|
json_serializable:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: json_serializable
|
||||||
|
sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.9.5"
|
||||||
just_audio:
|
just_audio:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -245,6 +437,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.1"
|
version: "5.1.1"
|
||||||
|
logging:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -269,6 +469,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.16.0"
|
||||||
|
mime:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
package_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_config
|
||||||
|
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -349,6 +565,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
pool:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pool
|
||||||
|
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.2"
|
||||||
posix:
|
posix:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -357,6 +581,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.3"
|
version: "6.0.3"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
pubspec_parse:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pubspec_parse
|
||||||
|
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.0"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -421,11 +661,43 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.4.1"
|
||||||
|
shelf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf
|
||||||
|
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.2"
|
||||||
|
shelf_web_socket:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_web_socket
|
||||||
|
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
source_gen:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_gen
|
||||||
|
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
source_helper:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_helper
|
||||||
|
sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.7"
|
||||||
source_span:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -450,6 +722,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
stream_transform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_transform
|
||||||
|
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -482,6 +762,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
version: "0.7.6"
|
||||||
|
timing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: timing
|
||||||
|
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -514,6 +802,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.2"
|
version: "15.0.2"
|
||||||
|
watcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: watcher
|
||||||
|
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -522,6 +818,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
web_socket:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web_socket
|
||||||
|
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
|
web_socket_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web_socket_channel
|
||||||
|
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -38,11 +38,18 @@ dependencies:
|
|||||||
path_provider: ^2.1.4
|
path_provider: ^2.1.4
|
||||||
shared_preferences: ^2.3.1
|
shared_preferences: ^2.3.1
|
||||||
just_audio: ^0.9.42
|
just_audio: ^0.9.42
|
||||||
|
# Code generation annotations
|
||||||
|
freezed_annotation: ^2.4.1
|
||||||
|
json_annotation: ^4.9.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
fake_async: ^1.3.2
|
fake_async: ^1.3.2
|
||||||
|
# Code generation
|
||||||
|
build_runner: ^2.4.13
|
||||||
|
freezed: ^2.5.7
|
||||||
|
json_serializable: ^6.8.0
|
||||||
|
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
# encourage good coding practices. The lint set provided by the package is
|
# encourage good coding practices. The lint set provided by the package is
|
||||||
|
|||||||
Reference in New Issue
Block a user