From 5a7f920e1d9cb7b424ac748c0dae5cdde207471b Mon Sep 17 00:00:00 2001 From: ened Date: Sat, 16 Aug 2025 19:00:01 +0900 Subject: [PATCH] implement initial map system (no assets) --- moon/README.md | 140 +++++ moon/data/maps/map_a_flower_village.json | 102 ++++ moon/data/maps/map_b_river_plains.json | 89 ++++ moon/data/maps/map_c_maple_valley.json | 89 ++++ moon/data/maps/map_d_snow_peak.json | 63 +++ moon/scenes/MainGame.tscn | 9 + moon/scripts/MainGame.cs | 209 ++++++++ moon/scripts/MainGame.cs.uid | 1 + moon/scripts/data/MapData.cs | 53 ++ moon/scripts/data/MapData.cs.uid | 1 + moon/scripts/managers/MapManager.cs | 632 +++++++++++++++++++++++ moon/scripts/managers/MapManager.cs.uid | 1 + 12 files changed, 1389 insertions(+) create mode 100644 moon/README.md create mode 100644 moon/data/maps/map_a_flower_village.json create mode 100644 moon/data/maps/map_b_river_plains.json create mode 100644 moon/data/maps/map_c_maple_valley.json create mode 100644 moon/data/maps/map_d_snow_peak.json create mode 100644 moon/scenes/MainGame.tscn create mode 100644 moon/scripts/MainGame.cs create mode 100644 moon/scripts/MainGame.cs.uid create mode 100644 moon/scripts/data/MapData.cs create mode 100644 moon/scripts/data/MapData.cs.uid create mode 100644 moon/scripts/managers/MapManager.cs create mode 100644 moon/scripts/managers/MapManager.cs.uid diff --git a/moon/README.md b/moon/README.md new file mode 100644 index 0000000..bfb7176 --- /dev/null +++ b/moon/README.md @@ -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 게임의 핵심 이동 시스템으로, 한국적 정서가 담긴 아름다운 장소들을 노드 기반으로 연결하여 플레이어가 직관적이고 의미있는 이동 경험을 할 수 있도록 설계되었습니다.** \ No newline at end of file diff --git a/moon/data/maps/map_a_flower_village.json b/moon/data/maps/map_a_flower_village.json new file mode 100644 index 0000000..84d71b9 --- /dev/null +++ b/moon/data/maps/map_a_flower_village.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/moon/data/maps/map_b_river_plains.json b/moon/data/maps/map_b_river_plains.json new file mode 100644 index 0000000..6820be7 --- /dev/null +++ b/moon/data/maps/map_b_river_plains.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/moon/data/maps/map_c_maple_valley.json b/moon/data/maps/map_c_maple_valley.json new file mode 100644 index 0000000..5caca2b --- /dev/null +++ b/moon/data/maps/map_c_maple_valley.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/moon/data/maps/map_d_snow_peak.json b/moon/data/maps/map_d_snow_peak.json new file mode 100644 index 0000000..81dba51 --- /dev/null +++ b/moon/data/maps/map_d_snow_peak.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/moon/scenes/MainGame.tscn b/moon/scenes/MainGame.tscn new file mode 100644 index 0000000..0ac3e71 --- /dev/null +++ b/moon/scenes/MainGame.tscn @@ -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) diff --git a/moon/scripts/MainGame.cs b/moon/scripts/MainGame.cs new file mode 100644 index 0000000..51ffe81 --- /dev/null +++ b/moon/scripts/MainGame.cs @@ -0,0 +1,209 @@ +using Godot; +using LittleFairy.Managers; + +namespace LittleFairy +{ + /// + /// 게임의 메인 씬 매니저 + /// + 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("메인 게임 초기화 완료"); + } + + /// + /// UI 초기 설정 + /// + 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); + } + + /// + /// 노드 변경 이벤트 처리 + /// + /// 변경된 노드 ID + private void OnNodeChanged(string nodeId) + { + UpdateInfoPanel(); + GD.Print($"노드 변경 이벤트: {nodeId}"); + } + + /// + /// 맵 변경 이벤트 처리 + /// + /// 변경된 맵 ID + private void OnMapChanged(string mapId) + { + UpdateInfoPanel(); + GD.Print($"맵 변경 이벤트: {mapId}"); + } + + /// + /// 노드 잠금 해제 이벤트 처리 + /// + /// 잠금 해제된 노드 ID + private void OnNodeUnlocked(string nodeId) + { + UpdateInfoPanel(); + GD.Print($"노드 잠금 해제 이벤트: {nodeId}"); + } + + /// + /// 정보 패널 업데이트 + /// + 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}"; + } + } + + /// + /// 비밀길 해제 버튼 클릭 이벤트 + /// + private void OnUnlockSecretButtonPressed() + { + _mapManager?.UnlockNode("secret_path"); + GD.Print("비밀길 잠금 해제됨"); + } + + /// + /// 맵 선택 이벤트 처리 + /// + /// 선택된 맵 인덱스 + 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]}"); + } + } + } +} \ No newline at end of file diff --git a/moon/scripts/MainGame.cs.uid b/moon/scripts/MainGame.cs.uid new file mode 100644 index 0000000..2519fa1 --- /dev/null +++ b/moon/scripts/MainGame.cs.uid @@ -0,0 +1 @@ +uid://bey6df0q5laf5 diff --git a/moon/scripts/data/MapData.cs b/moon/scripts/data/MapData.cs new file mode 100644 index 0000000..f91aa8c --- /dev/null +++ b/moon/scripts/data/MapData.cs @@ -0,0 +1,53 @@ +using Godot; +using System.Collections.Generic; + +namespace LittleFairy.Data +{ + /// + /// 맵 노드의 종류 + /// + public enum NodeType + { + Residence, // 거주지 + NatureSpot, // 자연 명소 + WorkPlace, // 작업 장소 + SacredPlace, // 성스러운 장소 + SocialPlace, // 사교 장소 + SpecialPlace // 특별한 장소 + } + + /// + /// 개별 맵 노드 데이터 + /// + [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; } = ""; + } + + /// + /// 전체 맵 데이터 + /// + [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; } = ""; + } +} \ No newline at end of file diff --git a/moon/scripts/data/MapData.cs.uid b/moon/scripts/data/MapData.cs.uid new file mode 100644 index 0000000..e35fb93 --- /dev/null +++ b/moon/scripts/data/MapData.cs.uid @@ -0,0 +1 @@ +uid://b8bqqsu6vbeoe diff --git a/moon/scripts/managers/MapManager.cs b/moon/scripts/managers/MapManager.cs new file mode 100644 index 0000000..17cd33f --- /dev/null +++ b/moon/scripts/managers/MapManager.cs @@ -0,0 +1,632 @@ +using Godot; +using System; +using System.Collections.Generic; +using System.Linq; +using LittleFairy.Data; + +namespace LittleFairy.Managers +{ + /// + /// 노드 기반 맵 시스템을 관리하는 매니저 + /// + 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 _loadedMaps = new Dictionary(); + private Dictionary _nodeUnlockStates = new Dictionary(); + + // 노드 및 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"); + } + + /// + /// UI 초기 설정 + /// + 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 설정 완료"); + } + + /// + /// 맵 데이터 로드 + /// + 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(); + } + } + + /// + /// JSON 파일에서 맵 데이터 로드 + /// + /// JSON 파일 경로 + 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})"); + } + } + + /// + /// JSON 문자열을 MapData로 변환 + /// + /// JSON 문자열 + /// 변환된 MapData + 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(); + + 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; + } + } + + /// + /// JSON 딕셔너리를 NodeData로 변환 + /// + /// 노드 딕셔너리 + /// 변환된 NodeData + 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; + } + } + + /// + /// 폴백 맵 데이터 생성 (JSON 로드 실패 시) + /// + 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(); + + // 선녀의 둥지 + 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("폴백 맵 데이터 생성 완료"); + } + + /// + /// 맵 로드 및 활성화 + /// + /// 로드할 맵 ID + 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}"); + } + + /// + /// 지정된 노드로 이동 + /// + /// 이동할 노드 ID + 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})"); + } + + /// + /// 노드 잠금 해제 상태 확인 + /// + /// 확인할 노드 ID + /// 잠금 해제 여부 + 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); + } + + /// + /// 노드 잠금 해제 + /// + /// 잠금 해제할 노드 ID + 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}"); + } + + /// + /// 현재 노드에서 이동 가능한 노드 목록 반환 + /// + /// 이동 가능한 노드 ID 목록 + public string[] GetAvailableConnections() + { + if (CurrentNode == null) return new string[0]; + + return CurrentNode.ConnectedNodes + .Where(nodeId => IsNodeUnlocked(nodeId)) + .ToArray(); + } + + /// + /// 맵 UI 업데이트 + /// + private void UpdateMapUI() + { + if (CurrentMap == null) return; + + // 기존 노드 버튼들 제거 + foreach (Node child in _nodeContainer.GetChildren()) + { + child.QueueFree(); + } + + // 새 노드 버튼들 생성 + foreach (var node in CurrentMap.Nodes) + { + CreateNodeButton(node); + } + + UpdateCurrentLocationLabel(); + } + + /// + /// 노드 버튼 생성 + /// + /// 노드 데이터 + 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); + } + + /// + /// 노드 버튼 클릭 이벤트 처리 + /// + /// 클릭된 노드 ID + private void OnNodeButtonPressed(string nodeId) + { + // 현재 노드에서 이동 가능한지 확인 + if (CurrentNode != null) + { + if (nodeId == CurrentNodeId) + { + GD.Print("이미 현재 위치입니다."); + return; + } + + if (!CurrentNode.ConnectedNodes.Contains(nodeId)) + { + GD.Print("연결되지 않은 노드입니다."); + return; + } + } + + MoveToNode(nodeId); + } + + /// + /// 현재 위치 라벨 업데이트 + /// + private void UpdateCurrentLocationLabel() + { + if (CurrentNode != null && _currentLocationLabel != null) + { + _currentLocationLabel.Text = $"현재 위치: {CurrentNode.KoreanName} ({CurrentNode.Name})"; + } + } + + /// + /// 노드 버튼들 상태 업데이트 + /// + 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); + } + } + } + + /// + /// 키보드 입력 처리 + /// + 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; + } + } + } + + /// + /// 방향키에 따른 이동 처리 + /// + /// 이동 방향 + /// 이동 가능한 연결 목록 + 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); + } + } + + /// + /// 현재 노드의 정보 반환 + /// + /// 현재 노드 정보 + public MapNodeData GetCurrentNodeInfo() + { + return CurrentNode; + } + + /// + /// 현재 맵의 정보 반환 + /// + /// 현재 맵 정보 + public MapData GetCurrentMapInfo() + { + return CurrentMap; + } + } +} \ No newline at end of file diff --git a/moon/scripts/managers/MapManager.cs.uid b/moon/scripts/managers/MapManager.cs.uid new file mode 100644 index 0000000..cdbdbc3 --- /dev/null +++ b/moon/scripts/managers/MapManager.cs.uid @@ -0,0 +1 @@ +uid://bwyfe62ypechv