implement initial map system (no assets)
This commit is contained in:
140
moon/README.md
Normal file
140
moon/README.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Little Fairy - MapManager 시스템
|
||||
|
||||
## 프로젝트 개요
|
||||
|
||||
Little Fairy는 "살아있음의 기쁨"과 "생명의 연속성"을 체험할 수 있는 한국적 정서의 RPG 게임입니다. 이 프로젝트는 Godot 4.4.1 엔진과 C#을 사용하여 개발되었습니다.
|
||||
|
||||
현재 구현된 기능은 **노드 기반 맵 시스템(MapManager)**입니다.
|
||||
|
||||
## 구현된 기능
|
||||
|
||||
### MapManager 시스템
|
||||
|
||||
1. **노드 기반 맵 이동**
|
||||
- 각 맵은 의미있는 장소들로 구성된 노드 네트워크
|
||||
- 클릭 기반 즉시 이동 (복잡한 이동 메커니즘 배제)
|
||||
- 방향키를 통한 직관적인 노드 이동
|
||||
|
||||
2. **4개의 테스트 맵**
|
||||
- **Map A - 꽃마루**: 어린 선녀가 깨어나는 봄의 산골 마을
|
||||
- **Map B - 물소리 벌판**: 성장기 선녀의 여름 강변 들판
|
||||
- **Map C - 단풍골**: 성숙기 선녀의 가을 도시 근교
|
||||
- **Map D - 백설봉**: 지혜의 시기 겨울 설산
|
||||
|
||||
3. **노드 잠금/해제 시스템**
|
||||
- 스토리 진행에 따른 점진적 영역 확장
|
||||
- 테스트용 비밀길 해제 기능
|
||||
|
||||
4. **JSON 기반 맵 데이터**
|
||||
- 유연한 맵 구성과 쉬운 수정
|
||||
- 각 노드별 상세 정보 (이름, 설명, 연결 정보 등)
|
||||
|
||||
## 파일 구조
|
||||
|
||||
```
|
||||
moon/
|
||||
├── scripts/
|
||||
│ ├── data/
|
||||
│ │ └── MapData.cs # 맵 데이터 클래스
|
||||
│ ├── managers/
|
||||
│ │ └── MapManager.cs # 맵 관리 시스템
|
||||
│ └── MainGame.cs # 메인 게임 클래스
|
||||
├── data/
|
||||
│ └── maps/
|
||||
│ ├── map_a_flower_village.json # 꽃마루 맵 데이터
|
||||
│ ├── map_b_river_plains.json # 물소리 벌판 맵 데이터
|
||||
│ ├── map_c_maple_valley.json # 단풍골 맵 데이터
|
||||
│ └── map_d_snow_peak.json # 백설봉 맵 데이터
|
||||
└── scenes/
|
||||
└── MainGame.tscn # 메인 게임 씬
|
||||
```
|
||||
|
||||
## 조작법
|
||||
|
||||
### 키보드 조작
|
||||
- **↑↓←→**: 방향키로 연결된 노드로 이동
|
||||
- 현재 노드에서 연결된 노드 중 방향에 가장 가까운 노드로 이동
|
||||
|
||||
### 마우스 조작
|
||||
- **노드 버튼 클릭**: 해당 노드로 직접 이동
|
||||
- 연결되지 않은 노드는 클릭해도 이동되지 않음
|
||||
|
||||
### UI 기능
|
||||
- **맵 선택**: 드롭다운 메뉴로 다른 맵으로 전환
|
||||
- **비밀길 해제**: 테스트용 잠긴 노드 해제 버튼
|
||||
|
||||
## 맵 구조
|
||||
|
||||
### Map A - 꽃마루 (Spring Mountain Village)
|
||||
1. **선녀의 둥지** - 시작 지점, 신성한 장소
|
||||
2. **마을 광장** - 사회적 중심지, 해찬이가 있음
|
||||
3. **할머니 집** - 달빛 할머니의 거주지
|
||||
4. **시든 꽃밭** - 치유가 필요한 자연 공간, 꽃님 정령
|
||||
5. **나무꾼 오두막** - 석주 나무꾼의 작업장
|
||||
6. **성스러운 샘** - 치유의 성스러운 장소
|
||||
7. **비밀 숲길** - 잠긴 특별한 장소 (해제 가능)
|
||||
|
||||
### Map B - 물소리 벌판 (Summer River Plains)
|
||||
1. **강변 선착장** - 맵 진입점
|
||||
2. **광활한 들판** - 농부 철수의 작업 공간
|
||||
3. **물방앗간** - 전통적인 작업 시설
|
||||
4. **용궁 입구** - 용왕 미르의 신비한 거처
|
||||
5. **약초밭** - 의원 혜련의 치유 공간
|
||||
6. **마을 회관** - 청년 준호와의 사회적 공간
|
||||
|
||||
### Map C - 단풍골 (Autumn Maple Valley)
|
||||
1. **한옥 보존지구** - 영숙의 전통 보존 공간
|
||||
2. **현대식 아파트 단지** - 기업인 태영의 거주지
|
||||
3. **예술가의 거리** - 예술가 소희의 창작 공간
|
||||
4. **환경운동 거점** - 환경운동가 민수의 활동 기지
|
||||
5. **도시계획사무소** - 도시 수호신 한울의 업무 공간
|
||||
6. **전통시장** - 다양한 사람들이 모이는 곳
|
||||
|
||||
### Map D - 백설봉 (Winter Snow Peak)
|
||||
1. **설산 입구** - 신성한 산으로의 입구
|
||||
2. **명상의 동굴** - 깊은 성찰의 공간
|
||||
3. **선조들의 터** - 지혜가 축적된 역사적 장소
|
||||
4. **별빛 정상** - 산신령 설봉과의 만남, 완성의 장소
|
||||
|
||||
## 기술적 특징
|
||||
|
||||
### MapManager 클래스
|
||||
- **이벤트 기반 통신**: Signal을 통한 노드 변경, 맵 변경, 노드 해제 알림
|
||||
- **JSON 데이터 로딩**: 동적 맵 구성과 유연한 수정
|
||||
- **방향 기반 이동**: 방향키 입력에 따른 지능적 노드 선택
|
||||
- **잠금 시스템**: 스토리 진행에 따른 점진적 영역 확장
|
||||
|
||||
### 데이터 구조
|
||||
- **MapData**: 전체 맵 정보 (이름, 설명, 배경음악 등)
|
||||
- **MapNodeData**: 개별 노드 정보 (위치, 타입, 연결 정보 등)
|
||||
- **NodeType**: 노드 분류 (거주지, 자연명소, 작업장, 성지 등)
|
||||
|
||||
## 실행 방법
|
||||
|
||||
1. Godot 4.4.1 엔진에서 프로젝트 열기
|
||||
2. MainGame.tscn 씬을 메인 씬으로 설정 (이미 설정됨)
|
||||
3. F5 키로 프로젝트 실행
|
||||
4. 방향키나 마우스로 노드 이동 테스트
|
||||
5. 맵 선택 드롭다운으로 다른 맵으로 전환
|
||||
|
||||
## 향후 확장 계획
|
||||
|
||||
1. **캐릭터 시스템**: NPC와의 상호작용
|
||||
2. **인연 시스템**: 관계 발전과 호감도
|
||||
3. **매듭 시스템**: 한국 전통 매듭을 활용한 스킬 시스템
|
||||
4. **생명의 동반자**: 깊은 관계 형성과 결합
|
||||
5. **선녀의 별자리**: 세대 간 유전 시스템
|
||||
|
||||
## 개발 환경
|
||||
|
||||
- **엔진**: Godot 4.4.1
|
||||
- **언어**: C#
|
||||
- **플랫폼**: Windows (추후 다른 플랫폼 확장 예정)
|
||||
|
||||
## 라이선스
|
||||
|
||||
Little Fairy 프로젝트는 개발 중인 게임으로, 모든 권리는 개발팀에 있습니다.
|
||||
|
||||
---
|
||||
|
||||
**MapManager 시스템은 Little Fairy 게임의 핵심 이동 시스템으로, 한국적 정서가 담긴 아름다운 장소들을 노드 기반으로 연결하여 플레이어가 직관적이고 의미있는 이동 경험을 할 수 있도록 설계되었습니다.**
|
||||
102
moon/data/maps/map_a_flower_village.json
Normal file
102
moon/data/maps/map_a_flower_village.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"id": "map_a_flower_village",
|
||||
"name": "Spring Mountain Village",
|
||||
"korean_name": "꽃마루",
|
||||
"description": "어린 선녀가 처음 깨어나는 아름다운 산골 마을",
|
||||
"starting_node_id": "fairy_nest",
|
||||
"background_music": "res://assets/audio/music/spring_village_theme.ogg",
|
||||
"ambient_sound": "res://assets/audio/ambient/mountain_breeze.ogg",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "fairy_nest",
|
||||
"name": "Fairy's Nest",
|
||||
"korean_name": "선녀의 둥지",
|
||||
"description": "작은 선녀가 처음 깨어난 신비로운 둥지. 따뜻한 빛이 감돌고 있다.",
|
||||
"position": [400, 300],
|
||||
"type": "SacredPlace",
|
||||
"connected_nodes": ["village_square", "sacred_spring"],
|
||||
"available_characters": [],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapA_fairy_nest.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_home.png"
|
||||
},
|
||||
{
|
||||
"id": "village_square",
|
||||
"name": "Village Square",
|
||||
"korean_name": "마을 광장",
|
||||
"description": "꽃마루 마을의 중심지. 사람들이 모이고 이야기를 나누는 따뜻한 곳이다.",
|
||||
"position": [600, 400],
|
||||
"type": "SocialPlace",
|
||||
"connected_nodes": ["fairy_nest", "grandmother_house", "withered_garden"],
|
||||
"available_characters": ["boy_haechan"],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapA_village_square.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_social.png"
|
||||
},
|
||||
{
|
||||
"id": "grandmother_house",
|
||||
"name": "Grandmother's House",
|
||||
"korean_name": "할머니 집",
|
||||
"description": "따뜻한 마음의 달빛 할머니가 사는 아름다운 한옥. 지혜와 사랑이 가득하다.",
|
||||
"position": [800, 300],
|
||||
"type": "Residence",
|
||||
"connected_nodes": ["village_square", "secret_path"],
|
||||
"available_characters": ["grandmother_dalbit"],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapA_grandmother_house.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_house.png"
|
||||
},
|
||||
{
|
||||
"id": "withered_garden",
|
||||
"name": "Withered Flower Garden",
|
||||
"korean_name": "시든 꽃밭",
|
||||
"description": "한때 아름다웠던 꽃밭. 이제는 시들어 있지만 여전히 생명의 기운이 남아있다.",
|
||||
"position": [700, 500],
|
||||
"type": "NatureSpot",
|
||||
"connected_nodes": ["village_square", "woodcutter_cabin"],
|
||||
"available_characters": ["spirit_flownim"],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapA_withered_flower_garden.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_nature.png"
|
||||
},
|
||||
{
|
||||
"id": "woodcutter_cabin",
|
||||
"name": "Woodcutter's Cabin",
|
||||
"korean_name": "나무꾼 오두막",
|
||||
"description": "성실한 석주 나무꾼이 사는 소박하지만 정감있는 오두막.",
|
||||
"position": [500, 600],
|
||||
"type": "WorkPlace",
|
||||
"connected_nodes": ["withered_garden", "sacred_spring"],
|
||||
"available_characters": ["woodcutter_seokju"],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapA_woodcutter_cabin.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_work.png"
|
||||
},
|
||||
{
|
||||
"id": "sacred_spring",
|
||||
"name": "Sacred Spring",
|
||||
"korean_name": "성스러운 샘",
|
||||
"description": "맑고 신성한 기운이 흐르는 샘. 모든 생명에게 평화와 치유를 선사한다.",
|
||||
"position": [300, 500],
|
||||
"type": "SacredPlace",
|
||||
"connected_nodes": ["fairy_nest", "woodcutter_cabin", "secret_path"],
|
||||
"available_characters": [],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapA_sacred_spring.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_sacred.png"
|
||||
},
|
||||
{
|
||||
"id": "secret_path",
|
||||
"name": "Secret Forest Path",
|
||||
"korean_name": "비밀 숲길",
|
||||
"description": "숨겨진 신비로운 숲길. 특별한 인연을 쌓아야만 발견할 수 있는 곳이다.",
|
||||
"position": [900, 400],
|
||||
"type": "SpecialPlace",
|
||||
"connected_nodes": ["grandmother_house", "sacred_spring"],
|
||||
"available_characters": [],
|
||||
"is_unlocked": false,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapA_secret_forest_path.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_special.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
89
moon/data/maps/map_b_river_plains.json
Normal file
89
moon/data/maps/map_b_river_plains.json
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"id": "map_b_river_plains",
|
||||
"name": "Summer River Plains",
|
||||
"korean_name": "물소리 벌판",
|
||||
"description": "넓은 강변 들판에서 성장하는 선녀의 두 번째 여정",
|
||||
"starting_node_id": "riverside_dock",
|
||||
"background_music": "res://assets/audio/music/summer_plains_theme.ogg",
|
||||
"ambient_sound": "res://assets/audio/ambient/river_flow.ogg",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "riverside_dock",
|
||||
"name": "Riverside Dock",
|
||||
"korean_name": "강변 선착장",
|
||||
"description": "물소리 벌판으로 들어오는 관문. 맑은 강물이 흘러간다.",
|
||||
"position": [300, 400],
|
||||
"type": "SocialPlace",
|
||||
"connected_nodes": ["vast_fields", "water_mill"],
|
||||
"available_characters": [],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapB_riverside_dock.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_dock.png"
|
||||
},
|
||||
{
|
||||
"id": "vast_fields",
|
||||
"name": "Vast Fields",
|
||||
"korean_name": "광활한 들판",
|
||||
"description": "끝없이 펼쳐진 들판. 농부들이 열심히 일하고 있다.",
|
||||
"position": [500, 300],
|
||||
"type": "WorkPlace",
|
||||
"connected_nodes": ["riverside_dock", "village_hall", "herb_garden"],
|
||||
"available_characters": ["farmer_cheolsu"],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapB_vast_fields.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_field.png"
|
||||
},
|
||||
{
|
||||
"id": "water_mill",
|
||||
"name": "Water Mill",
|
||||
"korean_name": "물방앗간",
|
||||
"description": "강물의 힘으로 돌아가는 방앗간. 곡식을 갈아 가루를 만든다.",
|
||||
"position": [200, 500],
|
||||
"type": "WorkPlace",
|
||||
"connected_nodes": ["riverside_dock", "dragon_palace_entrance"],
|
||||
"available_characters": [],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapB_water_mill.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_mill.png"
|
||||
},
|
||||
{
|
||||
"id": "dragon_palace_entrance",
|
||||
"name": "Dragon Palace Entrance",
|
||||
"korean_name": "용궁 입구",
|
||||
"description": "전설의 용왕이 사는 용궁으로 들어가는 신비한 입구.",
|
||||
"position": [100, 600],
|
||||
"type": "SacredPlace",
|
||||
"connected_nodes": ["water_mill"],
|
||||
"available_characters": ["dragon_king_mir"],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapB_dragon_palace_entrance.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_palace.png"
|
||||
},
|
||||
{
|
||||
"id": "herb_garden",
|
||||
"name": "Herb Garden",
|
||||
"korean_name": "약초밭",
|
||||
"description": "다양한 약초들이 자라는 곳. 치유의 힘을 가진 식물들이 가득하다.",
|
||||
"position": [700, 200],
|
||||
"type": "NatureSpot",
|
||||
"connected_nodes": ["vast_fields", "village_hall"],
|
||||
"available_characters": ["doctor_hyeolryeon"],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapB_herb_garden.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_herb.png"
|
||||
},
|
||||
{
|
||||
"id": "village_hall",
|
||||
"name": "Village Hall",
|
||||
"korean_name": "마을 회관",
|
||||
"description": "마을 사람들이 모여 중요한 일들을 의논하는 곳.",
|
||||
"position": [600, 400],
|
||||
"type": "SocialPlace",
|
||||
"connected_nodes": ["vast_fields", "herb_garden"],
|
||||
"available_characters": ["youth_junho"],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapB_village_hall.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_hall.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
89
moon/data/maps/map_c_maple_valley.json
Normal file
89
moon/data/maps/map_c_maple_valley.json
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"id": "map_c_maple_valley",
|
||||
"name": "Autumn Maple Valley",
|
||||
"korean_name": "단풍골",
|
||||
"description": "전통과 현대가 만나는 단풍이 아름다운 도시 근교",
|
||||
"starting_node_id": "hanok_preservation_area",
|
||||
"background_music": "res://assets/audio/music/autumn_valley_theme.ogg",
|
||||
"ambient_sound": "res://assets/audio/ambient/autumn_wind.ogg",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "hanok_preservation_area",
|
||||
"name": "Hanok Preservation Area",
|
||||
"korean_name": "한옥 보존지구",
|
||||
"description": "전통 한옥들이 아름답게 보존되어 있는 문화유산 지역.",
|
||||
"position": [400, 300],
|
||||
"type": "SacredPlace",
|
||||
"connected_nodes": ["traditional_market", "artists_street"],
|
||||
"available_characters": ["hanok_preserver_yeongsuk"],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapC_hanok_preservation_area.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_hanok.png"
|
||||
},
|
||||
{
|
||||
"id": "modern_apartment_complex",
|
||||
"name": "Modern Apartment Complex",
|
||||
"korean_name": "현대식 아파트 단지",
|
||||
"description": "새로 지어진 현대식 아파트 단지. 편리하지만 삭막함도 느껴진다.",
|
||||
"position": [700, 200],
|
||||
"type": "Residence",
|
||||
"connected_nodes": ["urban_planning_office", "artists_street"],
|
||||
"available_characters": ["businessman_taeyoung"],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapC_modern_apartment_complex.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_apartment.png"
|
||||
},
|
||||
{
|
||||
"id": "artists_street",
|
||||
"name": "Artists' Street",
|
||||
"korean_name": "예술가의 거리",
|
||||
"description": "다양한 예술가들이 모여 창작 활동을 하는 활기찬 거리.",
|
||||
"position": [500, 400],
|
||||
"type": "SocialPlace",
|
||||
"connected_nodes": ["hanok_preservation_area", "modern_apartment_complex", "environmental_movement_base"],
|
||||
"available_characters": ["artist_sohee"],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapC_artists_street.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_art.png"
|
||||
},
|
||||
{
|
||||
"id": "environmental_movement_base",
|
||||
"name": "Environmental Movement Base",
|
||||
"korean_name": "환경운동 거점",
|
||||
"description": "환경을 보호하기 위해 활동하는 사람들의 본거지.",
|
||||
"position": [300, 500],
|
||||
"type": "WorkPlace",
|
||||
"connected_nodes": ["artists_street", "traditional_market"],
|
||||
"available_characters": ["environmentalist_minsu"],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapC_environmental_movement_base.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_environment.png"
|
||||
},
|
||||
{
|
||||
"id": "urban_planning_office",
|
||||
"name": "Urban Planning Office",
|
||||
"korean_name": "도시계획사무소",
|
||||
"description": "도시의 미래를 설계하는 곳. 개발과 보존 사이의 균형을 찾고 있다.",
|
||||
"position": [800, 300],
|
||||
"type": "WorkPlace",
|
||||
"connected_nodes": ["modern_apartment_complex", "traditional_market"],
|
||||
"available_characters": ["city_guardian_hanul"],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapC_urban_planning_office.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_office.png"
|
||||
},
|
||||
{
|
||||
"id": "traditional_market",
|
||||
"name": "Traditional Market",
|
||||
"korean_name": "전통시장",
|
||||
"description": "옛 정취가 남아있는 전통시장. 정겨운 상인들과 다양한 물건들이 가득하다.",
|
||||
"position": [600, 500],
|
||||
"type": "SocialPlace",
|
||||
"connected_nodes": ["hanok_preservation_area", "environmental_movement_base", "urban_planning_office"],
|
||||
"available_characters": [],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapC_traditional_market.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_market.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
63
moon/data/maps/map_d_snow_peak.json
Normal file
63
moon/data/maps/map_d_snow_peak.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": "map_d_snow_peak",
|
||||
"name": "Winter Snow Peak",
|
||||
"korean_name": "백설봉",
|
||||
"description": "지혜와 완성의 시기를 맞이하는 신성한 설산",
|
||||
"starting_node_id": "mountain_entrance",
|
||||
"background_music": "res://assets/audio/music/winter_peak_theme.ogg",
|
||||
"ambient_sound": "res://assets/audio/ambient/mountain_wind.ogg",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "mountain_entrance",
|
||||
"name": "Snowy Mountain Entrance",
|
||||
"korean_name": "설산 입구",
|
||||
"description": "백설봉으로 들어가는 신성한 입구. 깨끗한 눈이 내려 세상을 정화한다.",
|
||||
"position": [400, 600],
|
||||
"type": "SacredPlace",
|
||||
"connected_nodes": ["meditation_cave"],
|
||||
"available_characters": [],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapD_snowy_mountain_entrance.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_mountain.png"
|
||||
},
|
||||
{
|
||||
"id": "meditation_cave",
|
||||
"name": "Meditation Cave",
|
||||
"korean_name": "명상의 동굴",
|
||||
"description": "깊은 명상과 성찰을 위한 고요한 동굴. 내면의 소리에 귀 기울일 수 있다.",
|
||||
"position": [500, 450],
|
||||
"type": "SacredPlace",
|
||||
"connected_nodes": ["mountain_entrance", "ancestors_site"],
|
||||
"available_characters": [],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapD_meditation_cave.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_cave.png"
|
||||
},
|
||||
{
|
||||
"id": "ancestors_site",
|
||||
"name": "Ancestors' Site",
|
||||
"korean_name": "선조들의 터",
|
||||
"description": "오래전 선조들이 머물렀던 신성한 터. 지혜와 경험이 축적된 곳이다.",
|
||||
"position": [600, 300],
|
||||
"type": "SacredPlace",
|
||||
"connected_nodes": ["meditation_cave", "starlight_peak"],
|
||||
"available_characters": [],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapD_ancestors_site.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_ancestors.png"
|
||||
},
|
||||
{
|
||||
"id": "starlight_peak",
|
||||
"name": "Starlight Peak",
|
||||
"korean_name": "별빛 정상",
|
||||
"description": "백설봉의 최고봉. 별빛이 가장 가깝게 느껴지는 완성의 장소.",
|
||||
"position": [700, 150],
|
||||
"type": "SacredPlace",
|
||||
"connected_nodes": ["ancestors_site"],
|
||||
"available_characters": ["mountain_spirit_seolbong"],
|
||||
"is_unlocked": true,
|
||||
"background_texture": "res://assets/backgrounds/scenario1/mapD_starlight_peak.png",
|
||||
"icon_texture": "res://assets/ui/icons/icon_map_peak.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
9
moon/scenes/MainGame.tscn
Normal file
9
moon/scenes/MainGame.tscn
Normal file
@@ -0,0 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://cb6a2vl1w8hne"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bey6df0q5laf5" path="res://scripts/MainGame.cs" id="1_2xrdy"]
|
||||
|
||||
[node name="MainGame" type="Node"]
|
||||
script = ExtResource("1_2xrdy")
|
||||
|
||||
[node name="Camera2D" type="Camera2D" parent="."]
|
||||
position = Vector2(640, 360)
|
||||
209
moon/scripts/MainGame.cs
Normal file
209
moon/scripts/MainGame.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using Godot;
|
||||
using LittleFairy.Managers;
|
||||
|
||||
namespace LittleFairy
|
||||
{
|
||||
/// <summary>
|
||||
/// 게임의 메인 씬 매니저
|
||||
/// </summary>
|
||||
public partial class MainGame : Node
|
||||
{
|
||||
private MapManager _mapManager;
|
||||
private Control _uiContainer;
|
||||
private Panel _infoPanel;
|
||||
private Label _infoLabel;
|
||||
private Button _unlockSecretButton;
|
||||
private Panel _mapSelectorPanel;
|
||||
private OptionButton _mapSelector;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
GD.Print("메인 게임 초기화 중...");
|
||||
|
||||
// MapManager 생성 및 추가
|
||||
_mapManager = new MapManager();
|
||||
AddChild(_mapManager);
|
||||
|
||||
// MapManager 이벤트 연결
|
||||
_mapManager.NodeChanged += OnNodeChanged;
|
||||
_mapManager.MapChanged += OnMapChanged;
|
||||
_mapManager.NodeUnlocked += OnNodeUnlocked;
|
||||
|
||||
// UI 설정
|
||||
SetupUI();
|
||||
|
||||
GD.Print("메인 게임 초기화 완료");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI 초기 설정
|
||||
/// </summary>
|
||||
private void SetupUI()
|
||||
{
|
||||
// UI 컨테이너
|
||||
_uiContainer = new Control();
|
||||
_uiContainer.Name = "UIContainer";
|
||||
_uiContainer.SetAnchorsAndOffsetsPreset(Control.PresetMode.FullRect);
|
||||
AddChild(_uiContainer);
|
||||
|
||||
// 정보 패널
|
||||
_infoPanel = new Panel();
|
||||
_infoPanel.Name = "InfoPanel";
|
||||
_infoPanel.Position = new Vector2(20, 60);
|
||||
_infoPanel.Size = new Vector2(300, 200);
|
||||
|
||||
var styleBox = new StyleBoxFlat();
|
||||
styleBox.BgColor = new Color(0, 0, 0, 0.7f);
|
||||
styleBox.BorderColor = Colors.White;
|
||||
styleBox.BorderWidthTop = 2;
|
||||
styleBox.BorderWidthBottom = 2;
|
||||
styleBox.BorderWidthLeft = 2;
|
||||
styleBox.BorderWidthRight = 2;
|
||||
_infoPanel.AddThemeStyleboxOverride("panel", styleBox);
|
||||
|
||||
_uiContainer.AddChild(_infoPanel);
|
||||
|
||||
// 정보 라벨
|
||||
_infoLabel = new Label();
|
||||
_infoLabel.Name = "InfoLabel";
|
||||
_infoLabel.Position = new Vector2(10, 10);
|
||||
_infoLabel.Size = new Vector2(280, 150);
|
||||
_infoLabel.Text = "MapManager 테스트\n\n방향키로 이동\n마우스 클릭으로 이동";
|
||||
_infoLabel.AutowrapMode = TextServer.AutowrapMode.WordSmart;
|
||||
_infoPanel.AddChild(_infoLabel);
|
||||
|
||||
// 비밀길 해제 버튼 (테스트용)
|
||||
_unlockSecretButton = new Button();
|
||||
_unlockSecretButton.Name = "UnlockSecretButton";
|
||||
_unlockSecretButton.Position = new Vector2(10, 165);
|
||||
_unlockSecretButton.Size = new Vector2(150, 25);
|
||||
_unlockSecretButton.Text = "비밀길 해제";
|
||||
_unlockSecretButton.Pressed += OnUnlockSecretButtonPressed;
|
||||
_infoPanel.AddChild(_unlockSecretButton);
|
||||
|
||||
// 맵 선택 패널
|
||||
_mapSelectorPanel = new Panel();
|
||||
_mapSelectorPanel.Name = "MapSelectorPanel";
|
||||
_mapSelectorPanel.Position = new Vector2(340, 60);
|
||||
_mapSelectorPanel.Size = new Vector2(200, 100);
|
||||
|
||||
var mapSelectorStyle = new StyleBoxFlat();
|
||||
mapSelectorStyle.BgColor = new Color(0.2f, 0.2f, 0.2f, 0.8f);
|
||||
mapSelectorStyle.BorderColor = Colors.White;
|
||||
mapSelectorStyle.BorderWidthTop = 1;
|
||||
mapSelectorStyle.BorderWidthBottom = 1;
|
||||
mapSelectorStyle.BorderWidthLeft = 1;
|
||||
mapSelectorStyle.BorderWidthRight = 1;
|
||||
_mapSelectorPanel.AddThemeStyleboxOverride("panel", mapSelectorStyle);
|
||||
|
||||
_uiContainer.AddChild(_mapSelectorPanel);
|
||||
|
||||
// 맵 선택 라벨
|
||||
var mapSelectorLabel = new Label();
|
||||
mapSelectorLabel.Position = new Vector2(10, 10);
|
||||
mapSelectorLabel.Text = "맵 선택:";
|
||||
_mapSelectorPanel.AddChild(mapSelectorLabel);
|
||||
|
||||
// 맵 선택 드롭다운
|
||||
_mapSelector = new OptionButton();
|
||||
_mapSelector.Name = "MapSelector";
|
||||
_mapSelector.Position = new Vector2(10, 35);
|
||||
_mapSelector.Size = new Vector2(180, 30);
|
||||
_mapSelector.AddItem("Map A - 꽃마루");
|
||||
_mapSelector.AddItem("Map B - 물소리 벌판");
|
||||
_mapSelector.AddItem("Map C - 단풍골");
|
||||
_mapSelector.AddItem("Map D - 백설봉");
|
||||
_mapSelector.Selected = 0;
|
||||
_mapSelector.ItemSelected += OnMapSelected;
|
||||
_mapSelectorPanel.AddChild(_mapSelector);
|
||||
|
||||
// 조작법 라벨
|
||||
var controlsLabel = new Label();
|
||||
controlsLabel.Position = new Vector2(20, 280);
|
||||
controlsLabel.Text = "조작법:\n↑↓←→ : 방향키로 이동\n마우스 클릭 : 노드 직접 이동\n비밀길 해제 : 잠긴 노드 해제";
|
||||
controlsLabel.AutowrapMode = TextServer.AutowrapMode.WordSmart;
|
||||
_uiContainer.AddChild(controlsLabel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 변경 이벤트 처리
|
||||
/// </summary>
|
||||
/// <param name="nodeId">변경된 노드 ID</param>
|
||||
private void OnNodeChanged(string nodeId)
|
||||
{
|
||||
UpdateInfoPanel();
|
||||
GD.Print($"노드 변경 이벤트: {nodeId}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 변경 이벤트 처리
|
||||
/// </summary>
|
||||
/// <param name="mapId">변경된 맵 ID</param>
|
||||
private void OnMapChanged(string mapId)
|
||||
{
|
||||
UpdateInfoPanel();
|
||||
GD.Print($"맵 변경 이벤트: {mapId}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 잠금 해제 이벤트 처리
|
||||
/// </summary>
|
||||
/// <param name="nodeId">잠금 해제된 노드 ID</param>
|
||||
private void OnNodeUnlocked(string nodeId)
|
||||
{
|
||||
UpdateInfoPanel();
|
||||
GD.Print($"노드 잠금 해제 이벤트: {nodeId}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 정보 패널 업데이트
|
||||
/// </summary>
|
||||
private void UpdateInfoPanel()
|
||||
{
|
||||
if (_mapManager == null || _infoLabel == null) return;
|
||||
|
||||
var currentMap = _mapManager.GetCurrentMapInfo();
|
||||
var currentNode = _mapManager.GetCurrentNodeInfo();
|
||||
var availableConnections = _mapManager.GetAvailableConnections();
|
||||
|
||||
if (currentMap != null && currentNode != null)
|
||||
{
|
||||
string connectionsText = string.Join(", ", availableConnections);
|
||||
|
||||
_infoLabel.Text = $"현재 맵: {currentMap.KoreanName}\n" +
|
||||
$"현재 위치: {currentNode.KoreanName}\n" +
|
||||
$"설명: {currentNode.Description}\n\n" +
|
||||
$"이동 가능: {connectionsText}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 비밀길 해제 버튼 클릭 이벤트
|
||||
/// </summary>
|
||||
private void OnUnlockSecretButtonPressed()
|
||||
{
|
||||
_mapManager?.UnlockNode("secret_path");
|
||||
GD.Print("비밀길 잠금 해제됨");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 선택 이벤트 처리
|
||||
/// </summary>
|
||||
/// <param name="index">선택된 맵 인덱스</param>
|
||||
private void OnMapSelected(long index)
|
||||
{
|
||||
string[] mapIds = {
|
||||
"map_a_flower_village",
|
||||
"map_b_river_plains",
|
||||
"map_c_maple_valley",
|
||||
"map_d_snow_peak"
|
||||
};
|
||||
|
||||
if (index >= 0 && index < mapIds.Length)
|
||||
{
|
||||
_mapManager?.LoadMap(mapIds[index]);
|
||||
GD.Print($"맵 전환: {mapIds[index]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
moon/scripts/MainGame.cs.uid
Normal file
1
moon/scripts/MainGame.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bey6df0q5laf5
|
||||
53
moon/scripts/data/MapData.cs
Normal file
53
moon/scripts/data/MapData.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using Godot;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LittleFairy.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 노드의 종류
|
||||
/// </summary>
|
||||
public enum NodeType
|
||||
{
|
||||
Residence, // 거주지
|
||||
NatureSpot, // 자연 명소
|
||||
WorkPlace, // 작업 장소
|
||||
SacredPlace, // 성스러운 장소
|
||||
SocialPlace, // 사교 장소
|
||||
SpecialPlace // 특별한 장소
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 개별 맵 노드 데이터
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public partial class MapNodeData : Resource
|
||||
{
|
||||
[Export] public string Id { get; set; } = "";
|
||||
[Export] public string Name { get; set; } = "";
|
||||
[Export] public string KoreanName { get; set; } = "";
|
||||
[Export] public string Description { get; set; } = "";
|
||||
[Export] public Vector2 Position { get; set; } = Vector2.Zero;
|
||||
[Export] public NodeType Type { get; set; } = NodeType.NatureSpot;
|
||||
[Export] public string[] ConnectedNodes { get; set; } = new string[0];
|
||||
[Export] public string[] AvailableCharacters { get; set; } = new string[0];
|
||||
[Export] public bool IsUnlocked { get; set; } = true;
|
||||
[Export] public string BackgroundTexturePath { get; set; } = "";
|
||||
[Export] public string IconTexturePath { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 전체 맵 데이터
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public partial class MapData : Resource
|
||||
{
|
||||
[Export] public string Id { get; set; } = "";
|
||||
[Export] public string Name { get; set; } = "";
|
||||
[Export] public string KoreanName { get; set; } = "";
|
||||
[Export] public string Description { get; set; } = "";
|
||||
[Export] public MapNodeData[] Nodes { get; set; } = new MapNodeData[0];
|
||||
[Export] public string StartingNodeId { get; set; } = "";
|
||||
[Export] public string BackgroundMusicPath { get; set; } = "";
|
||||
[Export] public string AmbientSoundPath { get; set; } = "";
|
||||
}
|
||||
}
|
||||
1
moon/scripts/data/MapData.cs.uid
Normal file
1
moon/scripts/data/MapData.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b8bqqsu6vbeoe
|
||||
632
moon/scripts/managers/MapManager.cs
Normal file
632
moon/scripts/managers/MapManager.cs
Normal file
@@ -0,0 +1,632 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using LittleFairy.Data;
|
||||
|
||||
namespace LittleFairy.Managers
|
||||
{
|
||||
/// <summary>
|
||||
/// 노드 기반 맵 시스템을 관리하는 매니저
|
||||
/// </summary>
|
||||
public partial class MapManager : Node
|
||||
{
|
||||
// 시그널 정의
|
||||
[Signal] public delegate void NodeChanged(string nodeId);
|
||||
[Signal] public delegate void MapChanged(string mapId);
|
||||
[Signal] public delegate void NodeUnlocked(string nodeId);
|
||||
|
||||
// 현재 상태
|
||||
public string CurrentMapId { get; private set; } = "";
|
||||
public string CurrentNodeId { get; private set; } = "";
|
||||
public MapData CurrentMap { get; private set; }
|
||||
public MapNodeData CurrentNode { get; private set; }
|
||||
|
||||
// 데이터 저장소
|
||||
private Dictionary<string, MapData> _loadedMaps = new Dictionary<string, MapData>();
|
||||
private Dictionary<string, bool> _nodeUnlockStates = new Dictionary<string, bool>();
|
||||
|
||||
// 노드 및 UI 참조
|
||||
private Control _mapUIContainer;
|
||||
private Control _nodeContainer;
|
||||
private Label _currentLocationLabel;
|
||||
private Button _currentNodeButton;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
GD.Print("MapManager 초기화 중...");
|
||||
SetupUI();
|
||||
LoadMapData();
|
||||
|
||||
// 기본 맵으로 시작 (MapA - 꽃마루)
|
||||
LoadMap("map_a_flower_village");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI 초기 설정
|
||||
/// </summary>
|
||||
private void SetupUI()
|
||||
{
|
||||
// UI 컨테이너 생성
|
||||
_mapUIContainer = new Control();
|
||||
_mapUIContainer.Name = "MapUIContainer";
|
||||
_mapUIContainer.SetAnchorsAndOffsetsPreset(Control.PresetMode.FullRect);
|
||||
AddChild(_mapUIContainer);
|
||||
|
||||
// 현재 위치 표시 라벨
|
||||
_currentLocationLabel = new Label();
|
||||
_currentLocationLabel.Name = "CurrentLocationLabel";
|
||||
_currentLocationLabel.Text = "현재 위치: ";
|
||||
_currentLocationLabel.Position = new Vector2(20, 20);
|
||||
_currentLocationLabel.AddThemeStyleboxOverride("normal", new StyleBoxFlat());
|
||||
_mapUIContainer.AddChild(_currentLocationLabel);
|
||||
|
||||
// 노드 컨테이너
|
||||
_nodeContainer = new Control();
|
||||
_nodeContainer.Name = "NodeContainer";
|
||||
_nodeContainer.SetAnchorsAndOffsetsPreset(Control.PresetMode.FullRect);
|
||||
_mapUIContainer.AddChild(_nodeContainer);
|
||||
|
||||
GD.Print("MapManager UI 설정 완료");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 데이터 로드
|
||||
/// </summary>
|
||||
private void LoadMapData()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 모든 맵 데이터 로드
|
||||
LoadMapFromJson("res://data/maps/map_a_flower_village.json");
|
||||
LoadMapFromJson("res://data/maps/map_b_river_plains.json");
|
||||
LoadMapFromJson("res://data/maps/map_c_maple_valley.json");
|
||||
LoadMapFromJson("res://data/maps/map_d_snow_peak.json");
|
||||
|
||||
GD.Print($"맵 데이터 로드 완료: {_loadedMaps.Count}개 맵");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GD.PrintErr($"맵 데이터 로드 실패: {ex.Message}");
|
||||
CreateFallbackMapData();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 파일에서 맵 데이터 로드
|
||||
/// </summary>
|
||||
/// <param name="filePath">JSON 파일 경로</param>
|
||||
private void LoadMapFromJson(string filePath)
|
||||
{
|
||||
if (!FileAccess.FileExists(filePath))
|
||||
{
|
||||
GD.PrintErr($"맵 파일을 찾을 수 없습니다: {filePath}");
|
||||
return;
|
||||
}
|
||||
|
||||
using var file = FileAccess.Open(filePath, FileAccess.ModeFlags.Read);
|
||||
if (file == null)
|
||||
{
|
||||
GD.PrintErr($"맵 파일을 열 수 없습니다: {filePath}");
|
||||
return;
|
||||
}
|
||||
|
||||
string jsonContent = file.GetAsText();
|
||||
var mapData = JsonToMapData(jsonContent);
|
||||
|
||||
if (mapData != null)
|
||||
{
|
||||
_loadedMaps[mapData.Id] = mapData;
|
||||
GD.Print($"맵 로드 성공: {mapData.KoreanName} ({mapData.Id})");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 문자열을 MapData로 변환
|
||||
/// </summary>
|
||||
/// <param name="jsonContent">JSON 문자열</param>
|
||||
/// <returns>변환된 MapData</returns>
|
||||
private MapData JsonToMapData(string jsonContent)
|
||||
{
|
||||
try
|
||||
{
|
||||
var jsonDict = Json.ParseString(jsonContent).AsGodotDictionary();
|
||||
var mapData = new MapData();
|
||||
|
||||
mapData.Id = jsonDict["id"].AsString();
|
||||
mapData.Name = jsonDict["name"].AsString();
|
||||
mapData.KoreanName = jsonDict["korean_name"].AsString();
|
||||
mapData.Description = jsonDict["description"].AsString();
|
||||
mapData.StartingNodeId = jsonDict["starting_node_id"].AsString();
|
||||
mapData.BackgroundMusicPath = jsonDict.GetValueOrDefault("background_music", "").AsString();
|
||||
mapData.AmbientSoundPath = jsonDict.GetValueOrDefault("ambient_sound", "").AsString();
|
||||
|
||||
var nodesArray = jsonDict["nodes"].AsGodotArray();
|
||||
var nodesList = new List<MapNodeData>();
|
||||
|
||||
foreach (var nodeDict in nodesArray)
|
||||
{
|
||||
var nodeData = JsonToNodeData(nodeDict.AsGodotDictionary());
|
||||
if (nodeData != null)
|
||||
{
|
||||
nodesList.Add(nodeData);
|
||||
}
|
||||
}
|
||||
|
||||
mapData.Nodes = nodesList.ToArray();
|
||||
return mapData;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GD.PrintErr($"JSON 파싱 실패: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 딕셔너리를 NodeData로 변환
|
||||
/// </summary>
|
||||
/// <param name="nodeDict">노드 딕셔너리</param>
|
||||
/// <returns>변환된 NodeData</returns>
|
||||
private MapNodeData JsonToNodeData(Godot.Collections.Dictionary nodeDict)
|
||||
{
|
||||
try
|
||||
{
|
||||
var nodeData = new MapNodeData();
|
||||
|
||||
nodeData.Id = nodeDict["id"].AsString();
|
||||
nodeData.Name = nodeDict["name"].AsString();
|
||||
nodeData.KoreanName = nodeDict["korean_name"].AsString();
|
||||
nodeData.Description = nodeDict["description"].AsString();
|
||||
|
||||
var posArray = nodeDict["position"].AsGodotArray();
|
||||
nodeData.Position = new Vector2(posArray[0].AsSingle(), posArray[1].AsSingle());
|
||||
|
||||
nodeData.Type = (NodeType)Enum.Parse(typeof(NodeType), nodeDict["type"].AsString());
|
||||
|
||||
var connectedArray = nodeDict.GetValueOrDefault("connected_nodes", new Godot.Collections.Array()).AsGodotArray();
|
||||
nodeData.ConnectedNodes = connectedArray.Select(x => x.AsString()).ToArray();
|
||||
|
||||
var charactersArray = nodeDict.GetValueOrDefault("available_characters", new Godot.Collections.Array()).AsGodotArray();
|
||||
nodeData.AvailableCharacters = charactersArray.Select(x => x.AsString()).ToArray();
|
||||
|
||||
nodeData.IsUnlocked = nodeDict.GetValueOrDefault("is_unlocked", true).AsBool();
|
||||
nodeData.BackgroundTexturePath = nodeDict.GetValueOrDefault("background_texture", "").AsString();
|
||||
nodeData.IconTexturePath = nodeDict.GetValueOrDefault("icon_texture", "").AsString();
|
||||
|
||||
return nodeData;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GD.PrintErr($"노드 데이터 파싱 실패: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 폴백 맵 데이터 생성 (JSON 로드 실패 시)
|
||||
/// </summary>
|
||||
private void CreateFallbackMapData()
|
||||
{
|
||||
GD.Print("폴백 맵 데이터 생성 중...");
|
||||
|
||||
var mapData = new MapData();
|
||||
mapData.Id = "map_a_flower_village";
|
||||
mapData.Name = "Spring Mountain Village";
|
||||
mapData.KoreanName = "꽃마루";
|
||||
mapData.Description = "어린 선녀가 처음 깨어나는 아름다운 산골 마을";
|
||||
mapData.StartingNodeId = "fairy_nest";
|
||||
|
||||
var nodes = new List<MapNodeData>();
|
||||
|
||||
// 선녀의 둥지
|
||||
var fairyNest = new MapNodeData();
|
||||
fairyNest.Id = "fairy_nest";
|
||||
fairyNest.Name = "Fairy's Nest";
|
||||
fairyNest.KoreanName = "선녀의 둥지";
|
||||
fairyNest.Description = "작은 선녀가 처음 깨어난 신비로운 둥지";
|
||||
fairyNest.Position = new Vector2(400, 300);
|
||||
fairyNest.Type = NodeType.SacredPlace;
|
||||
fairyNest.ConnectedNodes = new string[] { "village_square", "sacred_spring" };
|
||||
fairyNest.IsUnlocked = true;
|
||||
nodes.Add(fairyNest);
|
||||
|
||||
// 마을 광장
|
||||
var villageSquare = new MapNodeData();
|
||||
villageSquare.Id = "village_square";
|
||||
villageSquare.Name = "Village Square";
|
||||
villageSquare.KoreanName = "마을 광장";
|
||||
villageSquare.Description = "꽃마루 마을의 중심지, 사람들이 모이는 곳";
|
||||
villageSquare.Position = new Vector2(600, 400);
|
||||
villageSquare.Type = NodeType.SocialPlace;
|
||||
villageSquare.ConnectedNodes = new string[] { "fairy_nest", "grandmother_house", "withered_garden" };
|
||||
villageSquare.AvailableCharacters = new string[] { "boy_haechan" };
|
||||
villageSquare.IsUnlocked = true;
|
||||
nodes.Add(villageSquare);
|
||||
|
||||
// 할머니 집
|
||||
var grandmotherHouse = new MapNodeData();
|
||||
grandmotherHouse.Id = "grandmother_house";
|
||||
grandmotherHouse.Name = "Grandmother's House";
|
||||
grandmotherHouse.KoreanName = "할머니 집";
|
||||
grandmotherHouse.Description = "따뜻한 마음의 달빛 할머니가 사는 한옥";
|
||||
grandmotherHouse.Position = new Vector2(800, 300);
|
||||
grandmotherHouse.Type = NodeType.Residence;
|
||||
grandmotherHouse.ConnectedNodes = new string[] { "village_square", "secret_path" };
|
||||
grandmotherHouse.AvailableCharacters = new string[] { "grandmother_dalbit" };
|
||||
grandmotherHouse.IsUnlocked = true;
|
||||
nodes.Add(grandmotherHouse);
|
||||
|
||||
// 시든 꽃밭
|
||||
var witheredGarden = new MapNodeData();
|
||||
witheredGarden.Id = "withered_garden";
|
||||
witheredGarden.Name = "Withered Flower Garden";
|
||||
witheredGarden.KoreanName = "시든 꽃밭";
|
||||
witheredGarden.Description = "한때 아름다웠던 꽃밭, 이제는 치유가 필요한 곳";
|
||||
witheredGarden.Position = new Vector2(700, 500);
|
||||
witheredGarden.Type = NodeType.NatureSpot;
|
||||
witheredGarden.ConnectedNodes = new string[] { "village_square", "woodcutter_cabin" };
|
||||
witheredGarden.AvailableCharacters = new string[] { "spirit_flownim" };
|
||||
witheredGarden.IsUnlocked = true;
|
||||
nodes.Add(witheredGarden);
|
||||
|
||||
// 나무꾼 오두막
|
||||
var woodcutterCabin = new MapNodeData();
|
||||
woodcutterCabin.Id = "woodcutter_cabin";
|
||||
woodcutterCabin.Name = "Woodcutter's Cabin";
|
||||
woodcutterCabin.KoreanName = "나무꾼 오두막";
|
||||
woodcutterCabin.Description = "석주 나무꾼이 사는 소박한 오두막";
|
||||
woodcutterCabin.Position = new Vector2(500, 600);
|
||||
woodcutterCabin.Type = NodeType.WorkPlace;
|
||||
woodcutterCabin.ConnectedNodes = new string[] { "withered_garden", "sacred_spring" };
|
||||
woodcutterCabin.AvailableCharacters = new string[] { "woodcutter_seokju" };
|
||||
woodcutterCabin.IsUnlocked = true;
|
||||
nodes.Add(woodcutterCabin);
|
||||
|
||||
// 성스러운 샘
|
||||
var sacredSpring = new MapNodeData();
|
||||
sacredSpring.Id = "sacred_spring";
|
||||
sacredSpring.Name = "Sacred Spring";
|
||||
sacredSpring.KoreanName = "성스러운 샘";
|
||||
sacredSpring.Description = "맑고 신성한 기운이 흐르는 샘";
|
||||
sacredSpring.Position = new Vector2(300, 500);
|
||||
sacredSpring.Type = NodeType.SacredPlace;
|
||||
sacredSpring.ConnectedNodes = new string[] { "fairy_nest", "woodcutter_cabin", "secret_path" };
|
||||
sacredSpring.IsUnlocked = true;
|
||||
nodes.Add(sacredSpring);
|
||||
|
||||
// 비밀 숲길
|
||||
var secretPath = new MapNodeData();
|
||||
secretPath.Id = "secret_path";
|
||||
secretPath.Name = "Secret Forest Path";
|
||||
secretPath.KoreanName = "비밀 숲길";
|
||||
secretPath.Description = "숨겨진 신비로운 숲길, 특별한 곳으로 이어진다";
|
||||
secretPath.Position = new Vector2(900, 400);
|
||||
secretPath.Type = NodeType.SpecialPlace;
|
||||
secretPath.ConnectedNodes = new string[] { "grandmother_house", "sacred_spring" };
|
||||
secretPath.IsUnlocked = false; // 처음에는 잠겨있음
|
||||
nodes.Add(secretPath);
|
||||
|
||||
mapData.Nodes = nodes.ToArray();
|
||||
_loadedMaps[mapData.Id] = mapData;
|
||||
|
||||
GD.Print("폴백 맵 데이터 생성 완료");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 로드 및 활성화
|
||||
/// </summary>
|
||||
/// <param name="mapId">로드할 맵 ID</param>
|
||||
public void LoadMap(string mapId)
|
||||
{
|
||||
if (!_loadedMaps.ContainsKey(mapId))
|
||||
{
|
||||
GD.PrintErr($"맵을 찾을 수 없습니다: {mapId}");
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentMap = _loadedMaps[mapId];
|
||||
CurrentMapId = mapId;
|
||||
|
||||
// 시작 노드로 이동
|
||||
if (!string.IsNullOrEmpty(CurrentMap.StartingNodeId))
|
||||
{
|
||||
MoveToNode(CurrentMap.StartingNodeId);
|
||||
}
|
||||
else if (CurrentMap.Nodes.Length > 0)
|
||||
{
|
||||
MoveToNode(CurrentMap.Nodes[0].Id);
|
||||
}
|
||||
|
||||
// UI 업데이트
|
||||
UpdateMapUI();
|
||||
|
||||
EmitSignal(SignalName.MapChanged, mapId);
|
||||
GD.Print($"맵 로드 완료: {CurrentMap.KoreanName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 노드로 이동
|
||||
/// </summary>
|
||||
/// <param name="nodeId">이동할 노드 ID</param>
|
||||
public void MoveToNode(string nodeId)
|
||||
{
|
||||
if (CurrentMap == null)
|
||||
{
|
||||
GD.PrintErr("활성화된 맵이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
var targetNode = CurrentMap.Nodes.FirstOrDefault(n => n.Id == nodeId);
|
||||
if (targetNode == null)
|
||||
{
|
||||
GD.PrintErr($"노드를 찾을 수 없습니다: {nodeId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 잠긴 노드인지 확인
|
||||
if (!IsNodeUnlocked(nodeId))
|
||||
{
|
||||
GD.Print($"잠긴 노드입니다: {targetNode.KoreanName}");
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentNode = targetNode;
|
||||
CurrentNodeId = nodeId;
|
||||
|
||||
// UI 업데이트
|
||||
UpdateCurrentLocationLabel();
|
||||
UpdateNodeButtons();
|
||||
|
||||
EmitSignal(SignalName.NodeChanged, nodeId);
|
||||
GD.Print($"노드 이동: {targetNode.KoreanName} ({nodeId})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 잠금 해제 상태 확인
|
||||
/// </summary>
|
||||
/// <param name="nodeId">확인할 노드 ID</param>
|
||||
/// <returns>잠금 해제 여부</returns>
|
||||
public bool IsNodeUnlocked(string nodeId)
|
||||
{
|
||||
var node = CurrentMap?.Nodes.FirstOrDefault(n => n.Id == nodeId);
|
||||
if (node == null) return false;
|
||||
|
||||
// 기본 설정 확인
|
||||
if (!node.IsUnlocked) return false;
|
||||
|
||||
// 런타임 잠금 상태 확인
|
||||
string key = $"{CurrentMapId}_{nodeId}";
|
||||
return _nodeUnlockStates.GetValueOrDefault(key, node.IsUnlocked);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 잠금 해제
|
||||
/// </summary>
|
||||
/// <param name="nodeId">잠금 해제할 노드 ID</param>
|
||||
public void UnlockNode(string nodeId)
|
||||
{
|
||||
string key = $"{CurrentMapId}_{nodeId}";
|
||||
_nodeUnlockStates[key] = true;
|
||||
|
||||
UpdateNodeButtons();
|
||||
EmitSignal(SignalName.NodeUnlocked, nodeId);
|
||||
|
||||
var node = CurrentMap?.Nodes.FirstOrDefault(n => n.Id == nodeId);
|
||||
GD.Print($"노드 잠금 해제: {node?.KoreanName ?? nodeId}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 노드에서 이동 가능한 노드 목록 반환
|
||||
/// </summary>
|
||||
/// <returns>이동 가능한 노드 ID 목록</returns>
|
||||
public string[] GetAvailableConnections()
|
||||
{
|
||||
if (CurrentNode == null) return new string[0];
|
||||
|
||||
return CurrentNode.ConnectedNodes
|
||||
.Where(nodeId => IsNodeUnlocked(nodeId))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 UI 업데이트
|
||||
/// </summary>
|
||||
private void UpdateMapUI()
|
||||
{
|
||||
if (CurrentMap == null) return;
|
||||
|
||||
// 기존 노드 버튼들 제거
|
||||
foreach (Node child in _nodeContainer.GetChildren())
|
||||
{
|
||||
child.QueueFree();
|
||||
}
|
||||
|
||||
// 새 노드 버튼들 생성
|
||||
foreach (var node in CurrentMap.Nodes)
|
||||
{
|
||||
CreateNodeButton(node);
|
||||
}
|
||||
|
||||
UpdateCurrentLocationLabel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 버튼 생성
|
||||
/// </summary>
|
||||
/// <param name="nodeData">노드 데이터</param>
|
||||
private void CreateNodeButton(MapNodeData nodeData)
|
||||
{
|
||||
var button = new Button();
|
||||
button.Name = $"NodeButton_{nodeData.Id}";
|
||||
button.Text = nodeData.KoreanName;
|
||||
button.Position = nodeData.Position;
|
||||
button.Size = new Vector2(120, 40);
|
||||
|
||||
// 버튼 스타일 설정
|
||||
var styleBox = new StyleBoxFlat();
|
||||
if (IsNodeUnlocked(nodeData.Id))
|
||||
{
|
||||
styleBox.BgColor = nodeData.Id == CurrentNodeId ? Colors.LightBlue : Colors.LightGreen;
|
||||
}
|
||||
else
|
||||
{
|
||||
styleBox.BgColor = Colors.Gray;
|
||||
button.Disabled = true;
|
||||
}
|
||||
|
||||
button.AddThemeStyleboxOverride("normal", styleBox);
|
||||
|
||||
// 클릭 이벤트 연결
|
||||
button.Pressed += () => OnNodeButtonPressed(nodeData.Id);
|
||||
|
||||
_nodeContainer.AddChild(button);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 버튼 클릭 이벤트 처리
|
||||
/// </summary>
|
||||
/// <param name="nodeId">클릭된 노드 ID</param>
|
||||
private void OnNodeButtonPressed(string nodeId)
|
||||
{
|
||||
// 현재 노드에서 이동 가능한지 확인
|
||||
if (CurrentNode != null)
|
||||
{
|
||||
if (nodeId == CurrentNodeId)
|
||||
{
|
||||
GD.Print("이미 현재 위치입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CurrentNode.ConnectedNodes.Contains(nodeId))
|
||||
{
|
||||
GD.Print("연결되지 않은 노드입니다.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
MoveToNode(nodeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 위치 라벨 업데이트
|
||||
/// </summary>
|
||||
private void UpdateCurrentLocationLabel()
|
||||
{
|
||||
if (CurrentNode != null && _currentLocationLabel != null)
|
||||
{
|
||||
_currentLocationLabel.Text = $"현재 위치: {CurrentNode.KoreanName} ({CurrentNode.Name})";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 버튼들 상태 업데이트
|
||||
/// </summary>
|
||||
private void UpdateNodeButtons()
|
||||
{
|
||||
foreach (Node child in _nodeContainer.GetChildren())
|
||||
{
|
||||
if (child is Button button && child.Name.AsString().StartsWith("NodeButton_"))
|
||||
{
|
||||
string nodeId = child.Name.AsString().Replace("NodeButton_", "");
|
||||
|
||||
var styleBox = new StyleBoxFlat();
|
||||
if (IsNodeUnlocked(nodeId))
|
||||
{
|
||||
button.Disabled = false;
|
||||
styleBox.BgColor = nodeId == CurrentNodeId ? Colors.LightBlue : Colors.LightGreen;
|
||||
}
|
||||
else
|
||||
{
|
||||
button.Disabled = true;
|
||||
styleBox.BgColor = Colors.Gray;
|
||||
}
|
||||
|
||||
button.AddThemeStyleboxOverride("normal", styleBox);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 키보드 입력 처리
|
||||
/// </summary>
|
||||
public override void _Input(InputEvent @event)
|
||||
{
|
||||
if (@event is InputEventKey keyEvent && keyEvent.Pressed)
|
||||
{
|
||||
var connections = GetAvailableConnections();
|
||||
|
||||
switch (keyEvent.Keycode)
|
||||
{
|
||||
case Key.Up:
|
||||
MoveInDirection(Vector2.Up, connections);
|
||||
break;
|
||||
case Key.Down:
|
||||
MoveInDirection(Vector2.Down, connections);
|
||||
break;
|
||||
case Key.Left:
|
||||
MoveInDirection(Vector2.Left, connections);
|
||||
break;
|
||||
case Key.Right:
|
||||
MoveInDirection(Vector2.Right, connections);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 방향키에 따른 이동 처리
|
||||
/// </summary>
|
||||
/// <param name="direction">이동 방향</param>
|
||||
/// <param name="availableConnections">이동 가능한 연결 목록</param>
|
||||
private void MoveInDirection(Vector2 direction, string[] availableConnections)
|
||||
{
|
||||
if (CurrentNode == null || availableConnections.Length == 0) return;
|
||||
|
||||
MapNodeData closestNode = null;
|
||||
float bestScore = float.MaxValue;
|
||||
|
||||
foreach (string connectionId in availableConnections)
|
||||
{
|
||||
var connectionNode = CurrentMap.Nodes.FirstOrDefault(n => n.Id == connectionId);
|
||||
if (connectionNode == null) continue;
|
||||
|
||||
Vector2 toConnection = (connectionNode.Position - CurrentNode.Position).Normalized();
|
||||
float dot = direction.Dot(toConnection);
|
||||
float distance = CurrentNode.Position.DistanceTo(connectionNode.Position);
|
||||
|
||||
// 방향성과 거리를 종합한 점수 (방향성 우선)
|
||||
float score = (1 - dot) * 1000 + distance;
|
||||
|
||||
if (dot > 0.3f && score < bestScore) // 최소 30도 범위 내
|
||||
{
|
||||
bestScore = score;
|
||||
closestNode = connectionNode;
|
||||
}
|
||||
}
|
||||
|
||||
if (closestNode != null)
|
||||
{
|
||||
MoveToNode(closestNode.Id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 노드의 정보 반환
|
||||
/// </summary>
|
||||
/// <returns>현재 노드 정보</returns>
|
||||
public MapNodeData GetCurrentNodeInfo()
|
||||
{
|
||||
return CurrentNode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 맵의 정보 반환
|
||||
/// </summary>
|
||||
/// <returns>현재 맵 정보</returns>
|
||||
public MapData GetCurrentMapInfo()
|
||||
{
|
||||
return CurrentMap;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
moon/scripts/managers/MapManager.cs.uid
Normal file
1
moon/scripts/managers/MapManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bwyfe62ypechv
|
||||
Reference in New Issue
Block a user