Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
L
ldefense
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Packages
Packages
Container Registry
Analytics
CI / CD Analytics
Repository Analytics
Value Stream Analytics
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Gavin An
ldefense
Commits
4b394e75
Commit
4b394e75
authored
Jul 20, 2026
by
Gavin An
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
작업중
parent
9d51f839
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
382 additions
and
27 deletions
+382
-27
src/core/rts_camera.gd
src/core/rts_camera.gd
+3
-3
src/entities/units/mage/mage_unit.gd
src/entities/units/mage/mage_unit.gd
+1
-1
src/ui/hud.gd
src/ui/hud.gd
+79
-5
src/ui/unit_placement/hud_unit_placement.gd
src/ui/unit_placement/hud_unit_placement.gd
+104
-3
src/ui/unit_placement/placement_tile_overlay.gd
src/ui/unit_placement/placement_tile_overlay.gd
+89
-9
src/ui/unit_placement/placement_tile_overlay.tscn
src/ui/unit_placement/placement_tile_overlay.tscn
+106
-6
No files found.
src/core/rts_camera.gd
View file @
4b394e75
...
@@ -4,9 +4,9 @@ extends Camera3D
...
@@ -4,9 +4,9 @@ extends Camera3D
## 하늘에서 전장을 살짝 비스듬히 내려다보는 고정 카메라입니다.
## 하늘에서 전장을 살짝 비스듬히 내려다보는 고정 카메라입니다.
## 이동, 줌인, 줌아웃 입력 없이 항상 맵 중앙을 바라봅니다.
## 이동, 줌인, 줌아웃 입력 없이 항상 맵 중앙을 바라봅니다.
@
export
var
fixed_position
:
Vector3
=
Vector3
(
0.0
,
2
2.0
,
11
.0
)
@
export
var
fixed_position
:
Vector3
=
Vector3
(
0.0
,
2
4.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
:
...
...
src/entities/units/mage/mage_unit.gd
View file @
4b394e75
...
@@ -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"
)
...
...
src/ui/hud.gd
View file @
4b394e75
...
@@ -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
=
"G
OLD: "
+
str
(
gold
)
+
"
G"
gold_label
.
text
=
"G
old "
+
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
=
"S
TAGE: "
+
str
(
stage
)
+
" /
"
+
str
(
GameManager
.
MAX_STAGE
)
stage_label
.
text
=
"S
tage "
+
str
(
stage
)
+
"/
"
+
str
(
GameManager
.
MAX_STAGE
)
## 필드에 남아 있는 몬스터 수를 갱신합니다.
## 필드에 남아 있는 몬스터 수를 갱신합니다.
func
_on_monster_changed
(
count
:
int
)
->
void
:
func
_on_monster_changed
(
count
:
int
)
->
void
:
monster_label
.
text
=
"M
ONSTERS: "
+
str
(
count
)
+
" /
"
+
str
(
GameManager
.
DEFEAT_MONSTER_LIMIT
)
monster_label
.
text
=
"M
onsters "
+
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"
)
...
...
src/ui/unit_placement/hud_unit_placement.gd
View file @
4b394e75
...
@@ -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
## 비활성 오버레이 하나를 풀에서 꺼냅니다.
## 비활성 오버레이 하나를 풀에서 꺼냅니다.
...
...
src/ui/unit_placement/placement_tile_overlay.gd
View file @
4b394e75
...
@@ -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
)
src/ui/unit_placement/placement_tile_overlay.tscn
View file @
4b394e75
[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("
Plane
Mesh_y7kwe")
mesh = SubResource("
Cylinder
Mesh_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")
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment