Adding Grid Field that supports both square and hex tiles
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
[gd_scene load_steps=9 format=4 uid="uid://qd6xvgwrd00g"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://3goclr4srt7k" path="res://scenes/Grid/grid_plane_manager.gd" id="1_72cxc"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ground"]
|
||||
albedo_color = Color(0.211765, 0.364706, 0.211765, 1)
|
||||
|
||||
[sub_resource type="PlaneMesh" id="PlaneMesh_ground"]
|
||||
size = Vector2(10, 10)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_ground"]
|
||||
size = Vector3(10, 0.4, 10)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_grid"]
|
||||
no_depth_test = true
|
||||
shading_mode = 0
|
||||
vertex_color_use_as_albedo = true
|
||||
albedo_color = Color(0.811765, 0.917647, 1, 0.74902)
|
||||
|
||||
[sub_resource type="ArrayMesh" id="ArrayMesh_q3k88"]
|
||||
_surfaces = [{
|
||||
"aabb": AABB(-5, 0.02, -5, 10, 1e-05, 10),
|
||||
"attribute_data": PackedByteArray("M7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6U="),
|
||||
"format": 34359738377,
|
||||
"primitive": 1,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 44,
|
||||
"vertex_data": PackedByteArray("AACgwArXozwAAKDAAACgwArXozwAAKBAAACAwArXozwAAKDAAACAwArXozwAAKBAAABAwArXozwAAKDAAABAwArXozwAAKBAAAAAwArXozwAAKDAAAAAwArXozwAAKBAAACAvwrXozwAAKDAAACAvwrXozwAAKBAAAAAAArXozwAAKDAAAAAAArXozwAAKBAAACAPwrXozwAAKDAAACAPwrXozwAAKBAAAAAQArXozwAAKDAAAAAQArXozwAAKBAAABAQArXozwAAKDAAABAQArXozwAAKBAAACAQArXozwAAKDAAACAQArXozwAAKBAAACgQArXozwAAKDAAACgQArXozwAAKBAAACgwArXozwAAKDAAACgQArXozwAAKDAAACgwArXozwAAIDAAACgQArXozwAAIDAAACgwArXozwAAEDAAACgQArXozwAAEDAAACgwArXozwAAADAAACgQArXozwAAADAAACgwArXozwAAIC/AACgQArXozwAAIC/AACgwArXozwAAAAAAACgQArXozwAAAAAAACgwArXozwAAIA/AACgQArXozwAAIA/AACgwArXozwAAABAAACgQArXozwAAABAAACgwArXozwAAEBAAACgQArXozwAAEBAAACgwArXozwAAIBAAACgQArXozwAAIBAAACgwArXozwAAKBAAACgQArXozwAAKBA")
|
||||
}]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_highlight"]
|
||||
transparency = 1
|
||||
no_depth_test = true
|
||||
shading_mode = 0
|
||||
albedo_color = Color(0.976471, 0.843137, 0.333333, 0.6)
|
||||
|
||||
[sub_resource type="PlaneMesh" id="PlaneMesh_8dkrf"]
|
||||
size = Vector2(1, 1)
|
||||
|
||||
[node name="GridField" type="Node3D"]
|
||||
script = ExtResource("1_72cxc")
|
||||
columns = 10
|
||||
rows = 10
|
||||
cell_size = 1.0
|
||||
ground_thickness = 0.4
|
||||
show_grid_lines = true
|
||||
grid_line_color = Color(0.2, 0.7, 1, 0.65)
|
||||
|
||||
[node name="GroundMesh" type="MeshInstance3D" parent="."]
|
||||
unique_name_in_owner = true
|
||||
material_override = SubResource("StandardMaterial3D_ground")
|
||||
cast_shadow = 0
|
||||
mesh = SubResource("PlaneMesh_ground")
|
||||
|
||||
[node name="GroundBody" type="StaticBody3D" parent="."]
|
||||
|
||||
[node name="GroundCollision" type="CollisionShape3D" parent="GroundBody"]
|
||||
unique_name_in_owner = true
|
||||
shape = SubResource("BoxShape3D_ground")
|
||||
|
||||
[node name="GridLines" type="MeshInstance3D" parent="."]
|
||||
unique_name_in_owner = true
|
||||
material_override = SubResource("StandardMaterial3D_grid")
|
||||
cast_shadow = 0
|
||||
mesh = SubResource("ArrayMesh_q3k88")
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(0.707107, -0.5, 0.5, 0, 0.707107, 0.707107, -0.707107, -0.5, 0.5, 0, 5, 0)
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="."]
|
||||
unique_name_in_owner = true
|
||||
transform = Transform3D(0.70710677, -0.40824828, 0.57735026, 0, 0.8164966, 0.57735026, -0.70710677, -0.40824828, 0.57735026, 6, 6, 6)
|
||||
current = true
|
||||
fov = 60.0
|
||||
|
||||
[node name="HoverHighlight" type="MeshInstance3D" parent="."]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
material_override = SubResource("StandardMaterial3D_highlight")
|
||||
cast_shadow = 0
|
||||
mesh = SubResource("PlaneMesh_8dkrf")
|
||||
@@ -0,0 +1,394 @@
|
||||
@tool
|
||||
extends Node3D
|
||||
class_name GridPlaneManager
|
||||
|
||||
enum GridType { SQUARE, HEX }
|
||||
|
||||
signal grid_layout_changed(cells: Dictionary)
|
||||
|
||||
var _grid_type: GridType = GridType.SQUARE
|
||||
@export var grid_type: GridType = GridType.SQUARE:
|
||||
set(value):
|
||||
if value == _grid_type:
|
||||
return
|
||||
_grid_type = value as GridType
|
||||
_queue_rebuild()
|
||||
get:
|
||||
return _grid_type
|
||||
|
||||
var _columns: int = 10
|
||||
@export_range(1, 256, 1) var columns: int:
|
||||
set(value):
|
||||
value = max(value, 1)
|
||||
if value == _columns:
|
||||
return
|
||||
_columns = value
|
||||
_queue_rebuild()
|
||||
get:
|
||||
return _columns
|
||||
|
||||
var _rows: int = 10
|
||||
@export_range(1, 256, 1) var rows: int:
|
||||
set(value):
|
||||
value = max(value, 1)
|
||||
if value == _rows:
|
||||
return
|
||||
_rows = value
|
||||
_queue_rebuild()
|
||||
get:
|
||||
return _rows
|
||||
|
||||
var _cell_size: float = 1.0
|
||||
@export_range(0.1, 10.0, 0.1) var cell_size: float:
|
||||
set(value):
|
||||
value = max(value, 0.25)
|
||||
if is_equal_approx(value, _cell_size):
|
||||
return
|
||||
_cell_size = value
|
||||
_queue_rebuild()
|
||||
get:
|
||||
return _cell_size
|
||||
|
||||
var _ground_thickness: float = 0.4
|
||||
@export_range(0.1, 2.0, 0.05) var ground_thickness: float:
|
||||
set(value):
|
||||
value = clamp(value, 0.1, 2.0)
|
||||
if is_equal_approx(value, _ground_thickness):
|
||||
return
|
||||
_ground_thickness = value
|
||||
_queue_rebuild()
|
||||
get:
|
||||
return _ground_thickness
|
||||
|
||||
var _show_grid_lines: bool = true
|
||||
@export var show_grid_lines: bool:
|
||||
set(value):
|
||||
if value == _show_grid_lines:
|
||||
return
|
||||
_show_grid_lines = value
|
||||
if is_node_ready():
|
||||
_update_grid_visibility()
|
||||
get:
|
||||
return _show_grid_lines
|
||||
|
||||
var _grid_line_color: Color = Color(0.2, 0.7, 1.0, 0.65)
|
||||
@export var grid_line_color: Color:
|
||||
set(value):
|
||||
if value == _grid_line_color:
|
||||
return
|
||||
_grid_line_color = value
|
||||
_queue_rebuild()
|
||||
get:
|
||||
return _grid_line_color
|
||||
|
||||
const _LINE_HEIGHT_OFFSET := 0.02
|
||||
const _SQRT3 := 1.7320508075688772
|
||||
|
||||
@onready var _ground_mesh: MeshInstance3D = %GroundMesh
|
||||
@onready var _ground_collision: CollisionShape3D = %GroundCollision
|
||||
@onready var _grid_lines: MeshInstance3D = %GridLines
|
||||
@onready var _camera: Camera3D = %Camera3D
|
||||
@onready var _hover_highlight: MeshInstance3D = %HoverHighlight
|
||||
|
||||
var _grid_size: Vector2 = Vector2.ZERO
|
||||
var _cells: Dictionary = {}
|
||||
var _queued_rebuild: bool = false
|
||||
var _hovered_cell: Vector2i = Vector2i(-1, -1)
|
||||
var _hex_center_offset_local: Vector3 = Vector3.ZERO
|
||||
|
||||
func _ready() -> void:
|
||||
set_process(true)
|
||||
_queue_rebuild()
|
||||
|
||||
func get_cells() -> Dictionary:
|
||||
return _cells.duplicate()
|
||||
|
||||
func get_cell_center(index: Vector2i) -> Vector3:
|
||||
return _cells.get(index, Vector3.ZERO)
|
||||
|
||||
func cell_to_world(index: Vector2i) -> Vector3:
|
||||
return get_cell_center(index)
|
||||
|
||||
func world_to_cell(world_position: Vector3, clamp_to_bounds: bool = true) -> Vector2i:
|
||||
var local_position: Vector3 = to_local(world_position)
|
||||
if _grid_type == GridType.HEX:
|
||||
return _world_to_hex_cell(local_position, clamp_to_bounds)
|
||||
return _world_to_square_cell(local_position, clamp_to_bounds)
|
||||
|
||||
func for_each_cell(callback: Callable) -> void:
|
||||
for index in _cells.keys():
|
||||
callback.call(index, _cells[index])
|
||||
|
||||
func _queue_rebuild() -> void:
|
||||
if _queued_rebuild:
|
||||
return
|
||||
_queued_rebuild = true
|
||||
call_deferred("_process_rebuild_request")
|
||||
|
||||
func _process_rebuild_request() -> void:
|
||||
_queued_rebuild = false
|
||||
if not is_node_ready():
|
||||
return
|
||||
_rebuild_grid()
|
||||
|
||||
func _rebuild_grid() -> void:
|
||||
match _grid_type:
|
||||
GridType.HEX:
|
||||
_generate_hex_grid()
|
||||
_:
|
||||
_generate_square_grid()
|
||||
_update_ground_assets()
|
||||
_update_grid_lines()
|
||||
_update_highlight_mesh()
|
||||
_update_camera_view()
|
||||
emit_signal("grid_layout_changed", _cells)
|
||||
|
||||
func _update_ground_assets() -> void:
|
||||
if _ground_mesh and _ground_mesh.mesh is PlaneMesh:
|
||||
var plane_mesh := _ground_mesh.mesh as PlaneMesh
|
||||
plane_mesh.size = _grid_size
|
||||
if _ground_collision and _ground_collision.shape is BoxShape3D:
|
||||
var box := _ground_collision.shape as BoxShape3D
|
||||
box.size = Vector3(_grid_size.x, _ground_thickness, _grid_size.y)
|
||||
|
||||
func _generate_square_grid() -> void:
|
||||
_hex_center_offset_local = Vector3.ZERO
|
||||
_cells.clear()
|
||||
_grid_size = Vector2(_columns * _cell_size, _rows * _cell_size)
|
||||
var start_x := -_grid_size.x * 0.5 + _cell_size * 0.5
|
||||
var start_z := -_grid_size.y * 0.5 + _cell_size * 0.5
|
||||
for row in range(_rows):
|
||||
for column in range(_columns):
|
||||
var center := Vector3(
|
||||
start_x + float(column) * _cell_size,
|
||||
0.0,
|
||||
start_z + float(row) * _cell_size
|
||||
)
|
||||
_cells[Vector2i(column, row)] = center
|
||||
|
||||
func _generate_hex_grid() -> void:
|
||||
_cells.clear()
|
||||
if _columns <= 0 or _rows <= 0:
|
||||
_grid_size = Vector2.ZERO
|
||||
return
|
||||
var radius: float = max(_cell_size * 0.5, 0.001)
|
||||
var min_corner_x: float = INF
|
||||
var max_corner_x: float = -INF
|
||||
var min_corner_z: float = INF
|
||||
var max_corner_z: float = -INF
|
||||
for row in range(_rows):
|
||||
for column in range(_columns):
|
||||
var x := radius * 1.5 * float(column)
|
||||
var z := radius * _SQRT3 * (float(row) + float(column) * 0.5)
|
||||
var center := Vector3(x, 0.0, z)
|
||||
_cells[Vector2i(column, row)] = center
|
||||
var corners := _compute_hex_corners(center, radius)
|
||||
for corner in corners:
|
||||
min_corner_x = min(min_corner_x, corner.x)
|
||||
max_corner_x = max(max_corner_x, corner.x)
|
||||
min_corner_z = min(min_corner_z, corner.z)
|
||||
max_corner_z = max(max_corner_z, corner.z)
|
||||
if min_corner_x == INF:
|
||||
_grid_size = Vector2.ZERO
|
||||
return
|
||||
var size_x: float = max_corner_x - min_corner_x
|
||||
var size_z: float = max_corner_z - min_corner_z
|
||||
var center_offset: Vector3 = Vector3(min_corner_x + size_x * 0.5, 0.0, min_corner_z + size_z * 0.5)
|
||||
for key in _cells.keys():
|
||||
_cells[key] -= center_offset
|
||||
_hex_center_offset_local = center_offset
|
||||
_grid_size = Vector2(size_x, size_z)
|
||||
|
||||
func _update_grid_lines() -> void:
|
||||
if not _grid_lines:
|
||||
return
|
||||
if not _show_grid_lines:
|
||||
_grid_lines.visible = false
|
||||
_grid_lines.mesh = null
|
||||
return
|
||||
_grid_lines.visible = true
|
||||
if _grid_type == GridType.HEX:
|
||||
_grid_lines.mesh = _build_hex_grid_mesh()
|
||||
else:
|
||||
_grid_lines.mesh = _build_square_grid_mesh()
|
||||
|
||||
func _update_grid_visibility() -> void:
|
||||
if not _grid_lines:
|
||||
return
|
||||
_grid_lines.visible = _show_grid_lines
|
||||
|
||||
func _build_square_grid_mesh() -> ArrayMesh:
|
||||
var st := SurfaceTool.new()
|
||||
st.begin(Mesh.PRIMITIVE_LINES)
|
||||
var half_width := _grid_size.x * 0.5
|
||||
var half_depth := _grid_size.y * 0.5
|
||||
var color := _grid_line_color
|
||||
for column in range(_columns + 1):
|
||||
var x := -half_width + float(column) * _cell_size
|
||||
st.set_color(color)
|
||||
st.add_vertex(Vector3(x, _LINE_HEIGHT_OFFSET, -half_depth))
|
||||
st.set_color(color)
|
||||
st.add_vertex(Vector3(x, _LINE_HEIGHT_OFFSET, half_depth))
|
||||
for row in range(_rows + 1):
|
||||
var z := -half_depth + float(row) * _cell_size
|
||||
st.set_color(color)
|
||||
st.add_vertex(Vector3(-half_width, _LINE_HEIGHT_OFFSET, z))
|
||||
st.set_color(color)
|
||||
st.add_vertex(Vector3(half_width, _LINE_HEIGHT_OFFSET, z))
|
||||
var mesh := st.commit()
|
||||
return mesh
|
||||
|
||||
func _build_hex_grid_mesh() -> ArrayMesh:
|
||||
var st := SurfaceTool.new()
|
||||
st.begin(Mesh.PRIMITIVE_LINES)
|
||||
var color := _grid_line_color
|
||||
var radius: float = max(_cell_size * 0.5, 0.001)
|
||||
for center in _cells.values():
|
||||
var center_vec: Vector3 = center
|
||||
var corners := _compute_hex_corners(center_vec, radius)
|
||||
for i in range(corners.size()):
|
||||
var a: Vector3 = corners[i]
|
||||
var b: Vector3 = corners[(i + 1) % corners.size()]
|
||||
st.set_color(color)
|
||||
st.add_vertex(a + Vector3(0, _LINE_HEIGHT_OFFSET, 0))
|
||||
st.set_color(color)
|
||||
st.add_vertex(b + Vector3(0, _LINE_HEIGHT_OFFSET, 0))
|
||||
var mesh := st.commit()
|
||||
return mesh
|
||||
|
||||
func _compute_hex_corners(center: Vector3, radius: float) -> Array[Vector3]:
|
||||
var corners: Array[Vector3] = []
|
||||
for i in range(6):
|
||||
var angle_deg := 60.0 * i
|
||||
var angle := deg_to_rad(angle_deg)
|
||||
var x := center.x + radius * cos(angle)
|
||||
var z := center.z + radius * sin(angle)
|
||||
corners.append(Vector3(x, 0.0, z))
|
||||
return corners
|
||||
|
||||
func _update_highlight_mesh() -> void:
|
||||
if not _hover_highlight:
|
||||
return
|
||||
if _grid_type == GridType.HEX:
|
||||
_hover_highlight.mesh = _build_hex_highlight_mesh()
|
||||
else:
|
||||
var plane := PlaneMesh.new()
|
||||
plane.size = Vector2(_cell_size, _cell_size)
|
||||
_hover_highlight.mesh = plane
|
||||
|
||||
func _build_hex_highlight_mesh() -> ArrayMesh:
|
||||
var st := SurfaceTool.new()
|
||||
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||
var radius: float = max(_cell_size * 0.5, 0.001)
|
||||
var corners := _compute_hex_corners(Vector3.ZERO, radius)
|
||||
for i in range(1, corners.size() - 1):
|
||||
st.add_vertex(corners[0])
|
||||
st.add_vertex(corners[i])
|
||||
st.add_vertex(corners[i + 1])
|
||||
var mesh := st.commit()
|
||||
return mesh
|
||||
|
||||
func _world_to_square_cell(local_position: Vector3, clamp_to_bounds: bool) -> Vector2i:
|
||||
var half_width := _grid_size.x * 0.5
|
||||
var half_depth := _grid_size.y * 0.5
|
||||
var normalized_x := (local_position.x + half_width) / _cell_size
|
||||
var normalized_z := (local_position.z + half_depth) / _cell_size
|
||||
var x := int(floor(normalized_x))
|
||||
var z := int(floor(normalized_z))
|
||||
if clamp_to_bounds:
|
||||
x = clamp(x, 0, _columns - 1)
|
||||
z = clamp(z, 0, _rows - 1)
|
||||
return Vector2i(x, z)
|
||||
|
||||
func _world_to_hex_cell(local_position: Vector3, clamp_to_bounds: bool) -> Vector2i:
|
||||
var adjusted: Vector3 = local_position + _hex_center_offset_local
|
||||
var axial: Vector2 = _local_hex_to_axial(adjusted)
|
||||
var cube: Vector3 = _axial_to_cube(axial)
|
||||
var rounded: Vector3 = _cube_round(cube)
|
||||
var q: int = int(rounded.x)
|
||||
var r: int = int(rounded.z)
|
||||
if clamp_to_bounds:
|
||||
q = clamp(q, 0, _columns - 1)
|
||||
r = clamp(r, 0, _rows - 1)
|
||||
elif q < 0 or q >= _columns or r < 0 or r >= _rows:
|
||||
return Vector2i(-1, -1)
|
||||
return Vector2i(q, r)
|
||||
|
||||
func _local_hex_to_axial(local_position: Vector3) -> Vector2:
|
||||
var radius: float = max(_cell_size * 0.5, 0.0001)
|
||||
var q: float = (2.0 / 3.0 * local_position.x) / radius
|
||||
var r: float = ((-1.0 / 3.0) * local_position.x + (_SQRT3 / 3.0) * local_position.z) / radius
|
||||
return Vector2(q, r)
|
||||
|
||||
func _axial_to_cube(axial: Vector2) -> Vector3:
|
||||
var x: float = axial.x
|
||||
var z: float = axial.y
|
||||
var y: float = -x - z
|
||||
return Vector3(x, y, z)
|
||||
|
||||
func _cube_round(cube: Vector3) -> Vector3:
|
||||
var rx: float = round(cube.x)
|
||||
var ry: float = round(cube.y)
|
||||
var rz: float = round(cube.z)
|
||||
var x_diff: float = abs(rx - cube.x)
|
||||
var y_diff: float = abs(ry - cube.y)
|
||||
var z_diff: float = abs(rz - cube.z)
|
||||
if x_diff > y_diff and x_diff > z_diff:
|
||||
rx = -ry - rz
|
||||
elif y_diff > z_diff:
|
||||
ry = -rx - rz
|
||||
else:
|
||||
rz = -rx - ry
|
||||
return Vector3(rx, ry, rz)
|
||||
|
||||
func _update_camera_view() -> void:
|
||||
if not _camera:
|
||||
return
|
||||
var extent: float = max(_grid_size.x, _grid_size.y)
|
||||
var half_extent: float = extent * 0.5
|
||||
var distance: float = max(half_extent * 1.1, 6.0)
|
||||
var height: float = max(half_extent * 0.8, 6.0)
|
||||
var offset: Vector3 = Vector3(distance, height, distance)
|
||||
var target: Vector3 = global_transform.origin
|
||||
var offset_global: Vector3 = global_transform.basis * offset
|
||||
var desired_position: Vector3 = target + offset_global
|
||||
_camera.look_at_from_position(desired_position, target, Vector3.UP)
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
var mouse_position: Vector2 = get_viewport().get_mouse_position()
|
||||
_update_hovered_cell(mouse_position)
|
||||
|
||||
func _update_hovered_cell(mouse_position: Vector2) -> void:
|
||||
if not _camera or not _hover_highlight:
|
||||
return
|
||||
var ray_origin: Vector3 = _camera.project_ray_origin(mouse_position)
|
||||
var ray_direction: Vector3 = _camera.project_ray_normal(mouse_position)
|
||||
var plane_normal: Vector3 = global_transform.basis.y.normalized()
|
||||
var plane_point: Vector3 = global_transform.origin
|
||||
var denominator: float = plane_normal.dot(ray_direction)
|
||||
if abs(denominator) < 0.0001:
|
||||
_clear_hover()
|
||||
return
|
||||
var t: float = plane_normal.dot(plane_point - ray_origin) / denominator
|
||||
if t <= 0.0:
|
||||
_clear_hover()
|
||||
return
|
||||
var world_point: Vector3 = ray_origin + ray_direction * t
|
||||
var cell_index: Vector2i = world_to_cell(world_point, false)
|
||||
if cell_index.x < 0 or cell_index.y < 0 or not _cells.has(cell_index):
|
||||
_clear_hover()
|
||||
return
|
||||
if cell_index == _hovered_cell:
|
||||
return
|
||||
_hovered_cell = cell_index
|
||||
var local_center: Vector3 = cell_to_world(cell_index)
|
||||
var target_position: Vector3 = to_global(local_center)
|
||||
_hover_highlight.global_position = target_position + plane_normal * 0.02
|
||||
_hover_highlight.visible = true
|
||||
|
||||
func _clear_hover() -> void:
|
||||
if _hover_highlight:
|
||||
_hover_highlight.visible = false
|
||||
_hovered_cell = Vector2i(-1, -1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://3goclr4srt7k
|
||||
Reference in New Issue
Block a user