Commit 017f6bfe authored by Gavin An's avatar Gavin An

샌드박스 구성

parent 65e74a22
# Behavioral Rules for Antigravity
- **[중요] 절대로 사용자의 편집기 설정(Editor Settings), 프로젝트 설정(Project Settings) 또는 시스템 설정 파일을 명시적인 요청이 없는 한 마음대로 변경하지 마십시오.**
- **Do NOT modify editor configurations, layout settings, or project-wide system settings unless explicitly and clearly requested by the user.**
[gd_resource type="AnimationLibrary" format=3]
[gd_resource type="AnimationLibrary" format=3 uid="uid://wdrqljuuoobi"]
[sub_resource type="Animation" id="Animation_yls3e"]
resource_name = "Right_Hand_Sword_Slash"
......
......@@ -24,6 +24,10 @@
- [x] 스테이지 제어 엔진 (`src/core/stage_manager.gd`) 작성
- 1초 간격 몬스터 스폰 타이머 관리 및 스폰 수량 통제
- 스테이지별 몬스터 체력 및 처치 골드 스케일링 공식 구현
- 스폰 및 스테이지 시작 대기 시간(쿨다운) 타이머를 정지/재개시키는 `toggle_spawning_pause()` 일시 정지 로직 추가
- [x] HUD UI (`src/ui/hud.gd`) 리팩토링 및 멈춤 버튼 추가
- 런타임에 소환 버튼 옆에 `PauseSpawningButton`을 동적 생성 및 배치
- 버튼 클릭 시 `StageManager`의 스폰 제어기와 연결되어 텍스트를 `"Pause Spawning"``"Resume Spawning"` 으로 상호 토글 및 리셋(재시작 시) 연동 완료
### [x] 3단계: 몬스터 시스템 구현
- [x] 몬스터 캐릭터 씬 및 비주얼 구체 메쉬 생성 (`src/entities/monsters/base_monster.tscn`)
......@@ -35,6 +39,7 @@
- [x] 런타임 내비게이션 맵 생성을 위한 `NavigationRegion3D` 탑재 및 자동 베이킹
- [x] RTS 드래그 다중 선택 제어기 구현 (`src/core/unit_controller.gd`)
- 화면 마우스 좌클릭 드래그 박스 2D 렌더링 및 3D 공간 투영(Unproject) 다중 유닛 수집
- 키보드 'H' 키 입력을 통한 선택 유닛 제자리 고정대기(Hold Position) 명령 기능 구현 완료
- [x] 우클릭 지점 대한 유닛 분산 격자 정렬 이동 및 몬스터 대상 타겟 어택 명령 하달 구현
### [x] 5단계: 유닛 클래스 정의 및 전투 구현
......@@ -43,6 +48,7 @@
- [x] 유닛 행동 및 전투 인공지능 스크립트 작성 (`src/entities/units/base_unit.gd`)
- 네비게이션 에이전트(`NavigationAgent3D`)를 활용한 맵 경로 이동
- 정지 상태 중 사거리 내의 가까운 적 자동 탐지 및 쿨타임 공격 연산 루프 구현
- 홀드 상태(`_is_holding`)에서 추격을 전면 억제하고 오직 자신의 공격 사거리 내에 들어온 몬스터만 수비 타겟팅하도록 AI 알고리즘 보완 완료 (수동 우클릭 지시 시 홀드 상태 자동 해제)
### [x] 6단계: UI 및 소환 시스템
- [x] HUD UI 인터페이스 구축 (`src/ui/hud.tscn`)
......@@ -65,7 +71,7 @@
- [x] `res://src/entities/units/guardian/` 폴더 내에 `guardian_unit.tscn` 씬을 설계 및 신설하여 가디언 모델의 스킨 및 계층 구조 연동 완료
- [x] `hud.gd` 스크립트를 수정하여 기존의 중복된 유닛 ID 버그를 해결(Striker 외형으로만 고정 스폰되던 현상 픽스)하고, Guardian 유닛 소환 시 전용 `guardian_unit.tscn`이 스폰되도록 연동 완료
- [x] 사용자가 직접 `striker_unit.tscn`에서 모델 Y축 회전 보정(180도)을 조치한 내용을 인지하고, `base_unit.gd` 스크립트 상에서 불필요해진 수동 회전 보정 코드부를 완전히 삭제하여 씬 지향적 설정을 완수
- [x] Striker 유닛의 뼈대 스켈레톤의 오른손 본(`RightHand`)에 `BoneAttachment3D` 노드를 런타임 동적 배치하고, `azure_blade.fbx` 검 무기를 텍스처 오버라이드 매핑(`azure_blade_0.png`)과 방향/크기 보정을 더해 안정적으로 쥐어주는 런타임 무기 장착 시스템 구현 완료
- [x] Striker 유닛의 뼈대 스켈레톤의 오른손 본(`RightHand`)에 `BoneAttachment3D` 노드를 런타임 동적 배치하고, `azure_blade.fbx` 검 무기를 텍스처 오버라이드 매핑(`azure_blade_0.png`)과 방향/크기 보정을 더해 안정적으로 쥐어주는 런타임 무기 장착 시스템 구현 완료 (스케일 150배 확대 적용 및 Z축 -0.6m 손잡이 쥠 정밀 오프셋 보정 완료)
- [x] 데이터와 UI/제어 로직의 느슨한 결합(Loose Coupling)을 위해 데이터 매니저 싱글톤 `DataManager` (`res://src/autoload/data_manager.gd`)을 신설하고 오토로드에 등록
- [x] `hud.gd`에 하드코딩되어 있던 5종 유닛 최초 세팅 데이터와 `stage_manager.gd`에 정의되어 있던 몬스터 스폰 스펙(HP 스케일링 공식 및 골드 리워드 테이블)을 모두 `DataManager` 싱글톤으로 온전히 이전 및 리팩토링 완료
......
......@@ -20,6 +20,10 @@ config/icon="res://icon.svg"
GameManager="*res://src/autoload/game_manager.gd"
DataManager="*res://src/autoload/data_manager.gd"
[display]
window/size/mode=2
[physics]
3d/physics_engine="Jolt Physics"
......
......@@ -23,52 +23,52 @@ func _initialize_unit_templates() -> void:
unit_templates.append(striker)
# 2. Guardian (밸런스형 근접 유닛)
var guardian: UnitData = UnitData.new()
guardian.unit_id = "Unit_Guardian"
guardian.unit_name = "Guardian"
guardian.damage = 25.0
guardian.cooldown = 1.0
guardian.move_speed = 4.0
guardian.attack_range = 1.5
guardian.is_ranged = false
guardian.unit_color = Color(0.85, 0.65, 0.12, 1.0) # 황금색
unit_templates.append(guardian)
#var guardian: UnitData = UnitData.new()
#guardian.unit_id = "Unit_Guardian"
#guardian.unit_name = "Guardian"
#guardian.damage = 25.0
#guardian.cooldown = 1.0
#guardian.move_speed = 4.0
#guardian.attack_range = 1.5
#guardian.is_ranged = false
#guardian.unit_color = Color(0.85, 0.65, 0.12, 1.0) # 황금색
#unit_templates.append(guardian)
# 3. Ranger (원거리 유닛)
var ranger: UnitData = UnitData.new()
ranger.unit_id = "Unit_Ranger"
ranger.unit_name = "Ranger"
ranger.damage = 18.0
ranger.cooldown = 0.8
ranger.move_speed = 4.5
ranger.attack_range = 10.0
ranger.is_ranged = true
ranger.unit_color = Color(0.12, 0.75, 0.25, 1.0) # 녹색
unit_templates.append(ranger)
#var ranger: UnitData = UnitData.new()
#ranger.unit_id = "Unit_Ranger"
#ranger.unit_name = "Ranger"
#ranger.damage = 18.0
#ranger.cooldown = 0.8
#ranger.move_speed = 4.5
#ranger.attack_range = 10.0
#ranger.is_ranged = true
#ranger.unit_color = Color(0.12, 0.75, 0.25, 1.0) # 녹색
#unit_templates.append(ranger)
# 4. Slayer (고화력 대형 유닛)
var slayer: UnitData = UnitData.new()
slayer.unit_id = "Unit_Slayer"
slayer.unit_name = "Slayer"
slayer.damage = 55.0
slayer.cooldown = 1.8
slayer.move_speed = 3.5
slayer.attack_range = 1.6
slayer.is_ranged = false
slayer.unit_color = Color(0.85, 0.12, 0.12, 1.0) # 진적색
unit_templates.append(slayer)
#var slayer: UnitData = UnitData.new()
#slayer.unit_id = "Unit_Slayer"
#slayer.unit_name = "Slayer"
#slayer.damage = 55.0
#slayer.cooldown = 1.8
#slayer.move_speed = 3.5
#slayer.attack_range = 1.6
#slayer.is_ranged = false
#slayer.unit_color = Color(0.85, 0.12, 0.12, 1.0) # 진적색
#unit_templates.append(slayer)
# 5. Archmage (극단적 극딜 마법 유닛)
var archmage: UnitData = UnitData.new()
archmage.unit_id = "Unit_Archmage"
archmage.unit_name = "Archmage"
archmage.damage = 110.0
archmage.cooldown = 3.0
archmage.move_speed = 3.0
archmage.attack_range = 12.0
archmage.is_ranged = true
archmage.unit_color = Color(0.55, 0.12, 0.85, 1.0) # 보라색
unit_templates.append(archmage)
#var archmage: UnitData = UnitData.new()
#archmage.unit_id = "Unit_Archmage"
#archmage.unit_name = "Archmage"
#archmage.damage = 110.0
#archmage.cooldown = 3.0
#archmage.move_speed = 3.0
#archmage.attack_range = 12.0
#archmage.is_ranged = true
#archmage.unit_color = Color(0.55, 0.12, 0.85, 1.0) # 보라색
#unit_templates.append(archmage)
## 특정 스테이지에 따른 몬스터 체력(HP) 반환
func get_monster_hp(stage: int) -> float:
......
......@@ -9,8 +9,10 @@ signal active_monster_count_changed(new_count: int)
signal game_lost
signal game_victory
const INIT_GOLD: int = 30000
# 글로벌 플레이어 상태 변수 (정적 타이핑 적용)
var player_gold: int = 30000:
var player_gold: int = INIT_GOLD:
set(value):
player_gold = value
gold_changed.emit(player_gold)
......@@ -89,7 +91,33 @@ func _check_victory_condition() -> void:
## 게임 리셋 기능 (테스트 및 재시작용)
func reset_game() -> void:
is_game_over = false
player_gold = 300
player_gold = INIT_GOLD
unit_spawn_cost = 100
current_stage = 1
active_monster_count = 0
## 게임 완전히 재시작 (모든 유닛/몬스터 제거 및 씬 리로드)
func restart_game() -> void:
# 필드 위 모든 유닛 제거
var units: Array[Node] = get_tree().get_nodes_in_group("units")
for unit: Node in units:
if is_instance_valid(unit):
unit.queue_free()
# 필드 위 모든 몬스터 제거
var monsters: Array[Node] = get_tree().get_nodes_in_group("monsters")
for monster: Node in monsters:
if is_instance_valid(monster):
monster.queue_free()
# 상태 리셋 후 현재 씬 리로드
reset_game()
get_tree().reload_current_scene()
## 필드 위의 모든 유닛 제거
func clear_all_units() -> void:
var units: Array[Node] = get_tree().get_nodes_in_group("units")
for unit: Node in units:
if is_instance_valid(unit):
unit.queue_free()
print("GameManager: All units cleared!")
......@@ -2,7 +2,7 @@
[ext_resource type="Script" uid="uid://dd1xgi6hnpuvs" path="res://src/core/rts_camera.gd" id="1_rts_cam"]
[ext_resource type="Script" uid="uid://cq7r51whh6po" path="res://src/core/stage_manager.gd" id="2_stage_mgr"]
[ext_resource type="PackedScene" uid="uid://bslqp550s3j0s" path="res://src/entities/monsters/base_monster.tscn" id="3_base_monster"]
[ext_resource type="PackedScene" uid="uid://bslqp550s3j0s" path="res://src/sandbox/monsters/monster_fit_test.tscn" id="3_base_monster"]
[ext_resource type="Script" uid="uid://dkplkt6q8e3b5" path="res://src/core/unit_controller.gd" id="4_unit_ctrl"]
[ext_resource type="PackedScene" uid="uid://bqga1rvcjyydy" path="res://src/ui/hud.tscn" id="5_hud"]
......
......@@ -26,12 +26,12 @@ var _target_focus: Vector3 = Vector3.ZERO
var _current_zoom: float = 10.0
var _target_zoom: float = 10.0
func _ready() -> void:
# 초기 카메라 위치를 기준으로 포커스 좌표 역계산
# pitch 각도를 60도로 설정 (main.tscn 기본값)
var angle_rad: float = deg_to_rad(60.0)
_current_zoom = global_position.y
# 초기 줌 레벨을 최대 줌 아웃(max_zoom, 가장 축소된 뷰) 상태로 설정
_current_zoom = max_zoom
_target_zoom = _current_zoom
# Z 오프셋 = Y / tan(60) 이므로, 포커스 타겟 Z = 카메라 Z - Z 오프셋
......
......@@ -27,7 +27,7 @@ func _ready() -> void:
else:
push_error("StageManager: Path3D node not found at path: " + str(path_node_path))
# 스폰 타이머 설정
# 몬스터 스폰 타이머 설정
_spawn_timer = Timer.new()
_spawn_timer.wait_time = spawn_interval
_spawn_timer.one_shot = false
......@@ -118,3 +118,18 @@ func _on_active_monster_count_changed(count: int) -> void:
# 다음 스테이지로 상태 전이
GameManager.current_stage += 1
_start_next_stage_cooldown(5.0)
# 몬스터 스폰 일시 정지 여부
var is_spawning_paused: bool = false
## 몬스터 스폰 일시정지 토글
func toggle_spawning_pause() -> bool:
is_spawning_paused = not is_spawning_paused
if _spawn_timer:
_spawn_timer.paused = is_spawning_paused
if _cooldown_timer:
_cooldown_timer.paused = is_spawning_paused
print("StageManager: Spawning Pause Toggled. Paused = ", is_spawning_paused)
return is_spawning_paused
......@@ -56,6 +56,12 @@ func _unhandled_input(event: InputEvent) -> void:
elif mouse_event.button_index == MOUSE_BUTTON_RIGHT and mouse_event.is_pressed():
_perform_action(mouse_event.position)
# 키보드 H 키 입력 감지 (선택 유닛 제자리 홀드 명령)
elif event is InputEventKey:
var key_event: InputEventKey = event as InputEventKey
if key_event.is_pressed() and not key_event.is_echo() and key_event.keycode == KEY_H:
_perform_hold_position()
# 마우스 드래그 중인 상태
elif event is InputEventMouseMotion and _is_dragging:
var motion_event: InputEventMouseMotion = event as InputEventMouseMotion
......@@ -222,3 +228,14 @@ func _get_monster_node(collider: Node) -> Node:
if grandparent and grandparent.is_in_group("monsters"):
return grandparent
return null
## 선택된 모든 유닛 제자리 고정(Hold) 명령 하달
func _perform_hold_position() -> void:
if selected_units.is_empty():
return
for unit: Node in selected_units:
if is_instance_valid(unit) and unit.has_method("hold_position"):
unit.call("hold_position")
print("UnitController: Ordered ", selected_units.size(), " units to hold position.")
......@@ -21,6 +21,9 @@ var _auto_target_cooldown: float = 0.0
@onready var _selection_ring: MeshInstance3D = $SelectionRing
@onready var _visual_mesh: MeshInstance3D = $VisualMesh
# 홀드 상태 변수
var _is_holding: bool = false
# 3D 모델 및 애니메이션 연동 변수 (정적 타이핑 캐싱)
var _model_instance: Node3D = null
var _model_anim_player: AnimationPlayer = null
......@@ -91,6 +94,12 @@ func _physics_process(delta: float) -> void:
if _target_monster and (not is_instance_valid(_target_monster) or _target_monster.get("is_dead")):
_target_monster = null
# 홀드 상태인데 타겟이 공격 사거리를 벗어나면 타겟팅 해제하여 추격 방지
if _is_holding and _target_monster:
var dist: float = global_position.distance_to(_target_monster.global_position)
if dist > unit_data.attack_range:
_target_monster = null
# 2. 적 자동 탐색 (타겟이 없고 수동 강제 이동 방지 쿨다운이 완료된 경우)
if not _target_monster and _auto_target_cooldown <= 0.0:
_find_nearest_target()
......@@ -158,7 +167,8 @@ func _look_at_target(target_pos: Vector3) -> void:
func _find_nearest_target() -> void:
var monsters: Array[Node] = get_tree().get_nodes_in_group("monsters")
var nearest_monster: Node3D = null
var min_distance: float = detection_range
# 홀드 상태 시에는 자신의 무기 사거리 내만 탐색, 일반 상태 시에는 자동 탐색 범위(15m) 적용
var min_distance: float = unit_data.attack_range if _is_holding else detection_range
for monster: Node in monsters:
if is_instance_valid(monster) and monster is Node3D:
......@@ -210,14 +220,23 @@ func _show_attack_visual() -> void:
else:
print("Unit ", name, " strikes ", _target_monster.name)
## 유닛을 제자리에 고정하고 사거리 내의 적만 공격하도록 설정 (Hold)
func hold_position() -> void:
_is_holding = true
_target_monster = null
_nav_agent.target_position = global_position
velocity = Vector3.ZERO
## 외부에서 호출하는 수동 이동 명령
func move_to(target_pos: Vector3) -> void:
_is_holding = false # 수동 조작 시 홀드 해제
_target_monster = null # 이동 명령 시 강제 공격 해제
_auto_target_cooldown = 0.5 # 수동 강제 이동 시 0.5초 동안 자동 공격 타겟 지정을 비활성화하여 피신을 허용
_nav_agent.target_position = target_pos
## 외부에서 호출하는 특정 몬스터 강제 공격 지정
func attack_target(monster: Node3D) -> void:
_is_holding = false # 강제 공격 시 홀드 해제
if is_instance_valid(monster) and not monster.get("is_dead"):
_target_monster = monster
_nav_agent.target_position = monster.global_position
......@@ -317,13 +336,12 @@ func _attach_weapon() -> void:
# 검의 로컬 스케일, 회전 및 오프셋 보정 (손안에 쏙 들어가도록)
# FBX 원본 메쉬 크기(AABB 약 0.008)가 매우 작으므로 150배 확대하여 캐릭터 스펙에 맞춤
weapon_inst.scale = Vector3(1.0, 1.0, 1.0) # 1배 확대 보정
weapon_inst.rotation_degrees = Vector3(90.0, 0.0, 90.0) # 검날 방향 보정
weapon_inst.position = Vector3(0.0, 0.0, 0.0)
weapon_inst.scale = Vector3(0.8, 0.7, 0.7) # 1배 확대 보정
weapon_inst.rotation_degrees = Vector3(30.0, 95.0, 165.0) # 검날 방향 보정
weapon_inst.position = Vector3(0.0000, 0.2000, -0.6000) # 손잡이 쥠 정밀 보정
# 무기 텍스처(azure_blade_0.png) 매핑
_apply_weapon_skin(weapon_inst)
bone_attachment.add_child(weapon_inst)
print("BaseUnit: Attached azure_blade to RightHand bone successfully.")
......
# 🛠️ 비주얼 피팅 샌드박스 (Fit Sandbox) 사용 가이드
이 공간은 유닛, 몬스터, 혹은 쥠 무기(Weapon)의 **Position(위치), Rotation(회전), Scale(크기)** 오프셋 수치를 Godot 에디터 3D 뷰포트에서 실시간으로 확인하고 조율하여, 그 값을 손쉽게 GDScript로 옮겨 담기 위해 마련된 테스트베드입니다.
---
## 📂 디렉토리 구조
```
res://src/sandbox/
├── fit_sandbox.tscn # 튜닝 전용 메인 3D 씬 (그리드 바닥, 조명, 카메라 포함)
├── fit_sandbox.gd # Transform 코드를 에디터에서 자동 추출해주는 헬퍼 스크립트
├── SANDBOX_GUIDE.md # 본 사용 설명서
├── units/
│ ├── striker_fit_test.tscn # Striker 유닛 조율용 복사본 씬
│ └── guardian_fit_test.tscn # Guardian 유닛 조율용 복사본 씬
└── monsters/
└── monster_fit_test.tscn # Monster 조율용 복사본 씬
```
---
## 🚀 빠른 조율 가이드 (예: Striker의 무기 쥠 오프셋 보정)
### Step 1. 샌드박스 씬 열기
1. Godot 에디터에서 `res://src/sandbox/fit_sandbox.tscn` 씬을 더블 클릭하여 엽니다.
### Step 2. 조율할 유닛 및 무기 배치
1. `res://src/sandbox/units/striker_fit_test.tscn` 인스턴스를 `TestingArea` 노드의 자식으로 드래그 앤 드롭하여 배치합니다.
2. 유닛의 본(Bone) 구조 내에서 무기가 장착될 손 노드를 찾습니다.
- 예: `Striker_FitTest` -> `striker_model` -> `Root` -> `Skeleton3D` -> `RightHand`
3. 쥠 무기 메쉬/씬(예: `res://assets/models/items/weapons/azure_blade.fbx`)을 해당 `RightHand` 노드의 자식으로 배치합니다.
### Step 3. 3D 뷰포트에서 피팅 튜닝
1. 배치한 무기 노드를 선택하고, 3D 뷰포트 조작 기즈모를 사용해 손잡이를 쥐는 알맞은 **Position, Rotation, Scale**로 눈으로 보며 정교하게 조율합니다.
### Step 4. GDScript 코드 즉시 추출
1. 씬 트리 루트 노드인 `FitSandbox`를 선택합니다.
2. 인스펙터 창에서 다음 속성을 설정합니다:
- **Target Node**: 방금 조율 완료한 무기 노드(예: `azure_blade`)를 드래그하여 지정합니다.
- **Export Transform Code**: 이 체크박스(`export_transform_code`)를 **True**로 체크합니다.
3. 에디터 하단의 **Output (출력)** 패널을 확인하면 다음과 같이 복사해서 바로 쓸 수 있는 GDScript 코드가 출력됩니다:
```gdscript
==================================================
FIT SANDBOX OFFSET EXPORT (Target: azure_blade)
==================================================
# GDScript 붙여넣기용 코드:
position = Vector3(0.0000, 0.0000, -0.6000)
rotation = Vector3(0.0000, 3.1416, 0.0000) # Degrees: (0.0, 180.0, 0.0)
scale = Vector3(150.0000, 150.0000, 150.0000)
==================================================
```
### Step 5. 소스 코드에 붙여넣기
1. 추출된 Vector3 값을 복사하여 장착 관련 로직(예: `base_unit.gd` 내 `_attach_weapon()` 메서드 등)에 대입합니다.
---
## 💡 유용한 팁
- **@tool 스크립트 기반**: 이 기능은 게임을 직접 **실행(Run)하지 않고도** Godot 3D 에디터 상에서 실시간으로 추출할 수 있습니다.
- **오프셋 보정의 분리**: 피팅 작업이 완료된 후, 실제 인게임 소스 코드에 값을 입력하고 나면 샌드박스 씬은 자유롭게 비우거나 새로 복사해서 마음껏 테스트용으로 덮어쓸 수 있습니다.
@tool
extends Node3D
## 비주얼 피팅 샌드박스 헬퍼 스크립트 (FitSandbox)
## 에디터 상에서 3D 노드의 오프셋(위치/회전/크기)을 맞춘 후, GDScript용 Vector3 코드를 즉시 추출해 줍니다.
@export_group("Transform Exporter")
## 이 값을 True로 체크하면 지정된 target_node의 Transform 정보를 출력합니다. (체크 후 자동 리셋됨)
@export var export_transform_code: bool = false:
set(val):
if val:
_export_selected_transform()
## 오프셋을 추출할 대상 노드 (예: 손 하위에 배치된 무기 MeshInstance3D 등)
@export var target_node: Node3D
func _export_selected_transform() -> void:
if not target_node:
push_warning("FitSandbox: Target Node가 지정되지 않았습니다. 인스펙터에서 target_node를 할당해 주세요.")
return
var pos: Vector3 = target_node.position
var rot_deg: Vector3 = target_node.rotation_degrees
var rot_rad: Vector3 = target_node.rotation
var scl: Vector3 = target_node.scale
print("\n==================================================")
print(" FIT SANDBOX OFFSET EXPORT (Target: %s)" % target_node.name)
print("==================================================")
print("# GDScript 붙여넣기용 코드:")
print("position = Vector3(%.4f, %.4f, %.4f)" % [pos.x, pos.y, pos.z])
print("rotation = Vector3(%.4f, %.4f, %.4f) # Degrees: (%.1f, %.1f, %.1f)" % [rot_rad.x, rot_rad.y, rot_rad.z, rot_deg.x, rot_deg.y, rot_deg.z])
print("scale = Vector3(%.4f, %.4f, %.4f)" % [scl.x, scl.y, scl.z])
print("==================================================\n")
[gd_scene format=3 uid="uid://dcfgmet1sevvb"]
[ext_resource type="Script" uid="uid://dxfqdaty7x0k1" path="res://src/sandbox/fit_sandbox.gd" id="1_fit_sandbox"]
[ext_resource type="PackedScene" uid="uid://dwvbc5m3myqbk" path="res://src/sandbox/units/striker_fit_test.tscn" id="2_bjfqw"]
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_sky"]
[sub_resource type="Sky" id="Sky_sky"]
sky_material = SubResource("ProceduralSkyMaterial_sky")
[sub_resource type="Environment" id="Environment_env"]
background_mode = 2
sky = SubResource("Sky_sky")
ambient_light_source = 3
[sub_resource type="PlaneMesh" id="PlaneMesh_floor"]
size = Vector2(20, 20)
[node name="FitSandbox" type="Node3D" unique_id=686593296 node_paths=PackedStringArray("target_node")]
script = ExtResource("1_fit_sandbox")
target_node = NodePath("")
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=486496694]
environment = SubResource("Environment_env")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1763577303]
transform = Transform3D(0.866025, -0.353553, 0.353553, 0, 0.707107, 0.707107, -0.5, -0.612372, 0.612372, 0, 5, 0)
shadow_enabled = true
[node name="VisualFloor" type="MeshInstance3D" parent="." unique_id=1624326344]
mesh = SubResource("PlaneMesh_floor")
[node name="SandboxCamera" type="Camera3D" parent="." unique_id=460715563]
transform = Transform3D(1, 0, 0, 0, 0.866025, 0.5, 0, -0.5, 0.866025, 0, 3, 5)
[node name="TestingArea" type="Node3D" parent="." unique_id=901272834]
[node name="StrikerUnit" parent="TestingArea" unique_id=1844747242 instance=ExtResource("2_bjfqw")]
[gd_scene format=3 uid="uid://bslqp550s3j0s"]
[ext_resource type="Script" uid="uid://bj0j32wc3fvkg" path="res://src/entities/monsters/base_monster.gd" id="1_base_monster"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_red"]
albedo_color = Color(0.901961, 0.098039, 0.098039, 1)
roughness = 0.5
[sub_resource type="SphereMesh" id="SphereMesh_monster"]
material = SubResource("StandardMaterial3D_red")
[sub_resource type="SphereShape3D" id="SphereShape3D_collision"]
radius = 0.6
[node name="BaseMonster" type="PathFollow3D" unique_id=979648792]
script = ExtResource("1_base_monster")
[node name="VisualMesh" type="MeshInstance3D" parent="." unique_id=1944106965]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
mesh = SubResource("SphereMesh_monster")
[node name="MonsterArea" type="Area3D" parent="." unique_id=1629900660]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
[node name="CollisionShape3D" type="CollisionShape3D" parent="MonsterArea" unique_id=1948998971]
shape = SubResource("SphereShape3D_collision")
[gd_scene format=3 uid="uid://cx4vbc5m3myqk"]
[ext_resource type="Script" uid="uid://daq4ko8ryry3w" path="res://src/entities/units/base_unit.gd" id="1_base_unit"]
[ext_resource type="PackedScene" uid="uid://dh00osxx32ll" path="res://src/entities/units/guardian/guardian.tscn" id="2_y84tp"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"]
radius = 0.4
height = 1.2
[sub_resource type="CapsuleMesh" id="CapsuleMesh_unit"]
radius = 0.4
height = 1.2
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ring"]
albedo_color = Color(0.098039, 0.901961, 0.098039, 1)
emission_enabled = true
emission = Color(0.098039, 0.901961, 0.098039, 1)
emission_energy_multiplier = 2.0
[sub_resource type="TorusMesh" id="TorusMesh_selection"]
material = SubResource("StandardMaterial3D_ring")
inner_radius = 0.65
outer_radius = 0.7
[node name="GuardianUnit" type="CharacterBody3D" unique_id=2140150158]
script = ExtResource("1_base_unit")
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1623768716]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0)
shape = SubResource("CapsuleShape3D_body")
[node name="VisualMesh" type="MeshInstance3D" parent="." unique_id=506359796]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0)
visible = false
mesh = SubResource("CapsuleMesh_unit")
[node name="SelectionRing" type="MeshInstance3D" parent="." unique_id=1020833698]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.02, 0)
mesh = SubResource("TorusMesh_selection")
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="." unique_id=1893995123]
[node name="guardian" parent="." unique_id=1303946976 instance=ExtResource("2_y84tp")]
[gd_scene format=3 uid="uid://dwvbc5m3myqbk"]
[ext_resource type="Script" uid="uid://daq4ko8ryry3w" path="res://src/entities/units/base_unit.gd" id="1_base_unit"]
[ext_resource type="PackedScene" uid="uid://c2aiejcidlihv" path="res://src/entities/units/striker/striker.tscn" id="2_iytoq"]
[ext_resource type="PackedScene" uid="uid://7aw71ibtccuf" path="res://assets/models/items/weapons/azure_blade.fbx" id="3_weapon"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"]
radius = 0.4
height = 1.2
[sub_resource type="CapsuleMesh" id="CapsuleMesh_unit"]
radius = 0.4
height = 1.2
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ring"]
albedo_color = Color(0.098039, 0.901961, 0.098039, 1)
emission_enabled = true
emission = Color(0.098039, 0.901961, 0.098039, 1)
emission_energy_multiplier = 2.0
[sub_resource type="TorusMesh" id="TorusMesh_selection"]
material = SubResource("StandardMaterial3D_ring")
inner_radius = 0.65
outer_radius = 0.7
[node name="StrikerUnit" type="CharacterBody3D" unique_id=1844747242]
script = ExtResource("1_base_unit")
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=332342782]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0)
shape = SubResource("CapsuleShape3D_body")
[node name="VisualMesh" type="MeshInstance3D" parent="." unique_id=758090532]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0)
visible = false
mesh = SubResource("CapsuleMesh_unit")
[node name="SelectionRing" type="MeshInstance3D" parent="." unique_id=1378906746]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.02, 0)
mesh = SubResource("TorusMesh_selection")
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="." unique_id=462079662]
[node name="striker" parent="." unique_id=1533136548 instance=ExtResource("2_iytoq")]
[node name="Skeleton3D" parent="striker/Armature" parent_id_path=PackedInt32Array(1533136548, 1984558724) index="0" unique_id=550080659]
bones/0/rotation = Quaternion(0.053775016, -0.07558027, 0.95148146, -0.2933922)
bones/1/rotation = Quaternion(-0.11642271, -0.9713257, 0.18020546, 0.102460474)
bones/2/rotation = Quaternion(0.55354464, 0.045569547, -0.0009506773, 0.83157134)
bones/3/rotation = Quaternion(-0.49771708, 0.11763392, 0.21810035, 0.8311873)
bones/4/rotation = Quaternion(-0.37741533, 0.09441602, 0.034894247, 0.9205573)
bones/5/rotation = Quaternion(-0.73469806, -0.4706895, 0.35849488, 0.33189112)
bones/6/rotation = Quaternion(0.54929906, -0.055585526, -0.007583472, 0.8337405)
bones/7/rotation = Quaternion(-0.6469232, -0.114821404, -0.31046525, 0.6869628)
bones/8/rotation = Quaternion(-0.3751127, -0.089608364, -0.033254314, 0.92203856)
bones/9/rotation = Quaternion(0.610278, 0.6213914, 0.3507232, 0.3441319)
bones/10/rotation = Quaternion(0.066713676, -0.0066856947, -0.021492973, 0.99751824)
bones/11/rotation = Quaternion(-0.021008668, 0.007633576, -0.001313759, 0.9997493)
bones/12/rotation = Quaternion(0.57989126, 0.49733877, -0.42518228, 0.48538694)
bones/13/rotation = Quaternion(0.2573209, 0.1580452, -0.12015072, 0.9457122)
bones/14/rotation = Quaternion(-0.5427628, -0.19160645, 0.28051028, 0.7681209)
bones/15/rotation = Quaternion(-0.019803066, -0.26306573, 0.22688207, 0.93751204)
bones/16/rotation = Quaternion(-0.6120462, 0.4637878, -0.43958727, -0.46590066)
bones/17/rotation = Quaternion(0.3186677, -0.28127205, -0.13971356, 0.89432496)
bones/18/rotation = Quaternion(-0.428532, 0.14101788, -0.15769473, 0.87841153)
bones/19/rotation = Quaternion(0.11561307, -0.49338162, -0.2142872, 0.8350384)
bones/20/rotation = Quaternion(-0.12836885, 0.17267549, -0.025799042, 0.9762372)
bones/21/rotation = Quaternion(0.23437437, 0.25792313, -0.13371938, 0.9277196)
bones/22/rotation = Quaternion(-0.27558565, -2.7975927e-08, 0.0022902817, 0.9612738)
bones/23/rotation = Quaternion(0.5067052, 1.4996291e-09, -0.0042108223, 0.8621092)
[node name="RightHandAttachment" type="BoneAttachment3D" parent="striker/Armature/Skeleton3D" index="1" unique_id=133354659]
transform = Transform3D(0.7814436, -0.5886768, 0.20690073, -0.07732609, 0.23766617, 0.96826494, -0.61916757, -0.77264214, 0.14020297, -0.51514053, 0.19189739, 1.0136757)
bone_name = "RightHand"
bone_idx = 19
[node name="azure_blade" parent="striker/Armature/Skeleton3D/RightHandAttachment" unique_id=1183204526 instance=ExtResource("3_weapon")]
transform = Transform3D(0.1704825, -0.32099724, 0.6039109, 0.17931515, -0.5855614, -0.35, 0.7607771, 0.20994908, -0.05283537, 0, 0.2, -0.6)
[editable path="striker"]
......@@ -15,10 +15,12 @@ extends Control
@onready var result_title: Label = $ResultPopup/ResultTitle
@onready var restart_button: Button = $ResultPopup/RestartButton
# 몬스터 일시정지 버튼 런타임 저장 변수
var _pause_button: Button = null
func _ready() -> void:
func _ready() -> void:
# GameManager 시그널 핸들러 등록
GameManager.gold_changed.connect(_on_gold_changed)
GameManager.stage_changed.connect(_on_stage_changed)
......@@ -36,6 +38,27 @@ func _ready() -> void:
_on_stage_changed(GameManager.current_stage)
_on_monster_changed(GameManager.active_monster_count)
# 몬스터 생성 일시 정지, 게임 재시작 및 모든 유닛 삭제 버튼 런타임 생성
var bottom_bar: Control = get_node_or_null("BottomBar") as Control
if bottom_bar:
_pause_button = Button.new()
_pause_button.name = "PauseSpawningButton"
_pause_button.text = "Pause Spawning"
bottom_bar.add_child(_pause_button)
_pause_button.pressed.connect(_on_pause_spawning_pressed)
var restart_game_button: Button = Button.new()
restart_game_button.name = "RestartGameButton"
restart_game_button.text = "Restart Game"
bottom_bar.add_child(restart_game_button)
restart_game_button.pressed.connect(_on_restart_game_pressed)
var clear_units_button: Button = Button.new()
clear_units_button.name = "ClearUnitsButton"
clear_units_button.text = "Clear All Units"
bottom_bar.add_child(clear_units_button)
clear_units_button.pressed.connect(_on_clear_units_pressed)
# 골드 UI 갱신 및 소환 한도 검증
func _on_gold_changed(gold: int) -> void:
gold_label.text = "GOLD: " + str(gold) + " G"
......@@ -101,26 +124,26 @@ func _on_spawn_pressed() -> void:
print("Spawned random unit: ", chosen_template.unit_name, " at position ", spawn_offset)
# 게임 재시작 기능
# 상시 노출되는 게임 재시작 버튼 클릭 처리
func _on_restart_game_pressed() -> void:
GameManager.restart_game()
# 모든 유닛 삭제 버튼 클릭 처리
func _on_clear_units_pressed() -> void:
GameManager.clear_all_units()
# 게임 결과 팝업 내 재시작 버튼 클릭 처리
func _on_restart_pressed() -> void:
# 필드 위 모든 유닛 제거
var units: Array[Node] = get_tree().get_nodes_in_group("units")
for unit: Node in units:
if is_instance_valid(unit):
unit.queue_free()
# 필드 위 모든 몬스터 제거
var monsters: Array[Node] = get_tree().get_nodes_in_group("monsters")
for monster: Node in monsters:
if is_instance_valid(monster):
monster.queue_free()
# GameManager 상태 초기화
GameManager.reset_game()
result_popup.visible = false
GameManager.restart_game()
# 스테이지 재베이킹 및 스폰 리셋을 위해 씬 리로드 우회 처리 가능하나
# 여기서는 간단히 GameManager reset 이후 StageManager가 몬스터를 처음부터 다시 배치하도록 대응
# 몬스터 스폰 일시정지 버튼 클릭 처리
func _on_pause_spawning_pressed() -> void:
if not _pause_button:
return
var stage_mgr: Node = get_node_or_null("../StageManager")
if stage_mgr:
stage_mgr.call("_ready")
if stage_mgr and stage_mgr.has_method("toggle_spawning_pause"):
var is_paused: bool = stage_mgr.call("toggle_spawning_pause")
if is_paused:
_pause_button.text = "Resume Spawning"
else:
_pause_button.text = "Pause Spawning"
......@@ -20,6 +20,8 @@
- [x] 스테이지 제어 로직 구현 (`src/core/stage_manager.gd`)
- 1초 간격 몬스터 스폰 타이머 설정
- 스테이지별 몬스터 제원 (HP, 골드 보상 등) 스케일링 공식 적용
- 스폰 타이머 및 준비 쿨다운 타이머의 시간을 일시정지(`Timer.paused`)시키는 로직 탑재
- [x] HUD UI 스크립트(`hud.gd`) 개선을 통해 런타임 몬스터 생성 일시정지 버튼(`PauseSpawningButton`) 추가 및 토글 연동 완료
## [x] 3단계: 몬스터 시스템 구현
- [x] 기본 몬스터 씬 (`src/entities/monsters/base_monster.tscn`) 및 스크립트 작성
......@@ -30,6 +32,7 @@
- [x] 네비게이션 메시 (`NavigationRegion3D`) 맵에 추가 및 구워내기 (Bake)
- [x] 마우스 클릭 및 드래그 박스(`Area3D` 또는 스크린 투 월드 투영 기법)를 활용한 유닛 멀티 선택 기능 구현
- [x] 선택된 유닛이 맵 바닥 우클릭 시 네비게이션 에이전트(`NavigationAgent3D`)를 활용하여 목적지로 이동하도록 구현
- [x] 선택된 유닛이 키보드 H 키 클릭 시 제자리에 고정 대기(Hold Position)하며, 이동을 멈추고 자신의 공격 사거리 내에 들어온 몬스터만 수비 타겟팅하도록 전투 AI와 연계하여 홀드 기능 완료
## [x] 5단계: 유닛 클래스 정의 및 전투 구현
- [x] 유닛 기본 스키마 (`src/entities/units/base_unit.gd`) 작성 (정적 타이핑 적용)
......@@ -53,6 +56,6 @@
- [x] `guardian_unit.tscn` 씬을 설계 및 신설하여 가디언 모델과 스킨 텍스처를 씬에 연계 완료
- [x] `hud.gd`에 중복되어 존재하던 유닛 ID 버그를 일괄 픽스하고, 가디언 유닛 소환 시 `guardian_unit.tscn`이 씬 분기로 올바르게 로드되도록 연동 완료
- [x] 사용자가 직접 `striker_unit.tscn`에서 모델 회전 보정을 적용한 상태를 인지하고, 스크립트(`base_unit.gd`)의 수동 Y축 회전 코드를 완벽히 제거
- [x] Striker 유닛 오른손 본(`RightHand`)에 `BoneAttachment3D` 노드를 런타임 동적 배치하고 `azure_blade.fbx` 검 무기를 장착 및 텍스처를 씌우는 기능 구현 완료
- [x] Striker 유닛 오른손 본(`RightHand`)에 `BoneAttachment3D` 노드를 런타임 동적 배치하고 `azure_blade.fbx` 검 무기를 장착 및 텍스처를 씌우는 기능 구현 완료 (스케일 150배 확대 및 Z축 -0.6m 손잡이 쥠 오프셋 적용)
- [x] `DataManager` 싱글톤 신설 및 오토로드 등록을 완료하여 유닛 데이터(5종 스펙)와 몬스터 데이터(HP 스케일링, 보상 테이블)를 통합 분리
- [x] `hud.gd``stage_manager.gd`에서 하드코딩되었던 데이터 조작 로직들을 걷어내고 `DataManager`를 참조하도록 리팩토링 완수
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment