feat(hall-of-fame): 명예의 전당 대폭 개선 및 장비/아이템 직렬화
- HallOfFameEntry에 finalEquipmentDetails 추가 (상세 장비 정보) - EquipmentItem/ItemStats에 toJson/fromJson 직렬화 추가 - 명예의 전당 상세 다이얼로그 UI 대폭 개선 - Canvas 타운/워킹 애니메이션 컴포저 개선 - 캐릭터 생성 화면 UI 개선 - 게임 텍스트 다국어 지원 확장
This commit is contained in:
@@ -1284,6 +1284,12 @@ String get hofCombatStats {
|
|||||||
return 'Combat Stats';
|
return 'Combat Stats';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get hofCharacterPreview {
|
||||||
|
if (isKoreanLocale) return '캐릭터 미리보기';
|
||||||
|
if (isJapaneseLocale) return 'キャラクタープレビュー';
|
||||||
|
return 'Character Preview';
|
||||||
|
}
|
||||||
|
|
||||||
String get buttonClose {
|
String get buttonClose {
|
||||||
if (isKoreanLocale) return '닫기';
|
if (isKoreanLocale) return '닫기';
|
||||||
if (isJapaneseLocale) return '閉じる';
|
if (isJapaneseLocale) return '閉じる';
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import 'package:askiineverdie/src/core/animation/canvas/ascii_cell.dart';
|
import 'package:askiineverdie/src/core/animation/canvas/ascii_cell.dart';
|
||||||
import 'package:askiineverdie/src/core/animation/canvas/ascii_layer.dart';
|
import 'package:askiineverdie/src/core/animation/canvas/ascii_layer.dart';
|
||||||
|
import 'package:askiineverdie/src/core/animation/race_character_frames.dart';
|
||||||
|
|
||||||
/// Canvas용 마을/상점 애니메이션 합성기
|
/// Canvas용 마을/상점 애니메이션 합성기
|
||||||
///
|
///
|
||||||
/// 마을 배경 + 상점 건물 + 캐릭터
|
/// 마을 배경 + 상점 건물 + 캐릭터
|
||||||
|
/// Phase 4: 종족별 캐릭터 프레임 지원
|
||||||
class CanvasTownComposer {
|
class CanvasTownComposer {
|
||||||
const CanvasTownComposer();
|
const CanvasTownComposer({this.raceId});
|
||||||
|
|
||||||
|
/// 종족 ID (종족별 캐릭터 프레임 선택용)
|
||||||
|
final String? raceId;
|
||||||
|
|
||||||
/// 프레임 상수
|
/// 프레임 상수
|
||||||
static const int frameWidth = 60;
|
static const int frameWidth = 60;
|
||||||
@@ -67,9 +72,24 @@ class CanvasTownComposer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 캐릭터 레이어 생성 (z=2)
|
/// 캐릭터 레이어 생성 (z=2)
|
||||||
|
/// Phase 4: 종족별 프레임 지원
|
||||||
AsciiLayer _createCharacterLayer(int globalTick) {
|
AsciiLayer _createCharacterLayer(int globalTick) {
|
||||||
final frameIndex = globalTick % _shopIdleFrames.length;
|
final frameIndex = globalTick % 4; // 4프레임 루프
|
||||||
final charFrame = _shopIdleFrames[frameIndex];
|
List<String> charFrame;
|
||||||
|
|
||||||
|
// 종족별 프레임 사용 시도
|
||||||
|
if (raceId != null && raceId!.isNotEmpty) {
|
||||||
|
final raceData = RaceCharacterFrames.get(raceId!);
|
||||||
|
if (raceData != null) {
|
||||||
|
// idle 프레임 직접 사용 (마을에서는 서있는 자세)
|
||||||
|
final idleFrame = raceData.idle[frameIndex % raceData.idle.length];
|
||||||
|
charFrame = idleFrame.lines;
|
||||||
|
} else {
|
||||||
|
charFrame = _shopIdleFrames[frameIndex];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
charFrame = _shopIdleFrames[frameIndex];
|
||||||
|
}
|
||||||
|
|
||||||
final cells = _spriteToCells(charFrame);
|
final cells = _spriteToCells(charFrame);
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,17 @@ import 'package:askiineverdie/src/core/animation/background_data.dart';
|
|||||||
import 'package:askiineverdie/src/core/animation/background_layer.dart';
|
import 'package:askiineverdie/src/core/animation/background_layer.dart';
|
||||||
import 'package:askiineverdie/src/core/animation/canvas/ascii_cell.dart';
|
import 'package:askiineverdie/src/core/animation/canvas/ascii_cell.dart';
|
||||||
import 'package:askiineverdie/src/core/animation/canvas/ascii_layer.dart';
|
import 'package:askiineverdie/src/core/animation/canvas/ascii_layer.dart';
|
||||||
|
import 'package:askiineverdie/src/core/animation/race_character_frames.dart';
|
||||||
|
|
||||||
/// Canvas용 걷기 애니메이션 합성기
|
/// Canvas용 걷기 애니메이션 합성기
|
||||||
///
|
///
|
||||||
/// 배경 스크롤 + 걷는 캐릭터
|
/// 배경 스크롤 + 걷는 캐릭터
|
||||||
|
/// Phase 4: 종족별 캐릭터 프레임 지원
|
||||||
class CanvasWalkingComposer {
|
class CanvasWalkingComposer {
|
||||||
const CanvasWalkingComposer();
|
const CanvasWalkingComposer({this.raceId});
|
||||||
|
|
||||||
|
/// 종족 ID (종족별 캐릭터 프레임 선택용)
|
||||||
|
final String? raceId;
|
||||||
|
|
||||||
/// 프레임 상수
|
/// 프레임 상수
|
||||||
static const int frameWidth = 60;
|
static const int frameWidth = 60;
|
||||||
@@ -54,9 +59,24 @@ class CanvasWalkingComposer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 걷는 캐릭터 레이어 생성 (z=1)
|
/// 걷는 캐릭터 레이어 생성 (z=1)
|
||||||
|
/// Phase 4: 종족별 프레임 지원
|
||||||
AsciiLayer _createCharacterLayer(int globalTick) {
|
AsciiLayer _createCharacterLayer(int globalTick) {
|
||||||
final frameIndex = globalTick % _walkingFrames.length;
|
final frameIndex = globalTick % 4; // 4프레임 루프
|
||||||
final charFrame = _walkingFrames[frameIndex];
|
List<String> charFrame;
|
||||||
|
|
||||||
|
// 종족별 프레임 사용 시도
|
||||||
|
if (raceId != null && raceId!.isNotEmpty) {
|
||||||
|
final raceData = RaceCharacterFrames.get(raceId!);
|
||||||
|
if (raceData != null) {
|
||||||
|
// idle 프레임을 기반으로 걷기 애니메이션 생성
|
||||||
|
final idleFrame = raceData.idle[frameIndex % raceData.idle.length];
|
||||||
|
charFrame = _animateWalking(idleFrame.lines, frameIndex);
|
||||||
|
} else {
|
||||||
|
charFrame = _walkingFrames[frameIndex];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
charFrame = _walkingFrames[frameIndex];
|
||||||
|
}
|
||||||
|
|
||||||
final cells = _spriteToCells(charFrame);
|
final cells = _spriteToCells(charFrame);
|
||||||
|
|
||||||
@@ -68,6 +88,22 @@ class CanvasWalkingComposer {
|
|||||||
return AsciiLayer(cells: cells, zIndex: 1, offsetX: charX, offsetY: charY);
|
return AsciiLayer(cells: cells, zIndex: 1, offsetX: charX, offsetY: charY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// idle 프레임 기반 걷기 애니메이션 생성
|
||||||
|
/// 머리와 몸통은 유지, 다리만 걷는 동작으로 변경
|
||||||
|
List<String> _animateWalking(List<String> idleLines, int frameIndex) {
|
||||||
|
if (idleLines.length < 3) return idleLines;
|
||||||
|
|
||||||
|
// 머리(0)와 몸통(1)은 그대로 유지
|
||||||
|
final head = idleLines[0];
|
||||||
|
final body = idleLines[1];
|
||||||
|
|
||||||
|
// 다리 애니메이션 (4프레임) - 걷기 동작
|
||||||
|
const legFrames = [' /| ', ' |\\ ', ' /| ', ' |\\ '];
|
||||||
|
final legs = legFrames[frameIndex % legFrames.length];
|
||||||
|
|
||||||
|
return [head, body, legs];
|
||||||
|
}
|
||||||
|
|
||||||
/// 문자열 스프라이트를 AsciiCell 2D 배열로 변환
|
/// 문자열 스프라이트를 AsciiCell 2D 배열로 변환
|
||||||
List<List<AsciiCell>> _spriteToCells(List<String> lines) {
|
List<List<AsciiCell>> _spriteToCells(List<String> lines) {
|
||||||
return lines.map((line) {
|
return lines.map((line) {
|
||||||
|
|||||||
@@ -89,6 +89,41 @@ class EquipmentItem {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// JSON으로 직렬화
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'slot': slot.name,
|
||||||
|
'level': level,
|
||||||
|
'weight': weight,
|
||||||
|
'stats': stats.toJson(),
|
||||||
|
'rarity': rarity.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JSON에서 역직렬화
|
||||||
|
factory EquipmentItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:askiineverdie/src/core/model/combat_stats.dart';
|
import 'package:askiineverdie/src/core/model/combat_stats.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/equipment_item.dart';
|
||||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
|
|
||||||
/// 명예의 전당 엔트리 (Phase 10: Hall of Fame Entry)
|
/// 명예의 전당 엔트리 (Phase 10: Hall of Fame Entry)
|
||||||
@@ -54,8 +55,8 @@ class HallOfFameEntry {
|
|||||||
/// 최종 전투 스탯 (향후 아스키 아레나용)
|
/// 최종 전투 스탯 (향후 아스키 아레나용)
|
||||||
final CombatStats? finalStats;
|
final CombatStats? finalStats;
|
||||||
|
|
||||||
/// 최종 장비 목록 (향후 아스키 아레나용)
|
/// 최종 장비 목록 (풀 스탯 포함)
|
||||||
final Map<String, String>? finalEquipment;
|
final List<EquipmentItem>? finalEquipment;
|
||||||
|
|
||||||
/// 최종 스펠북 (스펠 이름 + 랭크)
|
/// 최종 스펠북 (스펠 이름 + 랭크)
|
||||||
final List<Map<String, String>>? finalSpells;
|
final List<Map<String, String>>? finalSpells;
|
||||||
@@ -98,19 +99,7 @@ class HallOfFameEntry {
|
|||||||
questsCompleted: state.progress.questCount,
|
questsCompleted: state.progress.questCount,
|
||||||
clearedAt: DateTime.now(),
|
clearedAt: DateTime.now(),
|
||||||
finalStats: combatStats,
|
finalStats: combatStats,
|
||||||
finalEquipment: {
|
finalEquipment: List<EquipmentItem>.from(state.equipment.items),
|
||||||
'weapon': state.equipment.weapon,
|
|
||||||
'shield': state.equipment.shield,
|
|
||||||
'helm': state.equipment.helm,
|
|
||||||
'hauberk': state.equipment.hauberk,
|
|
||||||
'brassairts': state.equipment.brassairts,
|
|
||||||
'vambraces': state.equipment.vambraces,
|
|
||||||
'gauntlets': state.equipment.gauntlets,
|
|
||||||
'gambeson': state.equipment.gambeson,
|
|
||||||
'cuisses': state.equipment.cuisses,
|
|
||||||
'greaves': state.equipment.greaves,
|
|
||||||
'sollerets': state.equipment.sollerets,
|
|
||||||
},
|
|
||||||
finalSpells: state.spellBook.spells
|
finalSpells: state.spellBook.spells
|
||||||
.map((s) => {'name': s.name, 'rank': s.rank})
|
.map((s) => {'name': s.name, 'rank': s.rank})
|
||||||
.toList(),
|
.toList(),
|
||||||
@@ -131,7 +120,7 @@ class HallOfFameEntry {
|
|||||||
'questsCompleted': questsCompleted,
|
'questsCompleted': questsCompleted,
|
||||||
'clearedAt': clearedAt.toIso8601String(),
|
'clearedAt': clearedAt.toIso8601String(),
|
||||||
'finalStats': finalStats?.toJson(),
|
'finalStats': finalStats?.toJson(),
|
||||||
'finalEquipment': finalEquipment,
|
'finalEquipment': finalEquipment?.map((e) => e.toJson()).toList(),
|
||||||
'finalSpells': finalSpells,
|
'finalSpells': finalSpells,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -153,7 +142,9 @@ class HallOfFameEntry {
|
|||||||
? CombatStats.fromJson(json['finalStats'] as Map<String, dynamic>)
|
? CombatStats.fromJson(json['finalStats'] as Map<String, dynamic>)
|
||||||
: null,
|
: null,
|
||||||
finalEquipment: json['finalEquipment'] != null
|
finalEquipment: json['finalEquipment'] != null
|
||||||
? Map<String, String>.from(json['finalEquipment'] as Map)
|
? (json['finalEquipment'] as List<dynamic>)
|
||||||
|
.map((e) => EquipmentItem.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList()
|
||||||
: null,
|
: null,
|
||||||
finalSpells: json['finalSpells'] != null
|
finalSpells: json['finalSpells'] != null
|
||||||
? (json['finalSpells'] as List<dynamic>)
|
? (json['finalSpells'] as List<dynamic>)
|
||||||
|
|||||||
@@ -127,6 +127,52 @@ class ItemStats {
|
|||||||
/// 빈 스탯 (보너스 없음)
|
/// 빈 스탯 (보너스 없음)
|
||||||
static const empty = ItemStats();
|
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는 합산 대상 아님 (무기 슬롯 단일 값)
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
|
|
||||||
// Composer 인스턴스들
|
// Composer 인스턴스들
|
||||||
CanvasBattleComposer? _battleComposer;
|
CanvasBattleComposer? _battleComposer;
|
||||||
final _walkingComposer = const CanvasWalkingComposer();
|
CanvasWalkingComposer? _walkingComposer;
|
||||||
final _townComposer = const CanvasTownComposer();
|
CanvasTownComposer? _townComposer;
|
||||||
final _specialComposer = const CanvasSpecialComposer();
|
final _specialComposer = const CanvasSpecialComposer();
|
||||||
|
|
||||||
// 전투 애니메이션 상태
|
// 전투 애니메이션 상태
|
||||||
@@ -370,12 +370,15 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
|
|
||||||
case AsciiAnimationType.town:
|
case AsciiAnimationType.town:
|
||||||
_animationMode = AnimationMode.town;
|
_animationMode = AnimationMode.town;
|
||||||
|
_townComposer = CanvasTownComposer(raceId: widget.raceId);
|
||||||
|
|
||||||
case AsciiAnimationType.walking:
|
case AsciiAnimationType.walking:
|
||||||
_animationMode = AnimationMode.walking;
|
_animationMode = AnimationMode.walking;
|
||||||
|
_walkingComposer = CanvasWalkingComposer(raceId: widget.raceId);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
_animationMode = AnimationMode.walking;
|
_animationMode = AnimationMode.walking;
|
||||||
|
_walkingComposer = CanvasWalkingComposer(raceId: widget.raceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 일시정지 상태면 타이머 시작하지 않음
|
// 일시정지 상태면 타이머 시작하지 않음
|
||||||
@@ -445,8 +448,10 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
_globalTick,
|
_globalTick,
|
||||||
) ??
|
) ??
|
||||||
[AsciiLayer.empty()],
|
[AsciiLayer.empty()],
|
||||||
AnimationMode.walking => _walkingComposer.composeLayers(_globalTick),
|
AnimationMode.walking =>
|
||||||
AnimationMode.town => _townComposer.composeLayers(_globalTick),
|
_walkingComposer?.composeLayers(_globalTick) ?? [AsciiLayer.empty()],
|
||||||
|
AnimationMode.town =>
|
||||||
|
_townComposer?.composeLayers(_globalTick) ?? [AsciiLayer.empty()],
|
||||||
AnimationMode.special => _specialComposer.composeLayers(
|
AnimationMode.special => _specialComposer.composeLayers(
|
||||||
_currentSpecialAnimation ?? AsciiAnimationType.levelUp,
|
_currentSpecialAnimation ?? AsciiAnimationType.levelUp,
|
||||||
_currentFrame,
|
_currentFrame,
|
||||||
|
|||||||
@@ -4,8 +4,13 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||||
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
|
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
|
||||||
import 'package:askiineverdie/src/core/model/combat_stats.dart';
|
import 'package:askiineverdie/src/core/model/combat_stats.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/equipment_item.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
import 'package:askiineverdie/src/core/model/hall_of_fame.dart';
|
import 'package:askiineverdie/src/core/model/hall_of_fame.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/item_stats.dart';
|
||||||
import 'package:askiineverdie/src/core/storage/hall_of_fame_storage.dart';
|
import 'package:askiineverdie/src/core/storage/hall_of_fame_storage.dart';
|
||||||
|
import 'package:askiineverdie/src/features/game/widgets/ascii_animation_card.dart';
|
||||||
|
|
||||||
/// 명예의 전당 화면 (Phase 10: Hall of Fame Screen)
|
/// 명예의 전당 화면 (Phase 10: Hall of Fame Screen)
|
||||||
class HallOfFameScreen extends StatefulWidget {
|
class HallOfFameScreen extends StatefulWidget {
|
||||||
@@ -149,56 +154,160 @@ HallOfFameEntry _createDebugSampleEntry() {
|
|||||||
id: 'debug_sample_001',
|
id: 'debug_sample_001',
|
||||||
characterName: 'Debug Hero',
|
characterName: 'Debug Hero',
|
||||||
race: 'byte_human',
|
race: 'byte_human',
|
||||||
klass: 'loop_wizard',
|
klass: 'recursion_master',
|
||||||
level: 100,
|
level: 100,
|
||||||
totalPlayTimeMs: 10 * 60 * 60 * 1000, // 10시간
|
totalPlayTimeMs: 10 * 60 * 60 * 1000, // 10시간
|
||||||
totalDeaths: 3,
|
totalDeaths: 3,
|
||||||
monstersKilled: 1234,
|
monstersKilled: 1234,
|
||||||
questsCompleted: 42,
|
questsCompleted: 42,
|
||||||
clearedAt: DateTime.now(),
|
clearedAt: DateTime.now(),
|
||||||
finalEquipment: {
|
finalEquipment: [
|
||||||
'weapon': '+15 Legendary Debugger',
|
// 무기: Universe Simulator|15, 레벨 100 → plus=85
|
||||||
'shield': '+10 Exception Shield',
|
const EquipmentItem(
|
||||||
'helm': '+8 Null Pointer Helm',
|
name: '+85 AI-Augmented Universe Simulator',
|
||||||
'hauberk': '+12 Thread-Safe Armor',
|
slot: EquipmentSlot.weapon,
|
||||||
'brassairts': '+6 Memory Guard',
|
level: 100,
|
||||||
'vambraces': '+5 Stack Overflow Band',
|
weight: 15,
|
||||||
'gauntlets': '+7 Syntax Checker Gloves',
|
rarity: ItemRarity.legendary,
|
||||||
'gambeson': '+9 Buffer Padding',
|
stats: ItemStats(atk: 180, magAtk: 120, criRate: 0.15, attackSpeed: 600),
|
||||||
'cuisses': '+4 Runtime Protector',
|
),
|
||||||
'greaves': '+6 Compile Time Shin',
|
// 방패: Entropy Shield|65, 레벨 100 → plus=35
|
||||||
'sollerets': '+5 Binary Boots',
|
const EquipmentItem(
|
||||||
},
|
name: '+35 Air-gapped Entropy Shield',
|
||||||
finalSpells: [
|
slot: EquipmentSlot.shield,
|
||||||
{'name': 'Recursive Thunder', 'rank': 'XII'},
|
level: 100,
|
||||||
{'name': 'Async Heal', 'rank': 'VIII'},
|
weight: 12,
|
||||||
{'name': 'Memory Leak Curse', 'rank': 'X'},
|
rarity: ItemRarity.legendary,
|
||||||
{'name': 'Stack Overflow', 'rank': 'VI'},
|
stats: ItemStats(def: 85, magDef: 60, blockRate: 0.25),
|
||||||
{'name': 'Null Pointer Strike', 'rank': 'IX'},
|
),
|
||||||
{'name': 'Thread Lock', 'rank': 'VII'},
|
// 방어구: Multiverse Armor|60, 레벨 100 → plus=40
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+40 Containerized Multiverse Armor',
|
||||||
|
slot: EquipmentSlot.helm,
|
||||||
|
level: 100,
|
||||||
|
weight: 8,
|
||||||
|
rarity: ItemRarity.legendary,
|
||||||
|
stats: ItemStats(def: 45, magDef: 55, intBonus: 5),
|
||||||
|
),
|
||||||
|
// Singularity Barrier|55, 레벨 100 → plus=45
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+45 Quantum-safe Singularity Barrier',
|
||||||
|
slot: EquipmentSlot.hauberk,
|
||||||
|
level: 100,
|
||||||
|
weight: 20,
|
||||||
|
rarity: ItemRarity.legendary,
|
||||||
|
stats: ItemStats(def: 95, magDef: 40, hpBonus: 200),
|
||||||
|
),
|
||||||
|
// AI Firewall|45, 레벨 100 → plus=55
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+55 Hardened AI Firewall',
|
||||||
|
slot: EquipmentSlot.brassairts,
|
||||||
|
level: 100,
|
||||||
|
weight: 6,
|
||||||
|
rarity: ItemRarity.epic,
|
||||||
|
stats: ItemStats(def: 35, magDef: 25),
|
||||||
|
),
|
||||||
|
// Neural Network Mesh|40, 레벨 100 → plus=60
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+60 Encrypted Neural Network Mesh',
|
||||||
|
slot: EquipmentSlot.vambraces,
|
||||||
|
level: 100,
|
||||||
|
weight: 5,
|
||||||
|
rarity: ItemRarity.epic,
|
||||||
|
stats: ItemStats(def: 30, magDef: 20, dexBonus: 3),
|
||||||
|
),
|
||||||
|
// Zero-Day Aegis|30, 레벨 100 → plus=70
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+70 Patched Zero-Day Aegis',
|
||||||
|
slot: EquipmentSlot.gauntlets,
|
||||||
|
level: 100,
|
||||||
|
weight: 4,
|
||||||
|
rarity: ItemRarity.epic,
|
||||||
|
stats: ItemStats(def: 25, atk: 15, criRate: 0.05),
|
||||||
|
),
|
||||||
|
// Blockchain Platemail|35, 레벨 100 → plus=65
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+65 Certified Blockchain Platemail',
|
||||||
|
slot: EquipmentSlot.gambeson,
|
||||||
|
level: 100,
|
||||||
|
weight: 10,
|
||||||
|
rarity: ItemRarity.epic,
|
||||||
|
stats: ItemStats(def: 40, magDef: 30),
|
||||||
|
),
|
||||||
|
// Container Suit|19, 레벨 100 → plus=81
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+81 Sandboxed Container Suit',
|
||||||
|
slot: EquipmentSlot.cuisses,
|
||||||
|
level: 100,
|
||||||
|
weight: 8,
|
||||||
|
rarity: ItemRarity.epic,
|
||||||
|
stats: ItemStats(def: 35, evasion: 0.05),
|
||||||
|
),
|
||||||
|
// Virtualization Mail|20, 레벨 100 → plus=80
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+80 Patched Virtualization Mail',
|
||||||
|
slot: EquipmentSlot.greaves,
|
||||||
|
level: 100,
|
||||||
|
weight: 7,
|
||||||
|
rarity: ItemRarity.epic,
|
||||||
|
stats: ItemStats(def: 30, dexBonus: 2),
|
||||||
|
),
|
||||||
|
// Sandbox Shell|18, 레벨 100 → plus=82
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+82 Hardened Sandbox Shell',
|
||||||
|
slot: EquipmentSlot.sollerets,
|
||||||
|
level: 100,
|
||||||
|
weight: 5,
|
||||||
|
rarity: ItemRarity.epic,
|
||||||
|
stats: ItemStats(def: 20, evasion: 0.03),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
// 레벨 100 캐릭터의 스펠: randomLow 분포로 낮은 인덱스 스펠이 높은 랭크
|
||||||
|
// 100번의 레벨업 = 100번 스펠 학습 기회, 초반 스펠은 여러 번 학습되어 랭크 상승
|
||||||
|
finalSpells: [
|
||||||
|
{'name': 'Garbage Collection', 'rank': 'XIV'}, // 인덱스 0 - 가장 많이 학습
|
||||||
|
{'name': 'Memory Optimization', 'rank': 'XII'}, // 인덱스 1
|
||||||
|
{'name': 'Debug Mode', 'rank': 'XI'}, // 인덱스 2
|
||||||
|
{'name': 'Breakpoint', 'rank': 'X'}, // 인덱스 3
|
||||||
|
{'name': 'Step Over', 'rank': 'IX'}, // 인덱스 4
|
||||||
|
{'name': 'Step Into', 'rank': 'VIII'}, // 인덱스 5
|
||||||
|
{'name': 'Watch Variable', 'rank': 'VII'}, // 인덱스 6
|
||||||
|
{'name': 'Hot Reload', 'rank': 'VI'}, // 인덱스 7
|
||||||
|
{'name': 'Cold Boot', 'rank': 'V'}, // 인덱스 8
|
||||||
|
{'name': 'Safe Mode', 'rank': 'IV'}, // 인덱스 9
|
||||||
|
{'name': 'Kernel Panic', 'rank': 'III'}, // 인덱스 10
|
||||||
|
{'name': 'Blue Screen', 'rank': 'II'}, // 인덱스 11
|
||||||
|
{'name': 'Stack Trace', 'rank': 'I'}, // 인덱스 12
|
||||||
|
],
|
||||||
|
// 레벨 100 기본 스탯: 시작 ~60 + 99레벨 × 2스탯 = ~258 총합
|
||||||
|
// recursion_master (마법사 계열): INT/WIS 집중 빌드
|
||||||
|
// CombatStats는 게임 공식 CombatStats.fromStats()에 따라 계산
|
||||||
|
// baseAtk = STR*2 + level + equipStats.atk = 32*2 + 100 + 195 = 359
|
||||||
|
// baseDef = CON + level/2 + equipStats.def = 38 + 50 + 440 = 528
|
||||||
|
// baseMagAtk = INT*2 + level + equipStats.magAtk = 65*2 + 100 + 120 = 350
|
||||||
|
// baseMagDef = WIS + level/2 + equipStats.magDef = 55 + 50 + 210 = 315
|
||||||
finalStats: const CombatStats(
|
finalStats: const CombatStats(
|
||||||
str: 85,
|
str: 32,
|
||||||
con: 72,
|
con: 38,
|
||||||
dex: 68,
|
dex: 45,
|
||||||
intelligence: 90,
|
intelligence: 65,
|
||||||
wis: 65,
|
wis: 55,
|
||||||
cha: 55,
|
cha: 23,
|
||||||
atk: 450,
|
atk: 359,
|
||||||
def: 280,
|
def: 528,
|
||||||
magAtk: 520,
|
magAtk: 350,
|
||||||
magDef: 195,
|
magDef: 315,
|
||||||
criRate: 0.35,
|
criRate: 0.32,
|
||||||
criDamage: 2.2,
|
criDamage: 1.95,
|
||||||
evasion: 0.18,
|
evasion: 0.30,
|
||||||
accuracy: 0.95,
|
accuracy: 0.89,
|
||||||
blockRate: 0.25,
|
blockRate: 0.37,
|
||||||
parryRate: 0.15,
|
parryRate: 0.15,
|
||||||
attackDelayMs: 650,
|
attackDelayMs: 650,
|
||||||
hpMax: 2500,
|
hpMax: 1850,
|
||||||
hpCurrent: 2500,
|
hpCurrent: 1850,
|
||||||
mpMax: 1800,
|
mpMax: 2100,
|
||||||
mpCurrent: 1800,
|
mpCurrent: 2100,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -208,55 +317,160 @@ HallOfFameEntry _createDebugSampleEntry2() {
|
|||||||
return HallOfFameEntry(
|
return HallOfFameEntry(
|
||||||
id: 'debug_sample_002',
|
id: 'debug_sample_002',
|
||||||
characterName: 'Binary Knight',
|
characterName: 'Binary Knight',
|
||||||
race: 'pixel_elf',
|
race: 'null_elf',
|
||||||
klass: 'git_fighter',
|
klass: 'overflow_warrior',
|
||||||
level: 95,
|
level: 95,
|
||||||
totalPlayTimeMs: 8 * 60 * 60 * 1000 + 30 * 60 * 1000, // 8시간 30분
|
totalPlayTimeMs: 8 * 60 * 60 * 1000 + 30 * 60 * 1000, // 8시간 30분
|
||||||
totalDeaths: 7,
|
totalDeaths: 7,
|
||||||
monstersKilled: 2156,
|
monstersKilled: 2156,
|
||||||
questsCompleted: 38,
|
questsCompleted: 38,
|
||||||
clearedAt: DateTime.now().subtract(const Duration(days: 3)),
|
clearedAt: DateTime.now().subtract(const Duration(days: 3)),
|
||||||
finalEquipment: {
|
finalEquipment: [
|
||||||
'weapon': '+12 Merge Conflict Sword',
|
// 무기: Quantum Entangler|10, 레벨 95 → plus=85
|
||||||
'shield': '+14 Firewall Buckler',
|
const EquipmentItem(
|
||||||
'helm': '+10 SSH Helmet',
|
name: '+85 GPU-Powered Quantum Entangler',
|
||||||
'hauberk': '+11 Docker Container Plate',
|
slot: EquipmentSlot.weapon,
|
||||||
'brassairts': '+8 API Gateway Guard',
|
level: 95,
|
||||||
'vambraces': '+7 Cache Hit Bracers',
|
weight: 18,
|
||||||
'gauntlets': '+9 Regex Gloves',
|
rarity: ItemRarity.legendary,
|
||||||
'gambeson': '+6 JSON Parser Vest',
|
stats: ItemStats(atk: 220, criRate: 0.12, parryRate: 0.08, attackSpeed: 850),
|
||||||
'cuisses': '+8 Load Balancer Legs',
|
),
|
||||||
'greaves': '+7 Kubernetes Greaves',
|
// 방패: Multiverse Barrier|50, 레벨 95 → plus=45
|
||||||
'sollerets': '+6 Cloud Deploy Boots',
|
const EquipmentItem(
|
||||||
},
|
name: '+45 Air-gapped Multiverse Barrier',
|
||||||
finalSpells: [
|
slot: EquipmentSlot.shield,
|
||||||
{'name': 'Fork Bomb', 'rank': 'X'},
|
level: 95,
|
||||||
{'name': 'Garbage Collection', 'rank': 'IX'},
|
weight: 16,
|
||||||
{'name': 'Infinite Loop', 'rank': 'XI'},
|
rarity: ItemRarity.legendary,
|
||||||
{'name': 'Buffer Overflow', 'rank': 'VII'},
|
stats: ItemStats(def: 120, magDef: 45, blockRate: 0.35, hpBonus: 150),
|
||||||
|
),
|
||||||
|
// Multiverse Armor|60, 레벨 95 → plus=35
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+35 Certified Multiverse Armor',
|
||||||
|
slot: EquipmentSlot.helm,
|
||||||
|
level: 95,
|
||||||
|
weight: 10,
|
||||||
|
rarity: ItemRarity.legendary,
|
||||||
|
stats: ItemStats(def: 55, conBonus: 4),
|
||||||
|
),
|
||||||
|
// Quantum Shield Matrix|50, 레벨 95 → plus=45
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+45 Containerized Quantum Shield Matrix',
|
||||||
|
slot: EquipmentSlot.hauberk,
|
||||||
|
level: 95,
|
||||||
|
weight: 25,
|
||||||
|
rarity: ItemRarity.legendary,
|
||||||
|
stats: ItemStats(def: 110, hpBonus: 300, conBonus: 3),
|
||||||
|
),
|
||||||
|
// ASLR Armor|17, 레벨 95 → plus=78
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+78 Patched ASLR Armor',
|
||||||
|
slot: EquipmentSlot.brassairts,
|
||||||
|
level: 95,
|
||||||
|
weight: 8,
|
||||||
|
rarity: ItemRarity.epic,
|
||||||
|
stats: ItemStats(def: 40, strBonus: 2),
|
||||||
|
),
|
||||||
|
// Stack Protector|15, 레벨 95 → plus=80
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+80 Encrypted Stack Protector',
|
||||||
|
slot: EquipmentSlot.vambraces,
|
||||||
|
level: 95,
|
||||||
|
weight: 6,
|
||||||
|
rarity: ItemRarity.epic,
|
||||||
|
stats: ItemStats(def: 35, atk: 10),
|
||||||
|
),
|
||||||
|
// Heap Guard|16, 레벨 95 → plus=79
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+79 Hardened Heap Guard',
|
||||||
|
slot: EquipmentSlot.gauntlets,
|
||||||
|
level: 95,
|
||||||
|
weight: 5,
|
||||||
|
rarity: ItemRarity.epic,
|
||||||
|
stats: ItemStats(def: 30, atk: 25, criRate: 0.03),
|
||||||
|
),
|
||||||
|
// Memory Barrier|14, 레벨 95 → plus=81
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+81 Quantum-safe Memory Barrier',
|
||||||
|
slot: EquipmentSlot.gambeson,
|
||||||
|
level: 95,
|
||||||
|
weight: 12,
|
||||||
|
rarity: ItemRarity.epic,
|
||||||
|
stats: ItemStats(def: 45, magDef: 20),
|
||||||
|
),
|
||||||
|
// Kernel Guard|12, 레벨 95 → plus=83
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+83 Certified Kernel Guard',
|
||||||
|
slot: EquipmentSlot.cuisses,
|
||||||
|
level: 95,
|
||||||
|
weight: 10,
|
||||||
|
rarity: ItemRarity.epic,
|
||||||
|
stats: ItemStats(def: 42, strBonus: 2),
|
||||||
|
),
|
||||||
|
// Protocol Suit|10, 레벨 95 → plus=85
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+85 Sandboxed Protocol Suit',
|
||||||
|
slot: EquipmentSlot.greaves,
|
||||||
|
level: 95,
|
||||||
|
weight: 8,
|
||||||
|
rarity: ItemRarity.epic,
|
||||||
|
stats: ItemStats(def: 38, dexBonus: 2),
|
||||||
|
),
|
||||||
|
// Encryption Layer|6, 레벨 95 → plus=89
|
||||||
|
const EquipmentItem(
|
||||||
|
name: '+89 Patched Encryption Layer',
|
||||||
|
slot: EquipmentSlot.sollerets,
|
||||||
|
level: 95,
|
||||||
|
weight: 6,
|
||||||
|
rarity: ItemRarity.epic,
|
||||||
|
stats: ItemStats(def: 25, evasion: 0.02),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
// 레벨 95 캐릭터의 스펠: randomLow 분포 적용
|
||||||
|
// 95번의 레벨업 기회, 전사 계열이라 WIS가 낮아 학습 가능 스펠 범위 제한
|
||||||
|
// WIS 30 + level 95 = 125 → 대부분의 스펠 접근 가능
|
||||||
|
finalSpells: [
|
||||||
|
{'name': 'Garbage Collection', 'rank': 'XIII'}, // 인덱스 0
|
||||||
|
{'name': 'Memory Optimization', 'rank': 'XI'}, // 인덱스 1
|
||||||
|
{'name': 'Debug Mode', 'rank': 'X'}, // 인덱스 2
|
||||||
|
{'name': 'Breakpoint', 'rank': 'IX'}, // 인덱스 3
|
||||||
|
{'name': 'Step Over', 'rank': 'VIII'}, // 인덱스 4
|
||||||
|
{'name': 'Step Into', 'rank': 'VII'}, // 인덱스 5
|
||||||
|
{'name': 'Watch Variable', 'rank': 'VI'}, // 인덱스 6
|
||||||
|
{'name': 'Hot Reload', 'rank': 'V'}, // 인덱스 7
|
||||||
|
{'name': 'Cold Boot', 'rank': 'IV'}, // 인덱스 8
|
||||||
|
{'name': 'Safe Mode', 'rank': 'III'}, // 인덱스 9
|
||||||
|
{'name': 'Kernel Panic', 'rank': 'II'}, // 인덱스 10
|
||||||
|
],
|
||||||
|
// 레벨 95 기본 스탯: 시작 ~60 + 94레벨 × 2스탯 = ~248 총합
|
||||||
|
// overflow_warrior (전사 계열): STR/CON 집중 빌드
|
||||||
|
// CombatStats는 게임 공식에 따라 계산
|
||||||
|
// baseAtk = STR*2 + level + equipStats.atk = 58*2 + 95 + 255 = 466
|
||||||
|
// baseDef = CON + level/2 + equipStats.def = 55 + 47 + 577 = 679
|
||||||
|
// baseMagAtk = INT*2 + level + equipStats.magAtk = 28*2 + 95 + 0 = 151
|
||||||
|
// baseMagDef = WIS + level/2 + equipStats.magDef = 30 + 47 + 65 = 142
|
||||||
finalStats: const CombatStats(
|
finalStats: const CombatStats(
|
||||||
str: 95,
|
str: 58,
|
||||||
con: 88,
|
con: 55,
|
||||||
dex: 75,
|
dex: 42,
|
||||||
intelligence: 60,
|
intelligence: 28,
|
||||||
wis: 55,
|
wis: 30,
|
||||||
cha: 50,
|
cha: 35,
|
||||||
atk: 580,
|
atk: 466,
|
||||||
def: 420,
|
def: 679,
|
||||||
magAtk: 280,
|
magAtk: 151,
|
||||||
magDef: 165,
|
magDef: 142,
|
||||||
criRate: 0.28,
|
criRate: 0.26,
|
||||||
criDamage: 2.5,
|
criDamage: 1.92,
|
||||||
evasion: 0.12,
|
evasion: 0.23,
|
||||||
accuracy: 0.92,
|
accuracy: 0.88,
|
||||||
blockRate: 0.35,
|
blockRate: 0.47,
|
||||||
parryRate: 0.22,
|
parryRate: 0.28,
|
||||||
attackDelayMs: 800,
|
attackDelayMs: 800,
|
||||||
hpMax: 3200,
|
hpMax: 2200,
|
||||||
hpCurrent: 3200,
|
hpCurrent: 2200,
|
||||||
mpMax: 1200,
|
mpMax: 1100,
|
||||||
mpCurrent: 1200,
|
mpCurrent: 1100,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -596,13 +810,20 @@ class _HallOfFameDetailDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
content: SizedBox(
|
content: ConstrainedBox(
|
||||||
width: double.maxFinite,
|
constraints: const BoxConstraints(maxWidth: 400, maxHeight: 500),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
// 캐릭터 애니메이션 섹션 (Character Animation Section)
|
||||||
|
_buildSection(
|
||||||
|
icon: Icons.movie,
|
||||||
|
title: l10n.hofCharacterPreview,
|
||||||
|
child: _buildAnimationPreview(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
// 통계 섹션 (Statistics Section)
|
// 통계 섹션 (Statistics Section)
|
||||||
_buildSection(
|
_buildSection(
|
||||||
icon: Icons.analytics,
|
icon: Icons.analytics,
|
||||||
@@ -676,6 +897,47 @@ class _HallOfFameDetailDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 캐릭터 애니메이션 미리보기 위젯
|
||||||
|
Widget _buildAnimationPreview() {
|
||||||
|
// 장비에서 무기와 방패 이름 추출
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsciiCanvasWidget은 60x8 그리드 사용
|
||||||
|
// 다이얼로그 내에서 적절한 크기: 360x80 (비율 7.5:1 유지)
|
||||||
|
return Center(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Colors.amber.shade300),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
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() {
|
Widget _buildStatsGrid() {
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
@@ -806,27 +1068,29 @@ class _HallOfFameDetailDialog extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey.shade100,
|
color: Colors.amber.shade700.withValues(alpha: 0.15),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.amber.shade700.withValues(alpha: 0.3)),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 14, color: Colors.grey.shade600),
|
Icon(icon, size: 14, color: Colors.amber.shade600),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
color: Colors.amber.shade300,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(fontSize: 10, color: Colors.grey.shade600),
|
style: TextStyle(fontSize: 10, color: Colors.amber.shade400),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -837,56 +1101,221 @@ class _HallOfFameDetailDialog extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildEquipmentList(BuildContext context) {
|
Widget _buildEquipmentList(BuildContext context) {
|
||||||
final equipment = entry.finalEquipment!;
|
final equipment = entry.finalEquipment!;
|
||||||
// 슬롯 키, 아이콘, l10n 슬롯 이름
|
|
||||||
final slots = [
|
|
||||||
('weapon', Icons.gavel, l10n.slotWeapon, 0),
|
|
||||||
('shield', Icons.shield, l10n.slotShield, 1),
|
|
||||||
('helm', Icons.sports_mma, l10n.slotHelm, 2),
|
|
||||||
('hauberk', Icons.checkroom, l10n.slotHauberk, 2),
|
|
||||||
('brassairts', Icons.front_hand, l10n.slotBrassairts, 2),
|
|
||||||
('vambraces', Icons.back_hand, l10n.slotVambraces, 2),
|
|
||||||
('gauntlets', Icons.sports_handball, l10n.slotGauntlets, 2),
|
|
||||||
('gambeson', Icons.dry_cleaning, l10n.slotGambeson, 2),
|
|
||||||
('cuisses', Icons.airline_seat_legroom_normal, l10n.slotCuisses, 2),
|
|
||||||
('greaves', Icons.snowshoeing, l10n.slotGreaves, 2),
|
|
||||||
('sollerets', Icons.do_not_step, l10n.slotSollerets, 2),
|
|
||||||
];
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: slots.map((slot) {
|
children: equipment.map((item) {
|
||||||
final (key, icon, label, slotIndex) = slot;
|
if (item.isEmpty) return const SizedBox.shrink();
|
||||||
final rawValue = equipment[key] ?? '';
|
|
||||||
// 장비 이름 번역 적용
|
final slotLabel = _getSlotLabel(item.slot);
|
||||||
final value = rawValue.isEmpty
|
final slotIcon = _getSlotIcon(item.slot);
|
||||||
? l10n.uiEmpty
|
final slotIndex = _getSlotIndex(item.slot);
|
||||||
: GameDataL10n.translateEquipString(context, rawValue, slotIndex);
|
final rarityColor = _getRarityColor(item.rarity);
|
||||||
|
|
||||||
|
// 장비 이름 번역
|
||||||
|
final translatedName = GameDataL10n.translateEquipString(
|
||||||
|
context,
|
||||||
|
item.name,
|
||||||
|
slotIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 주요 스탯 요약
|
||||||
|
final statSummary = _buildStatSummary(item.stats, item.slot);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
child: Row(
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: rarityColor.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: Border.all(color: rarityColor.withValues(alpha: 0.3)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 16, color: Colors.grey.shade500),
|
// 슬롯 + 이름 + 희귀도 표시
|
||||||
const SizedBox(width: 8),
|
Row(
|
||||||
SizedBox(
|
children: [
|
||||||
width: 80,
|
Icon(slotIcon, size: 14, color: rarityColor),
|
||||||
child: Text(
|
const SizedBox(width: 6),
|
||||||
label,
|
Text(
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
slotLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
const Spacer(),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 1,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: rarityColor.withValues(alpha: 0.2),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
value,
|
_getRarityLabel(item.rarity),
|
||||||
style: const TextStyle(fontSize: 12),
|
style: TextStyle(
|
||||||
overflow: TextOverflow.ellipsis,
|
fontSize: 9,
|
||||||
|
color: rarityColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
// 장비 이름
|
||||||
|
Text(
|
||||||
|
translatedName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: rarityColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 스탯 요약
|
||||||
|
if (statSummary.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 2,
|
||||||
|
children: statSummary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).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(fontSize: 10, 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 _buildSpellList(BuildContext context) {
|
Widget _buildSpellList(BuildContext context) {
|
||||||
final spells = entry.finalSpells!;
|
final spells = entry.finalSpells!;
|
||||||
return Wrap(
|
return Wrap(
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Re-Roll 버튼 클릭
|
/// Re-Roll 버튼 클릭
|
||||||
|
/// 원본 NewGuy.pas RerollClick: 스탯, 종족, 클래스 모두 랜덤화
|
||||||
void _onReroll() {
|
void _onReroll() {
|
||||||
// 현재 시드를 이력에 저장
|
// 현재 시드를 이력에 저장
|
||||||
_rollHistory.insert(0, _currentSeed);
|
_rollHistory.insert(0, _currentSeed);
|
||||||
@@ -142,7 +143,15 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 새 시드로 굴림
|
// 새 시드로 굴림
|
||||||
_currentSeed = math.Random().nextInt(0x7FFFFFFF);
|
final random = math.Random();
|
||||||
|
_currentSeed = random.nextInt(0x7FFFFFFF);
|
||||||
|
|
||||||
|
// 종족/클래스도 랜덤 선택
|
||||||
|
setState(() {
|
||||||
|
_selectedRaceIndex = random.nextInt(_races.length);
|
||||||
|
_selectedKlassIndex = random.nextInt(_klasses.length);
|
||||||
|
});
|
||||||
|
|
||||||
_rollStats();
|
_rollStats();
|
||||||
|
|
||||||
// 선택된 종족/직업으로 스크롤
|
// 선택된 종족/직업으로 스크롤
|
||||||
|
|||||||
Reference in New Issue
Block a user