Commit 4b394e75 authored by Gavin An's avatar Gavin An

작업중

parent 9d51f839
...@@ -4,9 +4,9 @@ extends Camera3D ...@@ -4,9 +4,9 @@ extends Camera3D
## 하늘에서 전장을 살짝 비스듬히 내려다보는 고정 카메라입니다. ## 하늘에서 전장을 살짝 비스듬히 내려다보는 고정 카메라입니다.
## 이동, 줌인, 줌아웃 입력 없이 항상 맵 중앙을 바라봅니다. ## 이동, 줌인, 줌아웃 입력 없이 항상 맵 중앙을 바라봅니다.
@export var fixed_position: Vector3 = Vector3(0.0, 22.0, 11.0) @export var fixed_position: Vector3 = Vector3(0.0, 24.0, 12.0)
@export var fixed_target: Vector3 = Vector3.ZERO @export var fixed_target: Vector3 = Vector3(0.0, 0.0, 1.0)
@export var fixed_fov: float = 50.0 @export var fixed_fov: float = 44.0
func _ready() -> void: func _ready() -> void:
......
...@@ -136,7 +136,7 @@ func _spawn_projectile(fire_position: Vector3) -> void: ...@@ -136,7 +136,7 @@ func _spawn_projectile(fire_position: Vector3) -> void:
proj.set("damage", unit_data.damage) proj.set("damage", unit_data.damage)
proj.set("target", _target_monster) proj.set("target", _target_monster)
proj.set("speed", 18.0) # 불덩이는 크고 묵직하므로 조금 느린 속도(18.0m/s)로 날아가게 설정 proj.set("speed", 18.0) # 불덩이는 크고 묵직하므로 조금 느린 속도(18.0m/s)로 날아가게 설정
#proj.set("color", Color(1.0, 0.35, 0.1, 1.0)) # 붉은 주황색 계열의 색상 지정 proj.set("color", Color(1.0, 0.35, 0.1, 1.0)) # 붉은 주황색 계열의 색상 지정
# 셰이더 및 GPUParticles3D가 사전 구성된 불덩이 비주얼 효과 씬(.tscn)을 전달합니다. # 셰이더 및 GPUParticles3D가 사전 구성된 불덩이 비주얼 효과 씬(.tscn)을 전달합니다.
var fb_effect: = preload("res://src/entities/projectiles/fireball/fireball_effect.tscn") var fb_effect: = preload("res://src/entities/projectiles/fireball/fireball_effect.tscn")
......
...@@ -10,6 +10,8 @@ extends Control ...@@ -10,6 +10,8 @@ extends Control
const HudUnitPlacementScript: GDScript = preload("res://src/ui/unit_placement/hud_unit_placement.gd") const HudUnitPlacementScript: GDScript = preload("res://src/ui/unit_placement/hud_unit_placement.gd")
const HudSummonerPanelScript: GDScript = preload("res://src/ui/summoner_panel/hud_summoner_panel.gd") const HudSummonerPanelScript: GDScript = preload("res://src/ui/summoner_panel/hud_summoner_panel.gd")
const HudHeroPanelScript: GDScript = preload("res://src/ui/hero_panel/hud_hero_panel.gd") const HudHeroPanelScript: GDScript = preload("res://src/ui/hero_panel/hud_hero_panel.gd")
const STATUS_CARD_MIN_SIZE: Vector2 = Vector2(118.0, 34.0)
const STATUS_CARD_FONT_SIZE: int = 14
## 기본 유닛 fallback 씬입니다. ## 기본 유닛 fallback 씬입니다.
## 유닛 ID에 맞는 전용 씬을 찾지 못하면 HudUnitPlacement가 이 씬을 사용합니다. ## 유닛 ID에 맞는 전용 씬을 찾지 못하면 HudUnitPlacement가 이 씬을 사용합니다.
...@@ -17,6 +19,7 @@ const HudHeroPanelScript: GDScript = preload("res://src/ui/hero_panel/hud_hero_p ...@@ -17,6 +19,7 @@ const HudHeroPanelScript: GDScript = preload("res://src/ui/hero_panel/hud_hero_p
## 상단 상태 표시 UI 노드 캐시입니다. ## 상단 상태 표시 UI 노드 캐시입니다.
## hud.tscn에 이미 배치된 노드들이므로 @onready로 안전하게 참조합니다. ## hud.tscn에 이미 배치된 노드들이므로 @onready로 안전하게 참조합니다.
@onready var top_bar: HBoxContainer = $TopBar
@onready var gold_label: Label = $TopBar/GoldLabel @onready var gold_label: Label = $TopBar/GoldLabel
@onready var stage_label: Label = $TopBar/StageLabel @onready var stage_label: Label = $TopBar/StageLabel
@onready var monster_label: Label = $TopBar/MonsterLabel @onready var monster_label: Label = $TopBar/MonsterLabel
...@@ -40,6 +43,7 @@ func _ready() -> void: ...@@ -40,6 +43,7 @@ func _ready() -> void:
_connect_game_manager_signals() _connect_game_manager_signals()
_setup_unit_placement() _setup_unit_placement()
_bind_static_buttons() _bind_static_buttons()
_setup_status_cards()
_initialize_status_labels() _initialize_status_labels()
_setup_summoner_flow() _setup_summoner_flow()
_create_runtime_bottom_buttons() _create_runtime_bottom_buttons()
...@@ -72,9 +76,79 @@ func _setup_unit_placement() -> void: ...@@ -72,9 +76,79 @@ func _setup_unit_placement() -> void:
## hud.tscn에 이미 존재하는 정적 버튼들의 클릭 신호를 연결합니다. ## hud.tscn에 이미 존재하는 정적 버튼들의 클릭 신호를 연결합니다.
func _bind_static_buttons() -> void: func _bind_static_buttons() -> void:
unit_spawn_button.pressed.connect(_on_spawn_pressed) unit_spawn_button.pressed.connect(_on_unit_spawn_pressed)
restart_button.pressed.connect(_on_restart_pressed) restart_button.pressed.connect(_on_restart_pressed)
## 상단 상태 라벨들을 작은 카드 형태로 재구성합니다.
## hud.tscn에는 라벨만 배치되어 있으므로, 런타임에 PanelContainer를 만들고 기존 라벨을 그 안으로 옮깁니다.
## 이렇게 하면 씬 원본을 직접 수정하지 않으면서도 스테이지/골드/몬스터 정보가 한 덩어리 카드처럼 보입니다.
func _setup_status_cards() -> void:
if not top_bar:
return
top_bar.add_theme_constant_override("separation", 8)
top_bar.alignment = BoxContainer.ALIGNMENT_CENTER
top_bar.offset_top = 12.0
top_bar.offset_bottom = 50.0
_wrap_status_label_in_card(stage_label, "StageStatusCard", Color(0.12, 0.16, 0.22, 0.82), Color(0.42, 0.62, 1.0, 0.8))
_wrap_status_label_in_card(gold_label, "GoldStatusCard", Color(0.22, 0.17, 0.08, 0.84), Color(1.0, 0.77, 0.18, 0.86))
_wrap_status_label_in_card(monster_label, "MonsterStatusCard", Color(0.22, 0.09, 0.10, 0.84), Color(1.0, 0.30, 0.26, 0.86))
## 기존 Label 하나를 작은 카드 안으로 옮깁니다.
## 이미 카드로 감싸진 상태에서 다시 호출될 수도 있으므로, 부모가 PanelContainer이면 중복 생성하지 않습니다.
func _wrap_status_label_in_card(label: Label, card_name: String, bg_color: Color, border_color: Color) -> void:
if not label:
return
if label.get_parent() is PanelContainer:
_style_status_label(label)
return
var label_index: int = label.get_index()
var card: PanelContainer = PanelContainer.new()
card.name = card_name
card.custom_minimum_size = STATUS_CARD_MIN_SIZE
card.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
card.size_flags_vertical = Control.SIZE_SHRINK_CENTER
card.mouse_filter = Control.MOUSE_FILTER_IGNORE
card.add_theme_stylebox_override("panel", _create_status_card_style(bg_color, border_color))
top_bar.add_child(card)
top_bar.move_child(card, label_index)
label.reparent(card)
_style_status_label(label)
## 상태 카드 라벨의 공통 타이포그래피를 맞춥니다.
## 기존 hud.tscn의 큰 22px 라벨 설정을 덮어써서 상단 카드가 작고 단정하게 보이도록 합니다.
func _style_status_label(label: Label) -> void:
label.custom_minimum_size = STATUS_CARD_MIN_SIZE
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size", STATUS_CARD_FONT_SIZE)
label.add_theme_color_override("font_color", Color(0.96, 0.98, 1.0, 0.96))
label.add_theme_constant_override("outline_size", 1)
label.add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 0.35))
## 상태 카드 배경 StyleBox를 생성합니다.
## 색만 다르게 주고 모서리/테두리/패딩은 통일해 세 카드가 같은 UI 패밀리처럼 읽히게 합니다.
func _create_status_card_style(bg_color: Color, border_color: Color) -> StyleBoxFlat:
var style: StyleBoxFlat = StyleBoxFlat.new()
style.bg_color = bg_color
style.border_color = border_color
style.border_width_left = 1
style.border_width_top = 1
style.border_width_right = 1
style.border_width_bottom = 1
style.corner_radius_top_left = 6
style.corner_radius_top_right = 6
style.corner_radius_bottom_left = 6
style.corner_radius_bottom_right = 6
style.content_margin_left = 8.0
style.content_margin_right = 8.0
style.content_margin_top = 4.0
style.content_margin_bottom = 4.0
return style
## 씬 진입 직후 현재 GameManager 상태를 상단 HUD와 결과 팝업에 반영합니다. ## 씬 진입 직후 현재 GameManager 상태를 상단 HUD와 결과 팝업에 반영합니다.
func _initialize_status_labels() -> void: func _initialize_status_labels() -> void:
result_popup.visible = false result_popup.visible = false
...@@ -134,18 +208,18 @@ func _create_runtime_bottom_buttons() -> void: ...@@ -134,18 +208,18 @@ func _create_runtime_bottom_buttons() -> void:
## 골드 UI 갱신 및 골드 기반 버튼 활성 상태를 반영합니다. ## 골드 UI 갱신 및 골드 기반 버튼 활성 상태를 반영합니다.
func _on_gold_changed(gold: int) -> void: func _on_gold_changed(gold: int) -> void:
gold_label.text = "GOLD: " + str(gold) + " G" gold_label.text = "Gold " + str(gold) + "G"
unit_spawn_button.disabled = gold < GameManager.unit_spawn_cost unit_spawn_button.disabled = gold < GameManager.unit_spawn_cost
if _hero_panel and _hero_panel.has_method("update_gold"): if _hero_panel and _hero_panel.has_method("update_gold"):
_hero_panel.call("update_gold", gold) _hero_panel.call("update_gold", gold)
## 현재 스테이지 텍스트를 갱신합니다. ## 현재 스테이지 텍스트를 갱신합니다.
func _on_stage_changed(stage: int) -> void: func _on_stage_changed(stage: int) -> void:
stage_label.text = "STAGE: " + str(stage) + " / " + str(GameManager.MAX_STAGE) stage_label.text = "Stage " + str(stage) + "/" + str(GameManager.MAX_STAGE)
## 필드에 남아 있는 몬스터 수를 갱신합니다. ## 필드에 남아 있는 몬스터 수를 갱신합니다.
func _on_monster_changed(count: int) -> void: func _on_monster_changed(count: int) -> void:
monster_label.text = "MONSTERS: " + str(count) + " / " + str(GameManager.DEFEAT_MONSTER_LIMIT) monster_label.text = "Monsters " + str(count) + "/" + str(GameManager.DEFEAT_MONSTER_LIMIT)
## 게임 패배 신호를 받으면 결과 팝업을 표시합니다. ## 게임 패배 신호를 받으면 결과 팝업을 표시합니다.
func _on_game_lost() -> void: func _on_game_lost() -> void:
...@@ -159,7 +233,7 @@ func _on_game_victory() -> void: ...@@ -159,7 +233,7 @@ func _on_game_victory() -> void:
## 일반 유닛 소환 버튼 클릭 처리입니다. ## 일반 유닛 소환 버튼 클릭 처리입니다.
## 실제 유닛 선정과 타일 배치 대기 전환은 HudUnitPlacement가 담당합니다. ## 실제 유닛 선정과 타일 배치 대기 전환은 HudUnitPlacement가 담당합니다.
func _on_spawn_pressed() -> void: func _on_unit_spawn_pressed() -> void:
if _unit_placement and _unit_placement.has_method("begin_random_unit_placement"): if _unit_placement and _unit_placement.has_method("begin_random_unit_placement"):
_unit_placement.call("begin_random_unit_placement") _unit_placement.call("begin_random_unit_placement")
......
...@@ -13,6 +13,7 @@ const RANGER_SCENE: PackedScene = preload("res://src/entities/units/ranger/range ...@@ -13,6 +13,7 @@ const RANGER_SCENE: PackedScene = preload("res://src/entities/units/ranger/range
const MAGE_SCENE: PackedScene = preload("res://src/entities/units/mage/mage_unit.tscn") const MAGE_SCENE: PackedScene = preload("res://src/entities/units/mage/mage_unit.tscn")
const PLACEMENT_TILE_OVERLAY_SCENE: PackedScene = preload("res://src/ui/unit_placement/placement_tile_overlay.tscn") const PLACEMENT_TILE_OVERLAY_SCENE: PackedScene = preload("res://src/ui/unit_placement/placement_tile_overlay.tscn")
const OVERLAY_POOL_WARMUP_BATCH_SIZE: int = 16 const OVERLAY_POOL_WARMUP_BATCH_SIZE: int = 16
const MONSTER_PATH_TILE_EXTRA_MARGIN_RATIO: float = 0.15
var _hud: Control = null var _hud: Control = null
var _unit_spawn_button: Button = null var _unit_spawn_button: Button = null
...@@ -30,6 +31,8 @@ var _overlay_pool: Array[Node] = [] ...@@ -30,6 +31,8 @@ var _overlay_pool: Array[Node] = []
var _active_overlays: Array[Node] = [] var _active_overlays: Array[Node] = []
var _overlay_pool_parent: Node = null var _overlay_pool_parent: Node = null
var _is_overlay_pool_warming_up: bool = false var _is_overlay_pool_warming_up: bool = false
var _monster_path_tile_cache_grid_map: GridMap = null
var _monster_path_tile_cache: Dictionary = {}
var _pending_preview_panel: Panel = null var _pending_preview_panel: Panel = null
var _pending_preview_name_label: Label = null var _pending_preview_name_label: Label = null
...@@ -322,8 +325,13 @@ func _find_grid_map(node: Node) -> GridMap: ...@@ -322,8 +325,13 @@ func _find_grid_map(node: Node) -> GridMap:
return null return null
## 이미 타일에 고정 배치된 유닛이 해당 셀을 점유 중인지 확인합니다. ## 이미 타일에 고정 배치된 유닛이 해당 셀을 점유 중인지 확인합니다.
## BaseUnit.place_on_tile()에서 기록한 occupied_tile_cell을 기준으로 중복 배치를 막습니다. ## - 몬스터가 지나다니는 길 타일은 유닛이 없어도 "점유됨"으로 취급해 배치를 막습니다.
## - BaseUnit.place_on_tile()에서 기록한 occupied_tile_cell을 기준으로 중복 배치를 막습니다.
func _is_tile_occupied(tile_cell: Vector3i) -> bool: func _is_tile_occupied(tile_cell: Vector3i) -> bool:
var grid_map: GridMap = _get_stage_grid_map()
if grid_map and _is_monster_path_tile(grid_map, tile_cell):
return true
var units: Array[Node] = _hud.get_tree().get_nodes_in_group("units") var units: Array[Node] = _hud.get_tree().get_nodes_in_group("units")
for unit_node: Node in units: for unit_node: Node in units:
if unit_node is BaseUnit: if unit_node is BaseUnit:
...@@ -332,6 +340,94 @@ func _is_tile_occupied(tile_cell: Vector3i) -> bool: ...@@ -332,6 +340,94 @@ func _is_tile_occupied(tile_cell: Vector3i) -> bool:
return true return true
return false return false
## 몬스터 이동 경로로 판단되는 타일인지 확인합니다.
## 길 타일은 "이미 유닛이 있다"는 의미의 점유와는 성격이 다르지만, 최종 배치 가능 여부에서는 똑같이 막혀야 합니다.
func _is_monster_path_tile(grid_map: GridMap, tile_cell: Vector3i) -> bool:
_ensure_monster_path_tile_cache(grid_map)
return _monster_path_tile_cache.has(tile_cell)
## 현재 GridMap 기준으로 몬스터 길 타일 캐시를 준비합니다.
## 배치 오버레이를 그릴 때 모든 셀마다 Path3D와 거리 계산을 반복하면 불필요하게 비싸므로, 맵이 바뀔 때 한 번만 계산합니다.
func _ensure_monster_path_tile_cache(grid_map: GridMap) -> void:
if _monster_path_tile_cache_grid_map == grid_map:
return
_monster_path_tile_cache_grid_map = grid_map
_monster_path_tile_cache.clear()
var used_cells: Array = grid_map.get_used_cells()
if used_cells.is_empty():
return
var cell_bounds: Dictionary = _get_used_cell_xz_bounds(used_cells)
var monster_path: Path3D = _get_monster_path()
for cell: Vector3i in used_cells:
if _is_outer_edge_cell(cell, cell_bounds) or _is_cell_close_to_monster_path(grid_map, monster_path, cell):
_monster_path_tile_cache[cell] = true
## GridMap에서 사용 중인 셀들의 x/z 최소/최대 범위를 계산합니다.
## 현재 맵은 몬스터 길이 외곽을 따라 돌기 때문에, 외곽 셀을 먼저 막으면 Path3D가 살짝 어긋나도 가장자리 배치가 열리지 않습니다.
func _get_used_cell_xz_bounds(used_cells: Array) -> Dictionary:
var first_cell: Vector3i = used_cells[0] as Vector3i
var min_x: int = first_cell.x
var max_x: int = first_cell.x
var min_z: int = first_cell.z
var max_z: int = first_cell.z
for cell: Vector3i in used_cells:
min_x = mini(min_x, cell.x)
max_x = maxi(max_x, cell.x)
min_z = mini(min_z, cell.z)
max_z = maxi(max_z, cell.z)
return {
"min_x": min_x,
"max_x": max_x,
"min_z": min_z,
"max_z": max_z
}
## 사용 셀 범위의 가장자리에 있는 셀인지 확인합니다.
## 사용자가 말한 "TileMap의 가장자리"를 직접 반영하는 빠른 판정입니다.
func _is_outer_edge_cell(tile_cell: Vector3i, cell_bounds: Dictionary) -> bool:
return (
tile_cell.x == cell_bounds["min_x"]
or tile_cell.x == cell_bounds["max_x"]
or tile_cell.z == cell_bounds["min_z"]
or tile_cell.z == cell_bounds["max_z"]
)
## 타일 중심이 MonsterPath에 충분히 가까우면 몬스터 길로 간주합니다.
## 외곽 길이 아닌 스테이지가 추가되어도 Path3D를 따라 배치 금지 영역이 자동으로 생기도록 하는 보강 판정입니다.
func _is_cell_close_to_monster_path(grid_map: GridMap, monster_path: Path3D, tile_cell: Vector3i) -> bool:
if not monster_path or not monster_path.curve:
return false
var tile_center: Vector3 = grid_map.to_global(grid_map.map_to_local(tile_cell))
var local_tile_center: Vector3 = monster_path.to_local(tile_center)
var closest_local_point: Vector3 = monster_path.curve.get_closest_point(local_tile_center)
var closest_world_point: Vector3 = monster_path.to_global(closest_local_point)
# 배치 판정은 바닥 평면 기준이므로 y 높이 차이는 무시합니다.
# half_tile_width에 작은 여유를 더해 경로가 타일 중앙에서 조금 벗어나 있어도 해당 길 타일이 잠기도록 합니다.
tile_center.y = 0.0
closest_world_point.y = 0.0
var half_tile_width: float = maxf(grid_map.cell_size.x, grid_map.cell_size.z) * 0.5
var path_margin: float = maxf(grid_map.cell_size.x, grid_map.cell_size.z) * MONSTER_PATH_TILE_EXTRA_MARGIN_RATIO
return tile_center.distance_to(closest_world_point) <= half_tile_width + path_margin
## 현재 전장 씬에 배치된 몬스터 이동 Path3D를 찾습니다.
## main.tscn에서는 MonsterPath라는 이름을 사용하지만, 고정 NodePath 대신 재귀 탐색으로 씬 구조 변경에 조금 더 견고하게 둡니다.
func _get_monster_path() -> Path3D:
var current_scene: Node = _hud.get_tree().current_scene
if not current_scene:
return null
var path_node: Node = current_scene.find_child("MonsterPath", true, false)
if path_node is Path3D:
return path_node as Path3D
return null
## 배치 모드 진입 시 모든 사용 타일 위에 가능/불가 오버레이를 표시합니다. ## 배치 모드 진입 시 모든 사용 타일 위에 가능/불가 오버레이를 표시합니다.
func _show_placement_tile_overlays() -> void: func _show_placement_tile_overlays() -> void:
_clear_placement_tile_overlays() _clear_placement_tile_overlays()
...@@ -342,6 +438,7 @@ func _show_placement_tile_overlays() -> void: ...@@ -342,6 +438,7 @@ func _show_placement_tile_overlays() -> void:
return return
var used_cells: Array = grid_map.get_used_cells() var used_cells: Array = grid_map.get_used_cells()
_ensure_monster_path_tile_cache(grid_map)
_ensure_overlay_pool_capacity(parent_scene, used_cells.size()) _ensure_overlay_pool_capacity(parent_scene, used_cells.size())
for cell: Vector3i in used_cells: for cell: Vector3i in used_cells:
_add_placement_tile_overlay(grid_map, cell) _add_placement_tile_overlay(grid_map, cell)
...@@ -359,14 +456,16 @@ func _clear_placement_tile_overlays() -> void: ...@@ -359,14 +456,16 @@ func _clear_placement_tile_overlays() -> void:
## 타일 하나에 대응하는 오버레이 씬을 풀에서 꺼내 위치/크기/재질을 설정합니다. ## 타일 하나에 대응하는 오버레이 씬을 풀에서 꺼내 위치/크기/재질을 설정합니다.
## 새 MeshInstance3D나 PlaneMesh를 만들지 않고 placement_tile_overlay.tscn 인스턴스를 재사용합니다. ## 새 MeshInstance3D나 PlaneMesh를 만들지 않고 placement_tile_overlay.tscn 인스턴스를 재사용합니다.
func _add_placement_tile_overlay(grid_map: GridMap, tile_cell: Vector3i) -> void: func _add_placement_tile_overlay(grid_map: GridMap, tile_cell: Vector3i) -> void:
if _is_monster_path_tile(grid_map, tile_cell):
return
var is_occupied: bool = _is_tile_occupied(tile_cell) var is_occupied: bool = _is_tile_occupied(tile_cell)
var overlay: Node = _take_overlay_from_pool() var overlay: Node = _take_overlay_from_pool()
if not overlay: if not overlay:
return return
var tile_center: Vector3 = grid_map.to_global(grid_map.map_to_local(tile_cell)) var tile_center: Vector3 = grid_map.to_global(grid_map.map_to_local(tile_cell))
tile_center.y += 1.03 var tile_size: Vector2 = Vector2(grid_map.cell_size.x, grid_map.cell_size.z)
var tile_size: Vector2 = Vector2(grid_map.cell_size.x * 0.92, grid_map.cell_size.z * 0.92)
if overlay.has_method("configure"): if overlay.has_method("configure"):
overlay.call("configure", tile_center, tile_size, _get_placement_material(is_occupied), is_occupied) overlay.call("configure", tile_center, tile_size, _get_placement_material(is_occupied), is_occupied)
_active_overlays.append(overlay) _active_overlays.append(overlay)
...@@ -417,6 +516,8 @@ func _reset_overlay_pool_if_parent_changed(parent_scene: Node) -> void: ...@@ -417,6 +516,8 @@ func _reset_overlay_pool_if_parent_changed(parent_scene: Node) -> void:
_overlay_pool.clear() _overlay_pool.clear()
_active_overlays.clear() _active_overlays.clear()
_monster_path_tile_cache_grid_map = null
_monster_path_tile_cache.clear()
_overlay_pool_parent = parent_scene _overlay_pool_parent = parent_scene
## 비활성 오버레이 하나를 풀에서 꺼냅니다. ## 비활성 오버레이 하나를 풀에서 꺼냅니다.
......
...@@ -3,28 +3,47 @@ extends MeshInstance3D ...@@ -3,28 +3,47 @@ extends MeshInstance3D
## 유닛 배치 모드에서 타일 위에 표시되는 3D 오버레이 노드입니다. ## 유닛 배치 모드에서 타일 위에 표시되는 3D 오버레이 노드입니다.
## - 이 노드는 placement_tile_overlay.tscn의 루트로 사용됩니다. ## - 이 노드는 placement_tile_overlay.tscn의 루트로 사용됩니다.
## - HudUnitPlacement는 이 씬을 풀로 미리 만들어 두고, 배치 모드마다 configure()로 재사용합니다. ## - HudUnitPlacement는 이 씬을 풀로 미리 만들어 두고, 배치 모드마다 configure()로 재사용합니다.
## - PlaneMesh 자체는 씬에 포함된 1x1 크기를 사용하고, 실제 타일 크기는 scale로 맞춰 런타임 Mesh 생성을 피합니다. ## - 씬에는 원판, 링, 코너 마커, 금지선이 미리 들어 있으므로 런타임에는 색/표시 상태만 바꿉니다.
## - 실제 타일 크기는 루트 scale로 맞춰, 배치 모드 진입 때마다 Mesh를 새로 만들지 않습니다.
const HIDDEN_POSITION: Vector3 = Vector3(0.0, -100.0, 0.0) const HIDDEN_POSITION: Vector3 = Vector3(0.0, -100.0, 0.0)
const AVAILABLE_FILL_COLOR: Color = Color(0.05, 1.0, 0.38, 0.24)
const AVAILABLE_ACCENT_COLOR: Color = Color(0.25, 1.0, 0.72, 0.86)
const AVAILABLE_CORE_COLOR: Color = Color(0.70, 1.0, 0.86, 0.62)
const OCCUPIED_FILL_COLOR: Color = Color(1.0, 0.06, 0.03, 0.20)
const OCCUPIED_ACCENT_COLOR: Color = Color(1.0, 0.18, 0.10, 0.88)
const OCCUPIED_CORE_COLOR: Color = Color(1.0, 0.48, 0.32, 0.62)
@onready var _outer_ring: MeshInstance3D = get_node_or_null("OuterRing") as MeshInstance3D
@onready var _inner_ring: MeshInstance3D = get_node_or_null("InnerRing") as MeshInstance3D
@onready var _core_disc: MeshInstance3D = get_node_or_null("CoreDisc") as MeshInstance3D
@onready var _corner_markers: Node3D = get_node_or_null("CornerMarkers") as Node3D
@onready var _deny_marks: Node3D = get_node_or_null("DenyMarks") as Node3D
## 풀에서 대기 중인지 여부입니다. ## 풀에서 대기 중인지 여부입니다.
## 디버깅 중 씬 트리에서 오버레이 상태를 확인할 때 쓸 수 있도록 명시적으로 보관합니다. ## 디버깅 중 씬 트리에서 오버레이 상태를 확인할 때 쓸 수 있도록 명시적으로 보관합니다.
var is_available_in_pool: bool = true var is_available_in_pool: bool = true
var _overlay_material_cache: Dictionary = {}
func _ready() -> void: func _ready() -> void:
# 오버레이는 순수 표시용이므로 그림자와 입력 충돌 비용을 만들지 않습니다. # 오버레이는 순수 표시용입니다.
cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF # 루트와 자식 모두 그림자를 끄면 투명 발광 메시가 타일 위에 떠 있을 때도 불필요한 렌더 비용이 늘지 않습니다.
_disable_shadow_recursively(self)
visible = false visible = false
global_position = HIDDEN_POSITION global_position = HIDDEN_POSITION
## 풀에서 꺼낸 오버레이를 실제 타일 위치/크기/재질로 설정하고 표시합니다. ## 풀에서 꺼낸 오버레이를 실제 타일 위치/크기/재질로 설정하고 표시합니다.
## PlaneMesh는 기본 1x1 크기이므로 scale.x/z로 GridMap 셀 크기에 맞춥니다. ## material 인자는 이전 단순 원판 오버레이와의 호출 호환성을 위해 유지하지만, 실제 색상은 이 씬의 파츠별 머티리얼로 제어합니다.
func configure(tile_center: Vector3, tile_size: Vector2, material: Material, is_occupied: bool) -> void: func configure(tile_center: Vector3, tile_size: Vector2, _material: Material, is_occupied: bool) -> void:
is_available_in_pool = false is_available_in_pool = false
name = "OccupiedTileOverlay" if is_occupied else "AvailableTileOverlay" name = "OccupiedTileOverlay" if is_occupied else "AvailableTileOverlay"
global_position = tile_center global_position = Vector3(tile_center.x, 1.0, tile_center.z)
scale = Vector3(tile_size.x, 1.0, tile_size.y) scale = Vector3(tile_size.x * 0.86, 1.0, tile_size.y * 0.86)
material_override = material material_override = _get_overlay_material(
OCCUPIED_FILL_COLOR if is_occupied else AVAILABLE_FILL_COLOR,
OCCUPIED_ACCENT_COLOR if is_occupied else AVAILABLE_ACCENT_COLOR
)
_apply_visual_state(is_occupied)
visible = true visible = true
## 배치 모드가 끝나면 삭제하지 않고 풀에 반환될 수 있도록 숨깁니다. ## 배치 모드가 끝나면 삭제하지 않고 풀에 반환될 수 있도록 숨깁니다.
...@@ -34,3 +53,64 @@ func release_to_pool() -> void: ...@@ -34,3 +53,64 @@ func release_to_pool() -> void:
visible = false visible = false
global_position = HIDDEN_POSITION global_position = HIDDEN_POSITION
material_override = null material_override = null
## 배치 가능/불가 상태에 맞춰 씬 안의 장식 파츠 색상과 표시 여부를 전환합니다.
## 가능 타일은 청록색 링과 마커가 살아나고, 점유 타일은 붉은 링과 X 표시가 켜집니다.
func _apply_visual_state(is_occupied: bool) -> void:
var accent_color: Color = OCCUPIED_ACCENT_COLOR if is_occupied else AVAILABLE_ACCENT_COLOR
var core_color: Color = OCCUPIED_CORE_COLOR if is_occupied else AVAILABLE_CORE_COLOR
var fill_color: Color = OCCUPIED_FILL_COLOR if is_occupied else AVAILABLE_FILL_COLOR
_apply_material_to_mesh(_outer_ring, accent_color, accent_color)
_apply_material_to_mesh(_inner_ring, core_color, accent_color)
_apply_material_to_mesh(_core_disc, fill_color, core_color)
_apply_material_to_children(_corner_markers, accent_color, accent_color)
_apply_material_to_children(_deny_marks, OCCUPIED_ACCENT_COLOR, OCCUPIED_ACCENT_COLOR)
if _corner_markers:
_corner_markers.visible = not is_occupied
if _deny_marks:
_deny_marks.visible = is_occupied
## MeshInstance3D 하나에 발광 투명 머티리얼을 적용합니다.
## 각 파츠가 서로 다른 색을 가질 수 있도록 material_override를 개별로 갱신합니다.
func _apply_material_to_mesh(mesh_instance: MeshInstance3D, color: Color, emission_color: Color) -> void:
if not mesh_instance:
return
mesh_instance.material_override = _get_overlay_material(color, emission_color)
## 여러 조각으로 나뉜 장식 그룹에 같은 머티리얼을 적용합니다.
## 코너 마커와 금지선은 자식 BoxMesh들의 묶음이라, 그룹 루트 아래를 순회합니다.
func _apply_material_to_children(parent_node: Node, color: Color, emission_color: Color) -> void:
if not parent_node:
return
for child: Node in parent_node.get_children():
if child is MeshInstance3D:
_apply_material_to_mesh(child as MeshInstance3D, color, emission_color)
## 타일 위에서 잘 보이도록 비조명/알파/발광 머티리얼을 만듭니다.
## 머티리얼은 상태 전환 때 공유해도 안전한 값만 담고 있어, 오버레이 풀의 각 인스턴스가 가볍게 재사용할 수 있습니다.
func _get_overlay_material(color: Color, emission_color: Color) -> StandardMaterial3D:
var cache_key: String = "%s|%s" % [color.to_html(true), emission_color.to_html(true)]
if _overlay_material_cache.has(cache_key):
return _overlay_material_cache[cache_key] as StandardMaterial3D
var material: StandardMaterial3D = StandardMaterial3D.new()
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
material.albedo_color = color
material.emission_enabled = true
material.emission = emission_color
material.emission_energy_multiplier = 1.7
material.cull_mode = BaseMaterial3D.CULL_DISABLED
material.no_depth_test = true
_overlay_material_cache[cache_key] = material
return material
## 루트와 자식 표시 메시들의 그림자를 모두 끕니다.
## 씬 파츠가 늘어나도 _ready()에서 한 번만 처리하면, 개별 노드 설정 누락으로 그림자가 생기는 일을 막을 수 있습니다.
func _disable_shadow_recursively(node: Node) -> void:
if node is GeometryInstance3D:
(node as GeometryInstance3D).cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
for child: Node in node.get_children():
_disable_shadow_recursively(child)
[gd_scene format=3] [gd_scene format=4 uid="uid://cjds6pckdmd77"]
[ext_resource type="Script" path="res://src/ui/unit_placement/placement_tile_overlay.gd" id="1_67clr"] [ext_resource type="Script" uid="uid://bd00wvrr48mfi" path="res://src/ui/unit_placement/placement_tile_overlay.gd" id="1_67clr"]
[sub_resource type="PlaneMesh" id="PlaneMesh_y7kwe"] [sub_resource type="CylinderMesh" id="CylinderMesh_y7kwe"]
size = Vector2(1, 1) height = 0.012
rings = 1
[node name="PlacementTileOverlay" type="MeshInstance3D" unique_id=1113555824] [sub_resource type="ArrayMesh" id="ArrayMesh_akyd8"]
_surfaces = [{
"aabb": AABB(-0.5, 0.018, -0.5, 1, 1e-05, 1),
"format": 34359742465,
"index_count": 384,
"index_data": PackedByteArray("AAACAAEAAQACAAMAAgAEAAMAAwAEAAUABAAGAAUABQAGAAcABgAIAAcABwAIAAkACAAKAAkACQAKAAsACgAMAAsACwAMAA0ADAAOAA0ADQAOAA8ADgAQAA8ADwAQABEAEAASABEAEQASABMAEgAUABMAEwAUABUAFAAWABUAFQAWABcAFgAYABcAFwAYABkAGAAaABkAGQAaABsAGgAcABsAGwAcAB0AHAAeAB0AHQAeAB8AHgAgAB8AHwAgACEAIAAiACEAIQAiACMAIgAkACMAIwAkACUAJAAmACUAJQAmACcAJgAoACcAJwAoACkAKAAqACkAKQAqACsAKgAsACsAKwAsAC0ALAAuAC0ALQAuAC8ALgAwAC8ALwAwADEAMAAyADEAMQAyADMAMgA0ADMAMwA0ADUANAA2ADUANQA2ADcANgA4ADcANwA4ADkAOAA6ADkAOQA6ADsAOgA8ADsAOwA8AD0APAA+AD0APQA+AD8APgBAAD8APwBAAEEAQABCAEEAQQBCAEMAQgBEAEMAQwBEAEUARABGAEUARQBGAEcARgBIAEcARwBIAEkASABKAEkASQBKAEsASgBMAEsASwBMAE0ATABOAE0ATQBOAE8ATgBQAE8ATwBQAFEAUABSAFEAUQBSAFMAUgBUAFMAUwBUAFUAVABWAFUAVQBWAFcAVgBYAFcAVwBYAFkAWABaAFkAWQBaAFsAWgBcAFsAWwBcAF0AXABeAF0AXQBeAF8AXgBgAF8AXwBgAGEAYABiAGEAYQBiAGMAYgBkAGMAYwBkAGUAZABmAGUAZQBmAGcAZgBoAGcAZwBoAGkAaABqAGkAaQBqAGsAagBsAGsAawBsAG0AbABuAG0AbQBuAG8AbgBwAG8AbwBwAHEAcAByAHEAcQByAHMAcgB0AHMAcwB0AHUAdAB2AHUAdQB2AHcAdgB4AHcAdwB4AHkAeAB6AHkAeQB6AHsAegB8AHsAewB8AH0AfAB+AH0AfQB+AH8AfgAAAH8AfwAAAAEA"),
"primitive": 3,
"uv_scale": Vector4(0, 0, 0, 0),
"vertex_count": 128,
"vertex_data": PackedByteArray("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")
}]
[sub_resource type="ArrayMesh" id="ArrayMesh_dwcvq"]
_surfaces = [{
"aabb": AABB(-0.31, 0.02, -0.31, 0.62, 1e-05, 0.62),
"format": 34359742465,
"index_count": 384,
"index_data": PackedByteArray("AAACAAEAAQACAAMAAgAEAAMAAwAEAAUABAAGAAUABQAGAAcABgAIAAcABwAIAAkACAAKAAkACQAKAAsACgAMAAsACwAMAA0ADAAOAA0ADQAOAA8ADgAQAA8ADwAQABEAEAASABEAEQASABMAEgAUABMAEwAUABUAFAAWABUAFQAWABcAFgAYABcAFwAYABkAGAAaABkAGQAaABsAGgAcABsAGwAcAB0AHAAeAB0AHQAeAB8AHgAgAB8AHwAgACEAIAAiACEAIQAiACMAIgAkACMAIwAkACUAJAAmACUAJQAmACcAJgAoACcAJwAoACkAKAAqACkAKQAqACsAKgAsACsAKwAsAC0ALAAuAC0ALQAuAC8ALgAwAC8ALwAwADEAMAAyADEAMQAyADMAMgA0ADMAMwA0ADUANAA2ADUANQA2ADcANgA4ADcANwA4ADkAOAA6ADkAOQA6ADsAOgA8ADsAOwA8AD0APAA+AD0APQA+AD8APgBAAD8APwBAAEEAQABCAEEAQQBCAEMAQgBEAEMAQwBEAEUARABGAEUARQBGAEcARgBIAEcARwBIAEkASABKAEkASQBKAEsASgBMAEsASwBMAE0ATABOAE0ATQBOAE8ATgBQAE8ATwBQAFEAUABSAFEAUQBSAFMAUgBUAFMAUwBUAFUAVABWAFUAVQBWAFcAVgBYAFcAVwBYAFkAWABaAFkAWQBaAFsAWgBcAFsAWwBcAF0AXABeAF0AXQBeAF8AXgBgAF8AXwBgAGEAYABiAGEAYQBiAGMAYgBkAGMAYwBkAGUAZABmAGUAZQBmAGcAZgBoAGcAZwBoAGkAaABqAGkAaQBqAGsAagBsAGsAawBsAG0AbABuAG0AbQBuAG8AbgBwAG8AbwBwAHEAcAByAHEAcQByAHMAcgB0AHMAcwB0AHUAdAB2AHUAdQB2AHcAdgB4AHcAdwB4AHkAeAB6AHkAeQB6AHsAegB8AHsAewB8AH0AfAB+AH0AfQB+AH8AfgAAAH8AfwAAAAEA"),
"primitive": 3,
"uv_scale": Vector4(0, 0, 0, 0),
"vertex_count": 128,
"vertex_data": PackedByteArray("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")
}]
[sub_resource type="CylinderMesh" id="CylinderMesh_qy5no"]
top_radius = 0.18
bottom_radius = 0.18
height = 0.01
radial_segments = 48
rings = 1
[sub_resource type="BoxMesh" id="BoxMesh_twcpl"]
size = Vector3(0.18, 0.018, 0.04)
[sub_resource type="BoxMesh" id="BoxMesh_4do50"]
size = Vector3(0.18, 0.018, 0.04)
[sub_resource type="BoxMesh" id="BoxMesh_k30w8"]
size = Vector3(0.18, 0.018, 0.04)
[sub_resource type="BoxMesh" id="BoxMesh_o5c5m"]
size = Vector3(0.18, 0.018, 0.04)
[sub_resource type="BoxMesh" id="BoxMesh_k81eb"]
size = Vector3(0.76, 0.024, 0.06)
[sub_resource type="BoxMesh" id="BoxMesh_navjj"]
size = Vector3(0.76, 0.024, 0.06)
[node name="PlacementTileOverlay" type="MeshInstance3D" unique_id=824361433]
visible = false visible = false
cast_shadow = 0 cast_shadow = 0
mesh = SubResource("PlaneMesh_y7kwe") mesh = SubResource("CylinderMesh_y7kwe")
script = ExtResource("1_67clr") script = ExtResource("1_67clr")
[node name="OuterRing" type="MeshInstance3D" parent="." unique_id=2031889431]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.006, 0)
cast_shadow = 0
mesh = SubResource("ArrayMesh_akyd8")
[node name="InnerRing" type="MeshInstance3D" parent="." unique_id=1922934890]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.014, 0)
cast_shadow = 0
mesh = SubResource("ArrayMesh_dwcvq")
[node name="CoreDisc" type="MeshInstance3D" parent="." unique_id=1007825039]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.02, 0)
cast_shadow = 0
mesh = SubResource("CylinderMesh_qy5no")
[node name="CornerMarkers" type="Node3D" parent="." unique_id=821439029]
[node name="NorthMarker" type="MeshInstance3D" parent="CornerMarkers" unique_id=1322843937]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.032, -0.4)
cast_shadow = 0
mesh = SubResource("BoxMesh_twcpl")
[node name="EastMarker" type="MeshInstance3D" parent="CornerMarkers" unique_id=1995589702]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0.4, 0.032, 0)
cast_shadow = 0
mesh = SubResource("BoxMesh_4do50")
[node name="SouthMarker" type="MeshInstance3D" parent="CornerMarkers" unique_id=223227002]
transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0, 0.032, 0.4)
cast_shadow = 0
mesh = SubResource("BoxMesh_k30w8")
[node name="WestMarker" type="MeshInstance3D" parent="CornerMarkers" unique_id=25749323]
transform = Transform3D(1.1924881e-08, 0, -1, 0, 1, 0, 1, 0, 1.1924881e-08, -0.4, 0.032, 0)
cast_shadow = 0
mesh = SubResource("BoxMesh_o5c5m")
[node name="DenyMarks" type="Node3D" parent="." unique_id=1649821206]
visible = false
[node name="DenyBarForward" type="MeshInstance3D" parent="DenyMarks" unique_id=1278658018]
transform = Transform3D(0.70710677, 0, 0.70710677, 0, 1, 0, -0.70710677, 0, 0.70710677, 0, 0.04, 0)
cast_shadow = 0
mesh = SubResource("BoxMesh_k81eb")
[node name="DenyBarBackward" type="MeshInstance3D" parent="DenyMarks" unique_id=247097600]
transform = Transform3D(0.70710677, 0, -0.70710677, 0, 1, 0, 0.70710677, 0, 0.70710677, 0, 0.04, 0)
cast_shadow = 0
mesh = SubResource("BoxMesh_navjj")
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