diff --git a/DungeonShooting_Godot/prefab/ui/MapEditor.tscn b/DungeonShooting_Godot/prefab/ui/MapEditor.tscn
index cd8e817..357a8dd 100644
--- a/DungeonShooting_Godot/prefab/ui/MapEditor.tscn
+++ b/DungeonShooting_Godot/prefab/ui/MapEditor.tscn
@@ -1,11 +1,13 @@
-[gd_scene load_steps=10 format=3 uid="uid://csbxfkdupsckv"]
+[gd_scene load_steps=12 format=3 uid="uid://csbxfkdupsckv"]
[ext_resource type="Script" path="res://src/game/ui/mapEditor/MapEditorPanel.cs" id="1_5s7a0"]
[ext_resource type="Texture2D" uid="uid://cajcnlimvoxk" path="res://resource/sprite/ui/mapEditorProject/Back.png" id="2_s2w5x"]
[ext_resource type="TileSet" uid="uid://b00g22o1cqhe8" path="res://resource/map/tileSet/TileSet1.tres" id="2_vrg60"]
-[ext_resource type="Script" path="res://src/game/ui/mapEditor/TileView/EditorTileMap.cs" id="2_waq8f"]
[ext_resource type="Texture2D" uid="uid://0878uloew5jo" path="res://resource/sprite/ui/mapEditor/ErrorCell.png" id="4_465u2"]
+[ext_resource type="Script" path="res://src/game/ui/mapEditor/tileView/EditorTileMap.cs" id="4_wbtsa"]
[ext_resource type="PackedScene" uid="uid://b4u66mxndxbrg" path="res://prefab/ui/MapEditorTools.tscn" id="6_7pvgu"]
+[ext_resource type="Texture2D" uid="uid://dmm8jw06bhffh" path="res://resource/sprite/ui/mapEditorTools/Lock.png" id="7_lli1g"]
+[ext_resource type="Texture2D" uid="uid://dqvg18aacx6db" path="res://resource/sprite/ui/mapEditorTools/Visible.png" id="8_gm7y5"]
[sub_resource type="Animation" id="Animation_o3btm"]
length = 0.001
@@ -144,7 +146,7 @@
scale = Vector2(4, 4)
tile_set = ExtResource("2_vrg60")
format = 2
-script = ExtResource("2_waq8f")
+script = ExtResource("4_wbtsa")
[node name="ErrorCell" type="Sprite2D" parent="Bg/VBoxContainer/HSplitContainer/Left/MarginContainer/MapView/SubViewport/TileMap"]
visible = false
@@ -172,3 +174,47 @@
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 3.0
+
+[node name="MarginContainer" type="MarginContainer" parent="Bg/VBoxContainer/HSplitContainer/Right"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 2
+theme_override_constants/margin_top = 2
+theme_override_constants/margin_right = 2
+theme_override_constants/margin_bottom = 2
+
+[node name="TabContainer" type="TabContainer" parent="Bg/VBoxContainer/HSplitContainer/Right/MarginContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="MapLayer" type="MarginContainer" parent="Bg/VBoxContainer/HSplitContainer/Right/MarginContainer/TabContainer"]
+layout_mode = 2
+theme_override_constants/margin_top = 10
+
+[node name="ScrollContainer" type="ScrollContainer" parent="Bg/VBoxContainer/HSplitContainer/Right/MarginContainer/TabContainer/MapLayer"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="LayerButton" type="Button" parent="Bg/VBoxContainer/HSplitContainer/Right/MarginContainer/TabContainer/MapLayer/ScrollContainer"]
+custom_minimum_size = Vector2(0, 70)
+layout_mode = 2
+size_flags_horizontal = 3
+text = "layer1"
+icon = ExtResource("7_lli1g")
+alignment = 0
+
+[node name="VisibleButton" type="TextureButton" parent="Bg/VBoxContainer/HSplitContainer/Right/MarginContainer/TabContainer/MapLayer/ScrollContainer/LayerButton"]
+layout_mode = 1
+anchors_preset = 11
+anchor_left = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = -55.0
+grow_horizontal = 0
+grow_vertical = 2
+texture_normal = ExtResource("8_gm7y5")
+stretch_mode = 3
diff --git a/DungeonShooting_Godot/resource/map/tileMaps/TestGroup1/battle/Room2/Room2_roomInfo.json b/DungeonShooting_Godot/resource/map/tileMaps/TestGroup1/battle/Room2/Room2_roomInfo.json
index ae58317..13d9a10 100644
--- a/DungeonShooting_Godot/resource/map/tileMaps/TestGroup1/battle/Room2/Room2_roomInfo.json
+++ b/DungeonShooting_Godot/resource/map/tileMaps/TestGroup1/battle/Room2/Room2_roomInfo.json
@@ -1 +1 @@
-{"Position":{"X":-3,"Y":-3},"Size":{"X":6,"Y":6},"DoorAreaInfos":[{"Direction":3,"Start":16,"End":-16},{"Direction":2,"Start":16,"End":-16},{"Direction":1,"Start":16,"End":-16},{"Direction":0,"Start":16,"End":-16}],"GroupName":"TestGroup1","RoomType":0,"RoomName":"Room2","Weight":100,"Remark":""}
\ No newline at end of file
+{"Position":{"X":-5,"Y":-5},"Size":{"X":13,"Y":12},"DoorAreaInfos":[{"Direction":3,"Start":0,"End":176},{"Direction":0,"Start":0,"End":160},{"Direction":2,"Start":0,"End":176},{"Direction":1,"Start":0,"End":160}],"GroupName":"TestGroup1","RoomType":0,"RoomName":"Room2","Weight":100,"Remark":""}
\ No newline at end of file
diff --git a/DungeonShooting_Godot/resource/map/tileMaps/TestGroup1/battle/Room2/Room2_tileInfo.json b/DungeonShooting_Godot/resource/map/tileMaps/TestGroup1/battle/Room2/Room2_tileInfo.json
index 2e7fe73..7e35f85 100644
--- a/DungeonShooting_Godot/resource/map/tileMaps/TestGroup1/battle/Room2/Room2_tileInfo.json
+++ b/DungeonShooting_Godot/resource/map/tileMaps/TestGroup1/battle/Room2/Room2_tileInfo.json
@@ -1 +1 @@
-{"NavigationList":[{"Type":0,"Points":[-24,-24,24,-24,24,32,-24,32]}],"Floor":[-1,-2,0,0,8,-1,-1,0,0,8,-1,0,0,0,8,-1,1,0,0,8,0,-2,0,0,8,0,-1,0,0,8,0,0,0,0,8,0,1,0,0,8,1,-2,0,0,8,1,-1,0,0,8,1,0,0,0,8,1,1,0,0,8,-2,-2,0,0,8,-2,-1,0,0,8,-2,0,0,0,8,-2,1,0,0,8],"Middle":[-2,-3,0,2,7,-1,-3,0,2,7,0,-3,0,2,7,1,-3,0,2,7],"Top":[-3,-3,0,3,4,-3,-2,0,3,3,-3,-1,0,3,3,-3,0,0,3,3,-3,1,0,3,3,-3,2,0,11,2,-2,2,0,2,2,-1,2,0,2,2,0,2,0,2,2,1,2,0,2,2,2,-3,0,1,4,2,-2,0,1,3,2,-1,0,1,3,2,0,0,1,3,2,1,0,1,3,2,2,0,13,2]}
\ No newline at end of file
+{"NavigationList":[{"Type":0,"Points":[-56,-56,104,-56,104,96,-56,96]}],"Floor":[0,5,0,0,8,0,4,0,0,8,0,3,0,0,8,0,2,0,0,8,0,-1,0,0,8,0,-2,0,0,8,0,-3,0,0,8,0,-4,0,0,8,0,1,0,0,8,0,0,0,0,8,1,5,0,0,8,1,4,0,0,8,1,3,0,0,8,1,2,0,0,8,1,-1,0,0,8,1,-2,0,0,8,1,-3,0,0,8,1,-4,0,0,8,1,1,0,0,8,1,0,0,0,8,-4,5,0,0,8,-4,4,0,0,8,-4,3,0,0,8,-4,2,0,0,8,-4,1,0,0,8,-4,0,0,0,8,-4,-1,0,0,8,-4,-2,0,0,8,-4,-3,0,0,8,-4,-4,0,0,8,-3,5,0,0,8,-3,4,0,0,8,-3,3,0,0,8,-3,2,0,0,8,-3,1,0,0,8,-3,0,0,0,8,-3,-1,0,0,8,-3,-2,0,0,8,-3,-3,0,0,8,-3,-4,0,0,8,-2,5,0,0,8,-2,4,0,0,8,-2,3,0,0,8,-2,2,0,0,8,-2,1,0,0,8,-2,0,0,0,8,-2,-1,0,0,8,-2,-2,0,0,8,-2,-3,0,0,8,-2,-4,0,0,8,-1,5,0,0,8,-1,4,0,0,8,-1,3,0,0,8,-1,2,0,0,8,-1,1,0,0,8,-1,0,0,0,8,-1,-1,0,0,8,-1,-2,0,0,8,-1,-3,0,0,8,-1,-4,0,0,8,2,5,0,0,8,2,4,0,0,8,2,3,0,0,8,2,2,0,0,8,2,1,0,0,8,2,0,0,0,8,2,-1,0,0,8,2,-2,0,0,8,2,-3,0,0,8,2,-4,0,0,8,3,5,0,0,8,3,4,0,0,8,3,3,0,0,8,3,2,0,0,8,3,1,0,0,8,3,0,0,0,8,3,-1,0,0,8,3,-2,0,0,8,3,-3,0,0,8,3,-4,0,0,8,4,5,0,0,8,4,4,0,0,8,4,3,0,0,8,4,2,0,0,8,4,1,0,0,8,4,0,0,0,8,4,-1,0,0,8,4,-2,0,0,8,4,-3,0,0,8,4,-4,0,0,8,5,5,0,0,8,5,4,0,0,8,5,3,0,0,8,5,2,0,0,8,5,1,0,0,8,5,0,0,0,8,5,-1,0,0,8,5,-2,0,0,8,5,-3,0,0,8,5,-4,0,0,8,6,5,0,0,8,6,4,0,0,8,6,3,0,0,8,6,2,0,0,8,6,1,0,0,8,6,0,0,0,8,6,-1,0,0,8,6,-2,0,0,8,6,-3,0,0,8,6,-4,0,0,8],"Middle":[-4,-5,0,2,7,-3,-5,0,2,7,-2,-5,0,2,7,-1,-5,0,2,7,0,-5,0,2,7,1,-5,0,2,7,2,-5,0,2,7,3,-5,0,2,7,4,-5,0,2,7,5,-5,0,2,7,6,-5,0,2,7],"Top":[-5,-5,0,3,4,-5,-4,0,3,3,-5,-3,0,3,3,-5,-2,0,3,3,-5,-1,0,3,3,-5,0,0,3,3,-5,1,0,3,3,-5,2,0,3,3,-5,3,0,3,3,-5,4,0,3,3,-5,5,0,3,3,-5,6,0,11,2,-4,6,0,2,2,-3,6,0,2,2,-2,6,0,2,2,-1,6,0,2,2,0,6,0,2,2,1,6,0,2,2,2,6,0,2,2,3,6,0,2,2,4,6,0,2,2,5,6,0,2,2,6,6,0,2,2,7,-5,0,1,4,7,-4,0,1,3,7,-3,0,1,3,7,-2,0,1,3,7,-1,0,1,3,7,0,0,1,3,7,1,0,1,3,7,2,0,1,3,7,3,0,1,3,7,4,0,1,3,7,5,0,1,3,7,6,0,13,2]}
\ No newline at end of file
diff --git a/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Hide.png b/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Hide.png
new file mode 100644
index 0000000..1925305
--- /dev/null
+++ b/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Hide.png
Binary files differ
diff --git a/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Hide.png.import b/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Hide.png.import
new file mode 100644
index 0000000..7ab915f
--- /dev/null
+++ b/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Hide.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://tx8ynskqa5ad"
+path="res://.godot/imported/Hide.png-9e29fe6e5d5256a8bf655e87bfb1c514.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://resource/sprite/ui/mapEditorTools/Hide.png"
+dest_files=["res://.godot/imported/Hide.png-9e29fe6e5d5256a8bf655e87bfb1c514.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Lock.png b/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Lock.png
new file mode 100644
index 0000000..40f033f
--- /dev/null
+++ b/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Lock.png
Binary files differ
diff --git a/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Lock.png.import b/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Lock.png.import
new file mode 100644
index 0000000..b614d76
--- /dev/null
+++ b/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Lock.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dmm8jw06bhffh"
+path="res://.godot/imported/Lock.png-04701e8ff332d79831f8e7e3966494e3.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://resource/sprite/ui/mapEditorTools/Lock.png"
+dest_files=["res://.godot/imported/Lock.png-04701e8ff332d79831f8e7e3966494e3.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Unlock.png b/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Unlock.png
new file mode 100644
index 0000000..7244b37
--- /dev/null
+++ b/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Unlock.png
Binary files differ
diff --git a/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Unlock.png.import b/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Unlock.png.import
new file mode 100644
index 0000000..107458b
--- /dev/null
+++ b/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Unlock.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://jvcvtpvt2fid"
+path="res://.godot/imported/Unlock.png-ddcb534918f7b36b0aebe029c72635c5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://resource/sprite/ui/mapEditorTools/Unlock.png"
+dest_files=["res://.godot/imported/Unlock.png-ddcb534918f7b36b0aebe029c72635c5.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Visible.png b/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Visible.png
new file mode 100644
index 0000000..9bbd5cb
--- /dev/null
+++ b/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Visible.png
Binary files differ
diff --git a/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Visible.png.import b/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Visible.png.import
new file mode 100644
index 0000000..4179b9e
--- /dev/null
+++ b/DungeonShooting_Godot/resource/sprite/ui/mapEditorTools/Visible.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dqvg18aacx6db"
+path="res://.godot/imported/Visible.png-2ddbc06e87cdb9bc38126fec565ee66d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://resource/sprite/ui/mapEditorTools/Visible.png"
+dest_files=["res://.godot/imported/Visible.png-2ddbc06e87cdb9bc38126fec565ee66d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/DungeonShooting_Godot/resource/theme/mainTheme.tres b/DungeonShooting_Godot/resource/theme/mainTheme.tres
index b485bed..2983772 100644
--- a/DungeonShooting_Godot/resource/theme/mainTheme.tres
+++ b/DungeonShooting_Godot/resource/theme/mainTheme.tres
@@ -1,4 +1,4 @@
-[gd_resource type="Theme" load_steps=59 format=3 uid="uid://ds668te2rph30"]
+[gd_resource type="Theme" load_steps=60 format=3 uid="uid://ds668te2rph30"]
[ext_resource type="FontFile" uid="uid://cad0in7dtweo5" path="res://resource/font/VonwaonBitmap-16px.ttf" id="1_1e6k7"]
@@ -378,6 +378,23 @@
border_width_bottom = 2
border_color = Color(0.188235, 0.188235, 0.188235, 1)
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_kt3n1"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(1, 1, 1, 0.75)
+draw_center = false
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+corner_detail = 5
+expand_margin_left = 2.0
+expand_margin_top = 2.0
+expand_margin_right = 2.0
+expand_margin_bottom = 2.0
+
[resource]
default_font = ExtResource("1_1e6k7")
default_font_size = 32
@@ -489,3 +506,4 @@
TextEdit/styles/normal = SubResource("StyleBoxFlat_0jpwx")
TextEdit/styles/read_only = SubResource("StyleBoxFlat_c3u8l")
TooltipLabel/font_sizes/font_size = 32
+Tree/styles/focus = SubResource("StyleBoxFlat_kt3n1")
diff --git a/DungeonShooting_Godot/src/framework/ui/UiNode.cs b/DungeonShooting_Godot/src/framework/ui/UiNode.cs
index 1ed8c8b..5213ae2 100644
--- a/DungeonShooting_Godot/src/framework/ui/UiNode.cs
+++ b/DungeonShooting_Godot/src/framework/ui/UiNode.cs
@@ -68,6 +68,16 @@
return (T)OpenNestedUi(uiName);
}
+ ///
+ /// 克隆当前节点, 并放到同父节点下
+ ///
+ public TCloneType CloneAndPut()
+ {
+ var inst = Clone();
+ Instance.GetParent().AddChild(inst.GetUiInstance());
+ return inst;
+ }
+
public Node GetUiInstance()
{
return Instance;
diff --git a/DungeonShooting_Godot/src/game/manager/ResourcePath.cs b/DungeonShooting_Godot/src/game/manager/ResourcePath.cs
index b8caa60..db5793c 100644
--- a/DungeonShooting_Godot/src/game/manager/ResourcePath.cs
+++ b/DungeonShooting_Godot/src/game/manager/ResourcePath.cs
@@ -183,7 +183,12 @@
public const string resource_sprite_ui_mapEditorTools_DoorDragButton_hover_png = "res://resource/sprite/ui/mapEditorTools/DoorDragButton_hover.png";
public const string resource_sprite_ui_mapEditorTools_DoorTool_png = "res://resource/sprite/ui/mapEditorTools/DoorTool.png";
public const string resource_sprite_ui_mapEditorTools_DragTool_png = "res://resource/sprite/ui/mapEditorTools/DragTool.png";
+ public const string resource_sprite_ui_mapEditorTools_Hide_png = "res://resource/sprite/ui/mapEditorTools/Hide.png";
+ public const string resource_sprite_ui_mapEditorTools_Lock_png = "res://resource/sprite/ui/mapEditorTools/Lock.png";
public const string resource_sprite_ui_mapEditorTools_PenTool_png = "res://resource/sprite/ui/mapEditorTools/PenTool.png";
+ public const string resource_sprite_ui_mapEditorTools_ToolSelect_png = "res://resource/sprite/ui/mapEditorTools/ToolSelect.png";
+ public const string resource_sprite_ui_mapEditorTools_Unlock_png = "res://resource/sprite/ui/mapEditorTools/Unlock.png";
+ public const string resource_sprite_ui_mapEditorTools_Visible_png = "res://resource/sprite/ui/mapEditorTools/Visible.png";
public const string resource_sprite_ui_roomUI_ChargeProgress_png = "res://resource/sprite/ui/roomUI/ChargeProgress.png";
public const string resource_sprite_ui_roomUI_ChargeProgressBar_png = "res://resource/sprite/ui/roomUI/ChargeProgressBar.png";
public const string resource_sprite_ui_roomUI_Cooldown_png = "res://resource/sprite/ui/roomUI/Cooldown.png";
diff --git a/DungeonShooting_Godot/src/game/ui/mapEditor/MapEditor.cs b/DungeonShooting_Godot/src/game/ui/mapEditor/MapEditor.cs
index 0cfd5e5..100a7f3 100644
--- a/DungeonShooting_Godot/src/game/ui/mapEditor/MapEditor.cs
+++ b/DungeonShooting_Godot/src/game/ui/mapEditor/MapEditor.cs
@@ -278,10 +278,142 @@
}
///
+ /// 类型: , 路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right.MarginContainer.TabContainer.MapLayer.ScrollContainer.LayerButton.VisibleButton
+ ///
+ public class VisibleButton : UiNode
+ {
+ public VisibleButton(MapEditor uiPanel, Godot.TextureButton node) : base(uiPanel, node) { }
+ public override VisibleButton Clone() => new (UiPanel, (Godot.TextureButton)Instance.Duplicate());
+ }
+
+ ///
+ /// 类型: , 路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right.MarginContainer.TabContainer.MapLayer.ScrollContainer.LayerButton
+ ///
+ public class LayerButton : UiNode
+ {
+ ///
+ /// 使用 Instance 属性获取当前节点实例对象, 节点类型: , 节点路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right.MarginContainer.TabContainer.MapLayer.ScrollContainer.VisibleButton
+ ///
+ public VisibleButton L_VisibleButton
+ {
+ get
+ {
+ if (_L_VisibleButton == null) _L_VisibleButton = new VisibleButton(UiPanel, Instance.GetNodeOrNull("VisibleButton"));
+ return _L_VisibleButton;
+ }
+ }
+ private VisibleButton _L_VisibleButton;
+
+ public LayerButton(MapEditor uiPanel, Godot.Button node) : base(uiPanel, node) { }
+ public override LayerButton Clone() => new (UiPanel, (Godot.Button)Instance.Duplicate());
+ }
+
+ ///
+ /// 类型: , 路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right.MarginContainer.TabContainer.MapLayer.ScrollContainer
+ ///
+ public class ScrollContainer : UiNode
+ {
+ ///
+ /// 使用 Instance 属性获取当前节点实例对象, 节点类型: , 节点路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right.MarginContainer.TabContainer.MapLayer.LayerButton
+ ///
+ public LayerButton L_LayerButton
+ {
+ get
+ {
+ if (_L_LayerButton == null) _L_LayerButton = new LayerButton(UiPanel, Instance.GetNodeOrNull("LayerButton"));
+ return _L_LayerButton;
+ }
+ }
+ private LayerButton _L_LayerButton;
+
+ public ScrollContainer(MapEditor uiPanel, Godot.ScrollContainer node) : base(uiPanel, node) { }
+ public override ScrollContainer Clone() => new (UiPanel, (Godot.ScrollContainer)Instance.Duplicate());
+ }
+
+ ///
+ /// 类型: , 路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right.MarginContainer.TabContainer.MapLayer
+ ///
+ public class MapLayer : UiNode
+ {
+ ///
+ /// 使用 Instance 属性获取当前节点实例对象, 节点类型: , 节点路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right.MarginContainer.TabContainer.ScrollContainer
+ ///
+ public ScrollContainer L_ScrollContainer
+ {
+ get
+ {
+ if (_L_ScrollContainer == null) _L_ScrollContainer = new ScrollContainer(UiPanel, Instance.GetNodeOrNull("ScrollContainer"));
+ return _L_ScrollContainer;
+ }
+ }
+ private ScrollContainer _L_ScrollContainer;
+
+ public MapLayer(MapEditor uiPanel, Godot.MarginContainer node) : base(uiPanel, node) { }
+ public override MapLayer Clone() => new (UiPanel, (Godot.MarginContainer)Instance.Duplicate());
+ }
+
+ ///
+ /// 类型: , 路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right.MarginContainer.TabContainer
+ ///
+ public class TabContainer : UiNode
+ {
+ ///
+ /// 使用 Instance 属性获取当前节点实例对象, 节点类型: , 节点路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right.MarginContainer.MapLayer
+ ///
+ public MapLayer L_MapLayer
+ {
+ get
+ {
+ if (_L_MapLayer == null) _L_MapLayer = new MapLayer(UiPanel, Instance.GetNodeOrNull("MapLayer"));
+ return _L_MapLayer;
+ }
+ }
+ private MapLayer _L_MapLayer;
+
+ public TabContainer(MapEditor uiPanel, Godot.TabContainer node) : base(uiPanel, node) { }
+ public override TabContainer Clone() => new (UiPanel, (Godot.TabContainer)Instance.Duplicate());
+ }
+
+ ///
+ /// 类型: , 路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right.MarginContainer
+ ///
+ public class MarginContainer_1 : UiNode
+ {
+ ///
+ /// 使用 Instance 属性获取当前节点实例对象, 节点类型: , 节点路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right.TabContainer
+ ///
+ public TabContainer L_TabContainer
+ {
+ get
+ {
+ if (_L_TabContainer == null) _L_TabContainer = new TabContainer(UiPanel, Instance.GetNodeOrNull("TabContainer"));
+ return _L_TabContainer;
+ }
+ }
+ private TabContainer _L_TabContainer;
+
+ public MarginContainer_1(MapEditor uiPanel, Godot.MarginContainer node) : base(uiPanel, node) { }
+ public override MarginContainer_1 Clone() => new (UiPanel, (Godot.MarginContainer)Instance.Duplicate());
+ }
+
+ ///
/// 类型: , 路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right
///
public class Right : UiNode
{
+ ///
+ /// 使用 Instance 属性获取当前节点实例对象, 节点类型: , 节点路径: MapEditor.Bg.VBoxContainer.HSplitContainer.MarginContainer
+ ///
+ public MarginContainer_1 L_MarginContainer
+ {
+ get
+ {
+ if (_L_MarginContainer == null) _L_MarginContainer = new MarginContainer_1(UiPanel, Instance.GetNodeOrNull("MarginContainer"));
+ return _L_MarginContainer;
+ }
+ }
+ private MarginContainer_1 _L_MarginContainer;
+
public Right(MapEditor uiPanel, Godot.Panel node) : base(uiPanel, node) { }
public override Right Clone() => new (UiPanel, (Godot.Panel)Instance.Duplicate());
}
@@ -430,16 +562,36 @@
public MapView S_MapView => L_Bg.L_VBoxContainer.L_HSplitContainer.L_Left.L_MarginContainer.L_MapView;
///
- /// 场景中唯一名称的节点, 节点类型: , 节点路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Left.MarginContainer
- ///
- public MarginContainer S_MarginContainer => L_Bg.L_VBoxContainer.L_HSplitContainer.L_Left.L_MarginContainer;
-
- ///
/// 场景中唯一名称的节点, 节点类型: , 节点路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Left
///
public Left S_Left => L_Bg.L_VBoxContainer.L_HSplitContainer.L_Left;
///
+ /// 场景中唯一名称的节点, 节点类型: , 节点路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right.MarginContainer.TabContainer.MapLayer.ScrollContainer.LayerButton.VisibleButton
+ ///
+ public VisibleButton S_VisibleButton => L_Bg.L_VBoxContainer.L_HSplitContainer.L_Right.L_MarginContainer.L_TabContainer.L_MapLayer.L_ScrollContainer.L_LayerButton.L_VisibleButton;
+
+ ///
+ /// 场景中唯一名称的节点, 节点类型: , 节点路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right.MarginContainer.TabContainer.MapLayer.ScrollContainer.LayerButton
+ ///
+ public LayerButton S_LayerButton => L_Bg.L_VBoxContainer.L_HSplitContainer.L_Right.L_MarginContainer.L_TabContainer.L_MapLayer.L_ScrollContainer.L_LayerButton;
+
+ ///
+ /// 场景中唯一名称的节点, 节点类型: , 节点路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right.MarginContainer.TabContainer.MapLayer.ScrollContainer
+ ///
+ public ScrollContainer S_ScrollContainer => L_Bg.L_VBoxContainer.L_HSplitContainer.L_Right.L_MarginContainer.L_TabContainer.L_MapLayer.L_ScrollContainer;
+
+ ///
+ /// 场景中唯一名称的节点, 节点类型: , 节点路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right.MarginContainer.TabContainer.MapLayer
+ ///
+ public MapLayer S_MapLayer => L_Bg.L_VBoxContainer.L_HSplitContainer.L_Right.L_MarginContainer.L_TabContainer.L_MapLayer;
+
+ ///
+ /// 场景中唯一名称的节点, 节点类型: , 节点路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right.MarginContainer.TabContainer
+ ///
+ public TabContainer S_TabContainer => L_Bg.L_VBoxContainer.L_HSplitContainer.L_Right.L_MarginContainer.L_TabContainer;
+
+ ///
/// 场景中唯一名称的节点, 节点类型: , 节点路径: MapEditor.Bg.VBoxContainer.HSplitContainer.Right
///
public Right S_Right => L_Bg.L_VBoxContainer.L_HSplitContainer.L_Right;
diff --git a/DungeonShooting_Godot/src/game/ui/mapEditor/MapEditorPanel.cs b/DungeonShooting_Godot/src/game/ui/mapEditor/MapEditorPanel.cs
index b886c99..c380fbd 100644
--- a/DungeonShooting_Godot/src/game/ui/mapEditor/MapEditorPanel.cs
+++ b/DungeonShooting_Godot/src/game/ui/mapEditor/MapEditorPanel.cs
@@ -6,10 +6,15 @@
public partial class MapEditorPanel : MapEditor
{
private EditorTileMapBar _editorTileMapBar;
+ private EditorLayerBar _editorLayerBar;
public override void OnCreateUi()
{
+ S_TabContainer.Instance.SetTabTitle(0, "地图");
+ //S_MapLayer.Instance.Init(S_MapLayer);
+
_editorTileMapBar = new EditorTileMapBar(this, S_TileMap);
+ _editorLayerBar = new EditorLayerBar(this, S_MapLayer);
}
public override void OnShowUi()
@@ -19,6 +24,7 @@
OnMapViewResized();
_editorTileMapBar.OnShow();
+ _editorLayerBar.OnShow();
}
public override void OnHideUi()
@@ -26,11 +32,19 @@
S_Left.Instance.Resized -= OnMapViewResized;
S_Back.Instance.Pressed -= OnBackClick;
_editorTileMapBar.OnHide();
+ _editorLayerBar.OnHide();
}
+ public override void OnDestroyUi()
+ {
+ _editorTileMapBar.OnDestroy();
+ _editorLayerBar.OnDestroy();
+ }
+
public override void Process(float delta)
{
_editorTileMapBar.Process(delta);
+ _editorLayerBar.Process(delta);
}
///
diff --git a/DungeonShooting_Godot/src/game/ui/mapEditor/TileView/EditorTileMapBar.cs b/DungeonShooting_Godot/src/game/ui/mapEditor/TileView/EditorTileMapBar.cs
index bc50836..eb9b839 100644
--- a/DungeonShooting_Godot/src/game/ui/mapEditor/TileView/EditorTileMapBar.cs
+++ b/DungeonShooting_Godot/src/game/ui/mapEditor/TileView/EditorTileMapBar.cs
@@ -4,14 +4,12 @@
public class EditorTileMapBar
{
- private MapEditorPanel _editorPanel;
private MapEditor.TileMap _editorTileMap;
private EventFactory _eventFactory;
public EditorTileMapBar(MapEditorPanel editorPanel, MapEditor.TileMap editorTileMap)
{
_editorTileMap = editorTileMap;
- _editorPanel = editorPanel;
_editorTileMap.Instance.MapEditorPanel = editorPanel;
_editorTileMap.Instance.MapEditorToolsPanel = editorPanel.S_MapEditorTools.Instance;
_editorTileMap.Instance.MapEditorToolsPanel.EditorMap = _editorTileMap;
@@ -44,4 +42,9 @@
{
_editorTileMap.Instance.DrawGuides(_editorTileMap.L_Brush.Instance);
}
+
+ public void OnDestroy()
+ {
+
+ }
}
\ No newline at end of file
diff --git a/DungeonShooting_Godot/src/game/ui/mapEditor/tabView/layerTab/EditorLayerBar.cs b/DungeonShooting_Godot/src/game/ui/mapEditor/tabView/layerTab/EditorLayerBar.cs
new file mode 100644
index 0000000..a04dd28
--- /dev/null
+++ b/DungeonShooting_Godot/src/game/ui/mapEditor/tabView/layerTab/EditorLayerBar.cs
@@ -0,0 +1,69 @@
+using Godot;
+
+namespace UI.MapEditor;
+
+public class EditorLayerBar
+{
+ public class LayerButtonData
+ {
+ ///
+ /// 显示文本
+ ///
+ public string Title;
+ ///
+ /// 是否锁定
+ ///
+ public bool IsLock;
+ ///
+ /// Map层级
+ ///
+ public int Layer;
+
+ public LayerButtonData(string title, bool isLock, int layer)
+ {
+ Title = title;
+ IsLock = isLock;
+ Layer = layer;
+ }
+ }
+
+ private readonly MapEditorPanel _mapEditorPanel;
+ private readonly MapEditor.MapLayer _mapLayer;
+ private readonly UiGrid _grid;
+
+ public EditorLayerBar(MapEditorPanel mapEditorPanel, MapEditor.MapLayer mapLayer)
+ {
+ _mapEditorPanel = mapEditorPanel;
+ _mapLayer = mapLayer;
+ _grid = new UiGrid(mapLayer.L_ScrollContainer.L_LayerButton, typeof(LayerButtonCell));
+ _grid.SetCellOffset(new Vector2I(0, 2));
+ _grid.SetHorizontalExpand(true);
+
+ _grid.Add(new LayerButtonData("地面", false, EditorTileMap.AutoFloorLayer));
+ _grid.Add(new LayerButtonData("自定义底层", false, EditorTileMap.CustomFloorLayer));
+ _grid.Add(new LayerButtonData("中层自动图块", true, EditorTileMap.AutoMiddleLayer));
+ _grid.Add(new LayerButtonData("自定义中层", false, EditorTileMap.CustomMiddleLayer));
+ _grid.Add(new LayerButtonData("高层自动图块", true, EditorTileMap.AutoTopLayer));
+ _grid.Add(new LayerButtonData("自定义高层", false, EditorTileMap.CustomTopLayer));
+ }
+
+ public void OnShow()
+ {
+
+ }
+
+ public void OnHide()
+ {
+
+ }
+
+ public void OnDestroy()
+ {
+ _grid.Destroy();
+ }
+
+ public void Process(float delta)
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/DungeonShooting_Godot/src/game/ui/mapEditor/tabView/layerTab/LayerButtonCell.cs b/DungeonShooting_Godot/src/game/ui/mapEditor/tabView/layerTab/LayerButtonCell.cs
new file mode 100644
index 0000000..60a740f
--- /dev/null
+++ b/DungeonShooting_Godot/src/game/ui/mapEditor/tabView/layerTab/LayerButtonCell.cs
@@ -0,0 +1,48 @@
+namespace UI.MapEditor;
+
+public class LayerButtonCell : UiCell
+{
+ private bool _visible;
+
+ public override void OnInit()
+ {
+ CellNode.L_VisibleButton.Instance.Pressed += OnVisibleButtonClick;
+ }
+
+ public override void OnSetData(EditorLayerBar.LayerButtonData data)
+ {
+ if (data.IsLock)
+ {
+ CellNode.Instance.Icon = ResourceManager.LoadTexture2D(ResourcePath.resource_sprite_ui_mapEditorTools_Lock_png);
+ }
+ else
+ {
+ CellNode.Instance.Icon = ResourceManager.LoadTexture2D(ResourcePath.resource_sprite_ui_mapEditorTools_Unlock_png);
+ }
+
+ CellNode.Instance.Text = data.Title;
+ var panel = (MapEditorPanel)CellNode.UiPanel;
+ _visible = panel.S_TileMap.Instance.IsLayerEnabled(data.Layer);
+ SetVisibleIcon(_visible);
+ }
+
+ private void OnVisibleButtonClick()
+ {
+ var panel = (MapEditorPanel)CellNode.UiPanel;
+ _visible = !_visible;
+ panel.S_TileMap.Instance.SetLayerEnabled(Data.Layer, _visible);
+ SetVisibleIcon(_visible);
+ }
+
+ private void SetVisibleIcon(bool visible)
+ {
+ if (visible)
+ {
+ CellNode.L_VisibleButton.Instance.TextureNormal = ResourceManager.LoadTexture2D(ResourcePath.resource_sprite_ui_mapEditorTools_Visible_png);
+ }
+ else
+ {
+ CellNode.L_VisibleButton.Instance.TextureNormal = ResourceManager.LoadTexture2D(ResourcePath.resource_sprite_ui_mapEditorTools_Hide_png);
+ }
+ }
+}
\ No newline at end of file
diff --git a/DungeonShooting_Godot/src/game/ui/mapEditor/tileView/EditorTileMap.cs b/DungeonShooting_Godot/src/game/ui/mapEditor/tileView/EditorTileMap.cs
new file mode 100644
index 0000000..eea44cd
--- /dev/null
+++ b/DungeonShooting_Godot/src/game/ui/mapEditor/tileView/EditorTileMap.cs
@@ -0,0 +1,925 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json;
+using Godot;
+using Godot.Collections;
+using UI.MapEditorTools;
+
+namespace UI.MapEditor;
+
+public partial class EditorTileMap : TileMap
+{
+
+ public enum MouseButtonType
+ {
+ ///
+ /// 无状态
+ ///
+ None,
+ ///
+ /// 拖拽模式
+ ///
+ Drag,
+ ///
+ /// 笔
+ ///
+ Pen,
+ ///
+ /// 绘制区域模式
+ ///
+ Area,
+ ///
+ /// 编辑门区域模式
+ ///
+ Door,
+ }
+
+ ///
+ /// 自动图块地板层
+ ///
+ public const int AutoFloorLayer = 0;
+ ///
+ /// 自定义图块地板层
+ ///
+ public const int CustomFloorLayer = 1;
+ ///
+ /// 自动图块中间层
+ ///
+ public const int AutoMiddleLayer = 2;
+ ///
+ /// 自定义图块中间层
+ ///
+ public const int CustomMiddleLayer = 3;
+ ///
+ /// 自动图块顶层
+ ///
+ public const int AutoTopLayer = 4;
+ ///
+ /// 自定义图块顶层
+ ///
+ public const int CustomTopLayer = 5;
+
+ ///
+ /// 所属地图编辑器UI
+ ///
+ public MapEditorPanel MapEditorPanel { get; set; }
+
+ ///
+ /// 编辑器工具UI
+ ///
+ public MapEditorToolsPanel MapEditorToolsPanel { get; set; }
+
+ ///
+ /// 左键功能
+ ///
+ public MouseButtonType MouseType { get; set; } = MouseButtonType.Pen;
+
+ //鼠标坐标
+ private Vector2 _mousePosition;
+ //鼠标所在的cell坐标
+ private Vector2I _mouseCellPosition;
+ //上一帧鼠标所在的cell坐标
+ private Vector2I _prevMouseCellPosition = new Vector2I(-99999, -99999);
+ //单次绘制是否改变过tile数据
+ private bool _changeFlag = false;
+ //左键开始按下时鼠标所在的坐标
+ private Vector2I _mouseStartCellPosition;
+ //鼠标中建是否按下
+ private bool _isMiddlePressed = false;
+ private Vector2 _moveOffset;
+ //左键是否按下
+ private bool _isLeftPressed = false;
+ //右键是否按下
+ private bool _isRightPressed = false;
+ //绘制填充区域
+ private bool _drawFullRect = false;
+ //负责存储自动图块数据
+ private Grid _autoCellLayerGrid = new Grid();
+ //用于生成导航网格
+ private DungeonTileMap _dungeonTileMap;
+ //停止绘制多久后开始执行生成操作
+ private float _generateInterval = 3f;
+ //生成自动图块和导航网格的计时器
+ private float _generateTimer = -1;
+ //检测地形结果
+ private bool _checkTerrainFlag = true;
+ //错误地形位置
+ private Vector2I _checkTerrainErrorPosition = Vector2I.Zero;
+ //是否执行生成地形成功
+ private bool _isGenerateTerrain = false;
+ private bool _initLayer = false;
+
+ //--------- 配置数据 -------------
+ private int _sourceId = 0;
+ private int _terrainSet = 0;
+ private int _terrain = 0;
+ private AutoTileConfig _autoTileConfig = new AutoTileConfig();
+
+ //原数据
+ private DungeonRoomSplit _roomSplit;
+
+ //变动过的数据
+
+ //地图位置, 单位: 格
+ private Vector2I _roomPosition;
+ //地图大小, 单位: 格
+ private Vector2I _roomSize;
+ private List _doorConfigs = new List();
+ //-------------------------------
+
+ public override void _Ready()
+ {
+ InitLayer();
+ }
+
+ public override void _Process(double delta)
+ {
+ var newDelta = (float)delta;
+ _drawFullRect = false;
+ var position = GetLocalMousePosition();
+ _mouseCellPosition = LocalToMap(position);
+ _mousePosition = new Vector2(
+ _mouseCellPosition.X * GameConfig.TileCellSize,
+ _mouseCellPosition.Y * GameConfig.TileCellSize
+ );
+
+ if (!MapEditorToolsPanel.S_HBoxContainer.Instance.IsPositionOver(GetGlobalMousePosition())) //不在Ui节点上
+ {
+ //左键绘制
+ if (_isLeftPressed)
+ {
+ if (MouseType == MouseButtonType.Pen) //绘制单格
+ {
+ if (_prevMouseCellPosition != _mouseCellPosition || !_changeFlag) //鼠标位置变过
+ {
+ _changeFlag = true;
+ _prevMouseCellPosition = _mouseCellPosition;
+ //绘制自动图块
+ SetSingleAutoCell(_mouseCellPosition);
+ }
+ }
+ else if (MouseType == MouseButtonType.Area) //绘制区域
+ {
+ _drawFullRect = true;
+ }
+ else if (MouseType == MouseButtonType.Drag) //拖拽
+ {
+ SetMapPosition(GetGlobalMousePosition() + _moveOffset);
+ }
+ }
+ else if (_isRightPressed) //右键擦除
+ {
+ if (MouseType == MouseButtonType.Pen) //绘制单格
+ {
+ if (_prevMouseCellPosition != _mouseCellPosition || !_changeFlag) //鼠标位置变过
+ {
+ _changeFlag = true;
+ _prevMouseCellPosition = _mouseCellPosition;
+ EraseSingleAutoCell(_mouseCellPosition);
+ }
+ }
+ else if (MouseType == MouseButtonType.Area) //绘制区域
+ {
+ _drawFullRect = true;
+ }
+ else if (MouseType == MouseButtonType.Drag) //拖拽
+ {
+ SetMapPosition(GetGlobalMousePosition() + _moveOffset);
+ }
+ }
+ else if (_isMiddlePressed) //中键移动
+ {
+ SetMapPosition(GetGlobalMousePosition() + _moveOffset);
+ }
+ }
+
+ //绘制停止指定时间后, 生成导航网格
+ if (_generateTimer > 0)
+ {
+ _generateTimer -= newDelta;
+ if (_generateTimer <= 0)
+ {
+ //计算区域
+ CalcTileRect(false);
+ GD.Print("开始检测是否可以生成地形...");
+ if (CheckTerrain())
+ {
+ GD.Print("开始绘制导航网格...");
+ if (GenerateNavigation())
+ {
+ GD.Print("开始绘制自动贴图...");
+ GenerateTerrain();
+ _isGenerateTerrain = true;
+ }
+ }
+ else
+ {
+ SetErrorCell(_checkTerrainErrorPosition);
+ }
+ }
+ }
+ }
+
+ ///
+ /// 绘制辅助线
+ ///
+ public void DrawGuides(CanvasItem canvasItem)
+ {
+ //轴线
+ canvasItem.DrawLine(new Vector2(0, 2000), new Vector2(0, -2000), Colors.Green);
+ canvasItem.DrawLine(new Vector2(2000, 0), new Vector2( -2000, 0), Colors.Red);
+
+ //绘制房间区域
+ if (_roomSize.X != 0 && _roomSize.Y != 0)
+ {
+ var size = TileSet.TileSize;
+ canvasItem.DrawRect(new Rect2(_roomPosition * size, _roomSize * size),
+ Colors.Aqua, false, 5f / Scale.X);
+ }
+
+ if (_checkTerrainFlag) //已经通过地形检测
+ {
+ //绘制导航网格
+ var result = _dungeonTileMap.GetGenerateNavigationResult();
+ if (result != null && result.Success)
+ {
+ var polygonData = _dungeonTileMap.GetPolygonData();
+ Utils.DrawNavigationPolygon(canvasItem, polygonData, 3f / Scale.X);
+ }
+ }
+
+ if (MouseType == MouseButtonType.Pen || MouseType == MouseButtonType.Area)
+ {
+ if (_drawFullRect) //绘制填充矩形
+ {
+ var size = TileSet.TileSize;
+ var cellPos = _mouseStartCellPosition;
+ var temp = size;
+ if (_mouseStartCellPosition.X > _mouseCellPosition.X)
+ {
+ cellPos.X += 1;
+ temp.X -= size.X;
+ }
+ if (_mouseStartCellPosition.Y > _mouseCellPosition.Y)
+ {
+ cellPos.Y += 1;
+ temp.Y -= size.Y;
+ }
+
+ var pos = cellPos * size;
+ canvasItem.DrawRect(new Rect2(pos, _mousePosition - pos + temp), Colors.White, false, 2f / Scale.X);
+ }
+ else //绘制单格
+ {
+ canvasItem.DrawRect(new Rect2(_mousePosition, TileSet.TileSize), Colors.White, false, 2f / Scale.X);
+ }
+ }
+ }
+
+ public override void _Input(InputEvent @event)
+ {
+ if (@event is InputEventMouseButton mouseButton)
+ {
+ if (mouseButton.ButtonIndex == MouseButton.Left) //左键
+ {
+ if (mouseButton.Pressed) //按下
+ {
+ _moveOffset = Position - GetGlobalMousePosition();
+ _mouseStartCellPosition = LocalToMap(GetLocalMousePosition());
+ }
+ else
+ {
+ _changeFlag = false;
+ if (_drawFullRect) //松开, 提交绘制的矩形区域
+ {
+ SetRectAutoCell(_mouseStartCellPosition, _mouseCellPosition);
+ _drawFullRect = false;
+ }
+ }
+
+ _isLeftPressed = mouseButton.Pressed;
+ }
+ else if (mouseButton.ButtonIndex == MouseButton.Right) //右键
+ {
+ if (mouseButton.Pressed) //按下
+ {
+ _moveOffset = Position - GetGlobalMousePosition();
+ _mouseStartCellPosition = LocalToMap(GetLocalMousePosition());
+ }
+ else
+ {
+ _changeFlag = false;
+ if (_drawFullRect) //松开, 提交擦除的矩形区域
+ {
+ EraseRectAutoCell(_mouseStartCellPosition, _mouseCellPosition);
+ _drawFullRect = false;
+ }
+ }
+
+ _isRightPressed = mouseButton.Pressed;
+ }
+ else if (mouseButton.ButtonIndex == MouseButton.WheelDown)
+ {
+ //缩小
+ Shrink();
+ }
+ else if (mouseButton.ButtonIndex == MouseButton.WheelUp)
+ {
+ //放大
+ Magnify();
+ }
+ else if (mouseButton.ButtonIndex == MouseButton.Middle)
+ {
+ _isMiddlePressed = mouseButton.Pressed;
+ if (_isMiddlePressed)
+ {
+ _moveOffset = Position - GetGlobalMousePosition();
+ }
+ }
+ }
+ else if (@event is InputEventKey eventKey)
+ {
+ if (eventKey.Pressed && eventKey.Keycode == Key.M)
+ {
+ GD.Print("保存地牢房间数据...");
+ TriggerSave();
+ }
+ }
+ }
+
+ //将指定层数据存入list中
+ private void PushLayerDataToList(int layer, int sourceId, List list)
+ {
+ var layerArray = GetUsedCellsById(layer, sourceId);
+ foreach (var pos in layerArray)
+ {
+ var atlasCoords = GetCellAtlasCoords(layer, pos);
+ list.Add(pos.X);
+ list.Add(pos.Y);
+ list.Add(_sourceId);
+ list.Add(atlasCoords.X);
+ list.Add(atlasCoords.Y);
+ }
+ }
+
+ private void SetLayerDataFromList(int layer, List list)
+ {
+ for (var i = 0; i < list.Count; i += 5)
+ {
+ var pos = new Vector2I(list[i], list[i + 1]);
+ var sourceId = list[i + 2];
+ var atlasCoords = new Vector2I(list[i + 3], list[i + 4]);
+ SetCell(layer, pos, sourceId, atlasCoords);
+ if (layer == AutoFloorLayer)
+ {
+ _autoCellLayerGrid.Set(pos, true);
+ }
+ }
+ }
+
+ //保存地牢
+ private void TriggerSave()
+ {
+ SaveRoomInfoConfig();
+ SaveTileInfoConfig();
+ }
+
+ ///
+ /// 加载地牢, 返回是否加载成功
+ ///
+ public bool Load(DungeonRoomSplit roomSplit)
+ {
+ _roomSplit = roomSplit;
+ var roomInfo = roomSplit.RoomInfo;
+ var tileInfo = roomSplit.TileInfo;
+
+ _roomPosition = roomInfo.Position.AsVector2I();
+ SetMapSize(roomInfo.Size.AsVector2I(), true);
+ _doorConfigs.Clear();
+ foreach (var doorAreaInfo in roomInfo.DoorAreaInfos)
+ {
+ _doorConfigs.Add(doorAreaInfo.Clone());
+ }
+
+ //初始化层级数据
+ InitLayer();
+
+ //地块数据
+ SetLayerDataFromList(AutoFloorLayer, tileInfo.Floor);
+ SetLayerDataFromList(AutoMiddleLayer, tileInfo.Middle);
+ SetLayerDataFromList(AutoTopLayer, tileInfo.Top);
+
+ //导航网格数据
+ _dungeonTileMap.SetPolygonData(tileInfo.NavigationList);
+
+ //聚焦
+ //MapEditorPanel.CallDelay(0.1f, OnClickCenterTool);
+ //CallDeferred(nameof(OnClickCenterTool), null);
+
+ //加载门编辑区域
+ foreach (var doorAreaInfo in _doorConfigs)
+ {
+ MapEditorToolsPanel.CreateDoorTool(doorAreaInfo);
+ }
+ return true;
+ }
+
+ private void InitLayer()
+ {
+ if (_initLayer)
+ {
+ return;
+ }
+
+ _initLayer = true;
+ //初始化层级数据
+ AddLayer(CustomFloorLayer);
+ SetLayerZIndex(CustomFloorLayer, CustomFloorLayer);
+ AddLayer(AutoMiddleLayer);
+ SetLayerZIndex(AutoMiddleLayer, AutoMiddleLayer);
+ AddLayer(CustomMiddleLayer);
+ SetLayerZIndex(CustomMiddleLayer, CustomMiddleLayer);
+ AddLayer(AutoTopLayer);
+ SetLayerZIndex(AutoTopLayer, AutoTopLayer);
+ AddLayer(CustomTopLayer);
+ SetLayerZIndex(CustomTopLayer, CustomTopLayer);
+
+ _dungeonTileMap = new DungeonTileMap(this);
+ _dungeonTileMap.SetFloorAtlasCoords(new List(new []{ _autoTileConfig.Floor.AutoTileCoord }));
+ }
+
+ //缩小
+ private void Shrink()
+ {
+ var pos = GetLocalMousePosition();
+ var scale = Scale / 1.1f;
+ if (scale.LengthSquared() >= 0.5f)
+ {
+ Scale = scale;
+ SetMapPosition(Position + pos * 0.1f * scale);
+ }
+ else
+ {
+ GD.Print("太小了");
+ }
+ }
+ //放大
+ private void Magnify()
+ {
+ var pos = GetLocalMousePosition();
+ var prevScale = Scale;
+ var scale = prevScale * 1.1f;
+ if (scale.LengthSquared() <= 2000)
+ {
+ Scale = scale;
+ SetMapPosition(Position - pos * 0.1f * prevScale);
+ }
+ else
+ {
+ GD.Print("太大了");
+ }
+ }
+
+ //绘制单个自动贴图
+ private void SetSingleAutoCell(Vector2I position)
+ {
+ SetCell(GetFloorLayer(), position, _sourceId, _autoTileConfig.Floor.AutoTileCoord);
+ if (!_autoCellLayerGrid.Contains(position.X, position.Y))
+ {
+ ResetGenerateTimer();
+ _autoCellLayerGrid.Set(position.X, position.Y, true);
+ }
+ }
+
+ //绘制区域自动贴图
+ private void SetRectAutoCell(Vector2I start, Vector2I end)
+ {
+ ResetGenerateTimer();
+
+ if (start.X > end.X)
+ {
+ var temp = end.X;
+ end.X = start.X;
+ start.X = temp;
+ }
+ if (start.Y > end.Y)
+ {
+ var temp = end.Y;
+ end.Y = start.Y;
+ start.Y = temp;
+ }
+
+ var width = end.X - start.X + 1;
+ var height = end.Y - start.Y + 1;
+ for (var i = 0; i < width; i++)
+ {
+ for (var j = 0; j < height; j++)
+ {
+ SetCell(GetFloorLayer(), new Vector2I(start.X + i, start.Y + j), _sourceId, _autoTileConfig.Floor.AutoTileCoord);
+ }
+ }
+
+ _autoCellLayerGrid.SetRect(start, new Vector2I(width, height), true);
+ }
+
+ //擦除单个自动图块
+ private void EraseSingleAutoCell(Vector2I position)
+ {
+ EraseCell(GetFloorLayer(), position);
+ if (_autoCellLayerGrid.Remove(position.X, position.Y))
+ {
+ ResetGenerateTimer();
+ }
+ }
+
+ //擦除一个区域内的自动贴图
+ private void EraseRectAutoCell(Vector2I start, Vector2I end)
+ {
+ ResetGenerateTimer();
+
+ if (start.X > end.X)
+ {
+ var temp = end.X;
+ end.X = start.X;
+ start.X = temp;
+ }
+ if (start.Y > end.Y)
+ {
+ var temp = end.Y;
+ end.Y = start.Y;
+ start.Y = temp;
+ }
+
+ var width = end.X - start.X + 1;
+ var height = end.Y - start.Y + 1;
+ for (var i = 0; i < width; i++)
+ {
+ for (var j = 0; j < height; j++)
+ {
+ EraseCell(GetFloorLayer(), new Vector2I(start.X + i, start.Y + j));
+ }
+ }
+ _autoCellLayerGrid.RemoveRect(start, new Vector2I(width, height));
+ }
+
+ //重置计时器
+ private void ResetGenerateTimer()
+ {
+ _generateTimer = _generateInterval;
+ _isGenerateTerrain = false;
+ _dungeonTileMap.ClearPolygonData();
+ ClearLayer(AutoTopLayer);
+ ClearLayer(AutoMiddleLayer);
+ }
+
+ //重新计算房间区域
+ private void CalcTileRect(bool refreshDoorTrans)
+ {
+ var rect = GetUsedRect();
+ _roomPosition = rect.Position;
+ SetMapSize(rect.Size, refreshDoorTrans);
+ }
+
+ //检测是否有不合规的图块, 返回true表示图块正常
+ private bool CheckTerrain()
+ {
+ var x = _roomPosition.X;
+ var y = _roomPosition.Y;
+ var w = _roomSize.X;
+ var h = _roomSize.Y;
+
+ for (var i = 0; i < w; i++)
+ {
+ for (var j = 0; j < h; j++)
+ {
+ var pos = new Vector2I(x + i, y + j);
+ if (GetCellSourceId(AutoFloorLayer, pos) == -1)
+ {
+ //先检测对边是否有地板
+ if ((_autoCellLayerGrid.Get(pos.X - 1, pos.Y) && _autoCellLayerGrid.Get(pos.X + 1, pos.Y)) //left & right
+ || (_autoCellLayerGrid.Get(pos.X, pos.Y + 1) && _autoCellLayerGrid.Get(pos.X, pos.Y - 1))) //top & down
+ {
+ _checkTerrainFlag = false;
+ _checkTerrainErrorPosition = pos;
+ return false;
+ }
+
+ //再检测对角是否有地板
+ var topLeft = _autoCellLayerGrid.Get(pos.X - 1, pos.Y + 1); //top-left
+ var downRight = _autoCellLayerGrid.Get(pos.X + 1, pos.Y - 1); //down-right
+ var downLeft = _autoCellLayerGrid.Get(pos.X - 1, pos.Y - 1); //down-left
+ var topRight = _autoCellLayerGrid.Get(pos.X + 1, pos.Y + 1); //top-right
+ if ((topLeft && downRight && !downLeft && !topRight) || (!topLeft && !downRight && downLeft && topRight))
+ {
+ _checkTerrainFlag = false;
+ _checkTerrainErrorPosition = pos;
+ return false;
+ }
+ }
+ }
+ }
+
+ _checkTerrainFlag = true;
+ return true;
+ }
+
+ //生成自动图块 (地形)
+ private void GenerateTerrain()
+ {
+ ClearLayer(AutoFloorLayer);
+
+ var list = new List();
+ _autoCellLayerGrid.ForEach((x, y, data) =>
+ {
+ if (data)
+ {
+ list.Add(new Vector2I(x, y));
+ }
+ });
+ var arr = new Array(list);
+ //绘制自动图块
+ SetCellsTerrainConnect(AutoFloorLayer, arr, _terrainSet, _terrain, false);
+ //计算区域
+ CalcTileRect(true);
+ //将墙壁移动到指定层
+ MoveTerrainCell();
+ }
+
+ //将自动生成的图块从 AutoFloorLayer 移动到指定图层中
+ private void MoveTerrainCell()
+ {
+ ClearLayer(AutoTopLayer);
+ ClearLayer(AutoMiddleLayer);
+
+ var x = _roomPosition.X;
+ var y = _roomPosition.Y;
+ var w = _roomSize.X;
+ var h = _roomSize.Y;
+
+ for (var i = 0; i < w; i++)
+ {
+ for (var j = 0; j < h; j++)
+ {
+ var pos = new Vector2I(x + i, y + j);
+ if (!_autoCellLayerGrid.Contains(pos) && GetCellSourceId(AutoFloorLayer, pos) != -1)
+ {
+ var atlasCoords = GetCellAtlasCoords(AutoFloorLayer, pos);
+ var layer = _autoTileConfig.GetLayer(atlasCoords);
+ if (layer == GameConfig.MiddleMapLayer)
+ {
+ layer = AutoMiddleLayer;
+ }
+ else if (layer == GameConfig.TopMapLayer)
+ {
+ layer = AutoTopLayer;
+ }
+ else
+ {
+ GD.PrintErr($"异常图块: {pos}, 这个图块的图集坐标'{atlasCoords}'不属于'MiddleMapLayer'和'TopMapLayer'!");
+ continue;
+ }
+ EraseCell(AutoFloorLayer, pos);
+ SetCell(layer, pos, _sourceId, atlasCoords);
+ }
+ }
+ }
+ }
+
+ //生成导航网格
+ private bool GenerateNavigation()
+ {
+ _dungeonTileMap.GenerateNavigationPolygon(AutoFloorLayer);
+ var result = _dungeonTileMap.GetGenerateNavigationResult();
+ if (result.Success)
+ {
+ CloseErrorCell();
+ }
+ else
+ {
+ SetErrorCell(result.Exception.Point);
+ }
+
+ return result.Success;
+ }
+
+ //设置显示的错误cell, 会标记上红色的闪烁动画
+ private void SetErrorCell(Vector2I pos)
+ {
+ MapEditorPanel.S_ErrorCell.Instance.Position = pos * CellQuadrantSize;
+ MapEditorPanel.S_ErrorCellAnimationPlayer.Instance.Play(AnimatorNames.Show);
+ }
+
+ //关闭显示的错误cell
+ private void CloseErrorCell()
+ {
+ MapEditorPanel.S_ErrorCellAnimationPlayer.Instance.Stop();
+ }
+
+ private int GetFloorLayer()
+ {
+ return AutoFloorLayer;
+ }
+
+ private int GetMiddleLayer()
+ {
+ return AutoMiddleLayer;
+ }
+
+ private int GetTopLayer()
+ {
+ return AutoTopLayer;
+ }
+
+ ///
+ /// 选中拖拽功能
+ ///
+ public void OnSelectHandTool(object arg)
+ {
+ MouseType = MouseButtonType.Drag;
+ }
+
+ ///
+ /// 选中画笔攻击
+ ///
+ public void OnSelectPenTool(object arg)
+ {
+ MouseType = MouseButtonType.Pen;
+ }
+
+ ///
+ /// 选中绘制区域功能
+ ///
+ public void OnSelectRectTool(object arg)
+ {
+ MouseType = MouseButtonType.Area;
+ }
+
+ ///
+ /// 选择编辑门区域
+ ///
+ public void OnSelectDoorTool(object arg)
+ {
+ MouseType = MouseButtonType.Door;
+ }
+
+ ///
+ /// 聚焦
+ ///
+ public void OnClickCenterTool(object arg)
+ {
+ var pos = MapEditorPanel.S_SubViewport.Instance.Size / 2;
+ if (_roomSize.X == 0 && _roomSize.Y == 0) //聚焦原点
+ {
+ SetMapPosition(pos);
+ }
+ else //聚焦地图中心点
+ {
+ SetMapPosition(pos - (_roomPosition + _roomSize / 2) * TileSet.TileSize * Scale);
+ }
+ }
+
+ ///
+ /// 创建地牢房间门区域
+ ///
+ /// 门方向
+ /// 起始坐标, 单位: 像素
+ /// 结束坐标, 单位: 像素
+ public DoorAreaInfo CreateDoorArea(DoorDirection direction, int start, int end)
+ {
+ var doorAreaInfo = new DoorAreaInfo();
+ doorAreaInfo.Direction = direction;
+ doorAreaInfo.Start = start;
+ doorAreaInfo.End = end;
+ //doorAreaInfo.CalcPosition(_roomPosition, _roomSize);
+ _doorConfigs.Add(doorAreaInfo);
+ return doorAreaInfo;
+ }
+
+ ///
+ /// 检测门区域数据是否可以提交
+ ///
+ /// 门方向
+ /// 起始坐标, 单位: 像素
+ /// 结束坐标, 单位: 像素
+ ///
+ public bool CheckDoorArea(DoorDirection direction, int start, int end)
+ {
+ foreach (var item in _doorConfigs)
+ {
+ if (item.Direction == direction)
+ {
+ if (CheckValueCollision(item.Start, item.End, start, end))
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// 检测门区域数据是否可以提交
+ ///
+ /// 需要检测的门
+ /// 起始坐标, 单位: 像素
+ /// 结束坐标, 单位: 像素
+ public bool CheckDoorArea(DoorAreaInfo target, int start, int end)
+ {
+ foreach (var item in _doorConfigs)
+ {
+ if (item.Direction == target.Direction && item != target)
+ {
+ if (CheckValueCollision(item.Start, item.End, start, end))
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private bool CheckValueCollision(float o1, float o2, float h1, float h2)
+ {
+ var size = GameConfig.TileCellSize;
+ return !(h2 < o1 - 3 * size || o2 + 3 * size < h1);
+ }
+
+ ///
+ /// 移除门区域数据
+ ///
+ public void RemoveDoorArea(DoorAreaInfo doorAreaInfo)
+ {
+ _doorConfigs.Remove(doorAreaInfo);
+ }
+
+ //保存房间配置
+ private void SaveRoomInfoConfig()
+ {
+ //存入本地
+ var roomInfo = _roomSplit.RoomInfo;
+ var path = MapProjectManager.GetConfigPath(roomInfo.GroupName,roomInfo.RoomType, roomInfo.RoomName);
+ if (!Directory.Exists(path))
+ {
+ Directory.CreateDirectory(path);
+ }
+
+ roomInfo.Position = new SerializeVector2(_roomPosition);
+ roomInfo.Size = new SerializeVector2(_roomSize);
+ roomInfo.DoorAreaInfos.Clear();
+ roomInfo.DoorAreaInfos.AddRange(_doorConfigs);
+
+ path += "/" + MapProjectManager.GetRoomInfoConfigName(roomInfo.RoomName);
+ var jsonStr = JsonSerializer.Serialize(roomInfo);
+ File.WriteAllText(path, jsonStr);
+ }
+
+ //保存地块数据
+ public void SaveTileInfoConfig()
+ {
+ //存入本地
+ var roomInfo = _roomSplit.RoomInfo;
+ var path = MapProjectManager.GetConfigPath(roomInfo.GroupName,roomInfo.RoomType, roomInfo.RoomName);
+ if (!Directory.Exists(path))
+ {
+ Directory.CreateDirectory(path);
+ }
+
+ var tileInfo = _roomSplit.TileInfo;
+ tileInfo.NavigationList.Clear();
+ tileInfo.NavigationList.AddRange(_dungeonTileMap.GetPolygonData());
+ tileInfo.Floor.Clear();
+ tileInfo.Middle.Clear();
+ tileInfo.Top.Clear();
+
+ PushLayerDataToList(AutoFloorLayer, _sourceId, tileInfo.Floor);
+ PushLayerDataToList(AutoMiddleLayer, _sourceId, tileInfo.Middle);
+ PushLayerDataToList(AutoTopLayer, _sourceId, tileInfo.Top);
+
+ path += "/" + MapProjectManager.GetTileInfoConfigName(roomInfo.RoomName);
+ var jsonStr = JsonSerializer.Serialize(tileInfo);
+ File.WriteAllText(path, jsonStr);
+ }
+
+ //设置地图坐标
+ private void SetMapPosition(Vector2 pos)
+ {
+ Position = pos;
+ MapEditorToolsPanel.SetDoorToolTransform(pos, Scale);
+ }
+
+ //设置地图大小
+ private void SetMapSize(Vector2I size, bool refreshDoorTrans)
+ {
+ if (_roomSize != size)
+ {
+ _roomSize = size;
+
+ if (refreshDoorTrans)
+ {
+ MapEditorToolsPanel.SetDoorHoverToolTransform(_roomPosition, _roomSize);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/DungeonShooting_Godot/src/game/ui/mapEditor/tileView/EditorTileMapBar.cs b/DungeonShooting_Godot/src/game/ui/mapEditor/tileView/EditorTileMapBar.cs
new file mode 100644
index 0000000..eb9b839
--- /dev/null
+++ b/DungeonShooting_Godot/src/game/ui/mapEditor/tileView/EditorTileMapBar.cs
@@ -0,0 +1,50 @@
+using Godot;
+
+namespace UI.MapEditor;
+
+public class EditorTileMapBar
+{
+ private MapEditor.TileMap _editorTileMap;
+ private EventFactory _eventFactory;
+
+ public EditorTileMapBar(MapEditorPanel editorPanel, MapEditor.TileMap editorTileMap)
+ {
+ _editorTileMap = editorTileMap;
+ _editorTileMap.Instance.MapEditorPanel = editorPanel;
+ _editorTileMap.Instance.MapEditorToolsPanel = editorPanel.S_MapEditorTools.Instance;
+ _editorTileMap.Instance.MapEditorToolsPanel.EditorMap = _editorTileMap;
+
+ _eventFactory = EventManager.CreateEventFactory();
+ }
+
+ public void OnShow()
+ {
+ _editorTileMap.L_Brush.Instance.Draw += OnDrawGuides;
+ _eventFactory.AddEventListener(EventEnum.OnSelectDragTool, _editorTileMap.Instance.OnSelectHandTool);
+ _eventFactory.AddEventListener(EventEnum.OnSelectPenTool, _editorTileMap.Instance.OnSelectPenTool);
+ _eventFactory.AddEventListener(EventEnum.OnSelectRectTool, _editorTileMap.Instance.OnSelectRectTool);
+ _eventFactory.AddEventListener(EventEnum.OnSelectDoorTool, _editorTileMap.Instance.OnSelectDoorTool);
+ _eventFactory.AddEventListener(EventEnum.OnClickCenterTool, _editorTileMap.Instance.OnClickCenterTool);
+ }
+
+ public void OnHide()
+ {
+ _editorTileMap.L_Brush.Instance.Draw -= OnDrawGuides;
+ _eventFactory.RemoveAllEventListener();
+ }
+
+ public void Process(float delta)
+ {
+ _editorTileMap.L_Brush.Instance.QueueRedraw();
+ }
+
+ private void OnDrawGuides()
+ {
+ _editorTileMap.Instance.DrawGuides(_editorTileMap.L_Brush.Instance);
+ }
+
+ public void OnDestroy()
+ {
+
+ }
+}
\ No newline at end of file