Interior Maps #13
Binary file not shown.
BIN
map_editor.scn (Stored with Git LFS)
BIN
map_editor.scn (Stored with Git LFS)
Binary file not shown.
|
@ -0,0 +1,18 @@
|
||||||
|
class_name MapEditorMenu extends VBoxContainer
|
||||||
|
|
||||||
|
##
|
||||||
|
##
|
||||||
|
##
|
||||||
|
signal edit_activated(edit_action: Callable)
|
||||||
|
|
||||||
|
##
|
||||||
|
##
|
||||||
|
##
|
||||||
|
func activate_editor(edit_action: Callable) -> void:
|
||||||
|
edit_activated.emit(edit_action)
|
||||||
|
|
||||||
|
##
|
||||||
|
##
|
||||||
|
##
|
||||||
|
func reset() -> void:
|
||||||
|
pass
|
Binary file not shown.
|
@ -0,0 +1,122 @@
|
||||||
|
class_name MeshGrid extends Node3D
|
||||||
|
|
||||||
|
const _CELL_COUNT := 8
|
||||||
|
|
||||||
|
class _Chunk:
|
||||||
|
var multimesh_instances: Array[_MultimeshInstance] = []
|
||||||
|
|
||||||
|
var meshes: Array[Mesh] = []
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
self.meshes.resize(_CELL_COUNT * _CELL_COUNT)
|
||||||
|
|
||||||
|
class _MultimeshInstance:
|
||||||
|
var _instance_rid := RID()
|
||||||
|
|
||||||
|
var _multimesh_rid := RID()
|
||||||
|
|
||||||
|
func _init(scenario_rid: RID, mesh: Mesh, transforms: Array) -> void:
|
||||||
|
_multimesh_rid = RenderingServer.multimesh_create()
|
||||||
|
_instance_rid = RenderingServer.instance_create2(_multimesh_rid, scenario_rid)
|
||||||
|
|
||||||
|
RenderingServer.multimesh_set_mesh(_multimesh_rid, mesh.get_rid())
|
||||||
|
|
||||||
|
var transform_count := transforms.size()
|
||||||
|
|
||||||
|
RenderingServer.multimesh_allocate_data(_multimesh_rid,
|
||||||
|
transform_count, RenderingServer.MULTIMESH_TRANSFORM_3D, true)
|
||||||
|
|
||||||
|
for i in transform_count:
|
||||||
|
RenderingServer.multimesh_instance_set_transform(_multimesh_rid, i, transforms[i])
|
||||||
|
|
||||||
|
var _chunks: Array[_Chunk] = []
|
||||||
|
|
||||||
|
var _grid_origin := Vector2(0.5, 0.5)
|
||||||
|
|
||||||
|
##
|
||||||
|
## The number of horizontal and vertical cells in the current grid.
|
||||||
|
##
|
||||||
|
## Setting this value will result in the current grid data being completely overwritten to return
|
||||||
|
## it to a sensible default.
|
||||||
|
##
|
||||||
|
@export
|
||||||
|
var size: Vector2i:
|
||||||
|
set(value):
|
||||||
|
self._chunks.resize(int(ceil((value.x * value.y) / float(_CELL_COUNT))))
|
||||||
|
|
||||||
|
for i in self._chunks.size():
|
||||||
|
self._chunks[i] = _Chunk.new()
|
||||||
|
|
||||||
|
size = value
|
||||||
|
|
||||||
|
func _notification(what: int) -> void:
|
||||||
|
match what:
|
||||||
|
NOTIFICATION_TRANSFORM_CHANGED:
|
||||||
|
for chunk in _chunks:
|
||||||
|
for multimesh_instance in chunk.multimesh_instances:
|
||||||
|
RenderingServer.instance_set_transform(
|
||||||
|
multimesh_instance._instance_rid, multimesh_instance.global_transform)
|
||||||
|
|
||||||
|
##
|
||||||
|
## Sets the cell at [code]coordinate[/code] to display [code]mesh[/code].
|
||||||
|
##
|
||||||
|
## Note that this function assumes [code]coordinate[/code] is within the bounds of the grid
|
||||||
|
## coordinate space.
|
||||||
|
##
|
||||||
|
## Note that changes to a cell result in the chunk it resides in being completely regenerated as
|
||||||
|
## part of its modification, and therefore has an implicitly higher overhead to other setter
|
||||||
|
## operations defined by [MeshGrid].
|
||||||
|
##
|
||||||
|
func set_mesh(coordinate: Vector2i, mesh: Mesh) -> void:
|
||||||
|
assert(Rect2i(Vector2i.ZERO, size).has_point(coordinate), "coordinate must be within grid")
|
||||||
|
|
||||||
|
# TODO: Once this is all lowered into native code, look for ways to parallelize the loops.
|
||||||
|
var chunk_coordinate := coordinate / _CELL_COUNT
|
||||||
|
var cell_coordinate := coordinate % _CELL_COUNT
|
||||||
|
var chunk := _chunks[(size.x * chunk_coordinate.y) + chunk_coordinate.x]
|
||||||
|
var chunk_meshes := chunk.meshes
|
||||||
|
|
||||||
|
chunk_meshes[(_CELL_COUNT * cell_coordinate.y) + cell_coordinate.x] = mesh
|
||||||
|
|
||||||
|
# Invalide any existing baked multimeshes in the chunk.
|
||||||
|
for multimesh_instance in chunk.multimesh_instances:
|
||||||
|
RenderingServer.free_rid(multimesh_instance._instance_rid)
|
||||||
|
RenderingServer.free_rid(multimesh_instance._multimesh_rid)
|
||||||
|
|
||||||
|
chunk.multimesh_instances.clear()
|
||||||
|
|
||||||
|
# Normalize mesh instance data for the chunk.
|
||||||
|
var mesh_transform_sets := {}
|
||||||
|
|
||||||
|
for i in chunk_meshes.size():
|
||||||
|
var chunk_mesh := chunk_meshes[i]
|
||||||
|
|
||||||
|
if chunk_mesh != null:
|
||||||
|
if not(chunk_mesh in mesh_transform_sets):
|
||||||
|
mesh_transform_sets[chunk_mesh] = []
|
||||||
|
|
||||||
|
mesh_transform_sets[chunk_mesh].push_back(Transform3D(Basis.IDENTITY, Vector3(
|
||||||
|
(float(chunk_coordinate.x * _CELL_COUNT) + (i % _CELL_COUNT)) -
|
||||||
|
(float(size.x) * _grid_origin.x), 0.0,
|
||||||
|
(float(chunk_coordinate.y * _CELL_COUNT) + (i / _CELL_COUNT)) -
|
||||||
|
(float(size.y) * _grid_origin.y))))
|
||||||
|
|
||||||
|
# Bake into multimesh instances for the chunk.
|
||||||
|
var scenario_rid := get_world_3d().scenario
|
||||||
|
|
||||||
|
for chunk_mesh in mesh_transform_sets:
|
||||||
|
var multimesh_instance :=\
|
||||||
|
_MultimeshInstance.new(scenario_rid, chunk_mesh, mesh_transform_sets[chunk_mesh])
|
||||||
|
|
||||||
|
RenderingServer.instance_set_transform(multimesh_instance._instance_rid, global_transform)
|
||||||
|
|
||||||
|
chunk.multimesh_instances.push_back(multimesh_instance)
|
||||||
|
|
||||||
|
##
|
||||||
|
## Returns [code]world_position[/code] converted into a coordinate aligned with the [MeshGrid].
|
||||||
|
##
|
||||||
|
## Note that [code]world_position[/code] values not within the [MeshGrid] will produce grid
|
||||||
|
## coordinates outside of the [MeshGrid] bounds as well.
|
||||||
|
##
|
||||||
|
func world_to_grid(world_position: Vector2) -> Vector2i:
|
||||||
|
return Vector2i((world_position + (Vector2(size) * _grid_origin)).floor())
|
|
@ -192,7 +192,7 @@ func get_cursor_point() -> Vector2:
|
||||||
## Successful point intersection will return the plane coordinates in a [class Vector2], otherwise
|
## Successful point intersection will return the plane coordinates in a [class Vector2], otherwise
|
||||||
## [code]null[/code] is returned.
|
## [code]null[/code] is returned.
|
||||||
##
|
##
|
||||||
func screen_to_plane(screen_point: Vector2):
|
func screen_to_plane(screen_point: Vector2) -> Variant:
|
||||||
if self._camera != null:
|
if self._camera != null:
|
||||||
var plane_target = Plane(Vector3.UP, 0.0).intersects_ray(
|
var plane_target = Plane(Vector3.UP, 0.0).intersects_ray(
|
||||||
self._camera.global_position, self._camera.project_ray_normal(screen_point))
|
self._camera.global_position, self._camera.project_ray_normal(screen_point))
|
||||||
|
|
|
@ -9,21 +9,36 @@
|
||||||
config_version=5
|
config_version=5
|
||||||
|
|
||||||
_global_script_classes=[{
|
_global_script_classes=[{
|
||||||
|
"base": "Control",
|
||||||
|
"class": &"ActionPrompt",
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://user_interface/action_prompt.gd"
|
||||||
|
}, {
|
||||||
"base": "HFlowContainer",
|
"base": "HFlowContainer",
|
||||||
"class": &"ItemSelection",
|
"class": &"ItemSelection",
|
||||||
"language": &"GDScript",
|
"language": &"GDScript",
|
||||||
"path": "res://user_interface/button_selection.gd"
|
"path": "res://user_interface/button_selection.gd"
|
||||||
}, {
|
}, {
|
||||||
"base": "Node",
|
"base": "VBoxContainer",
|
||||||
"class": &"MapEditorTerrainCanvas",
|
"class": &"MapEditorMenu",
|
||||||
"language": &"GDScript",
|
"language": &"GDScript",
|
||||||
"path": "res://map_editor/map_editor_terrain_canvas.gd"
|
"path": "res://map_editor/map_editor_menu.gd"
|
||||||
|
}, {
|
||||||
|
"base": "Node3D",
|
||||||
|
"class": &"MeshGrid",
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://mesh_grid.gd"
|
||||||
}, {
|
}, {
|
||||||
"base": "Node3D",
|
"base": "Node3D",
|
||||||
"class": &"PlayerController",
|
"class": &"PlayerController",
|
||||||
"language": &"GDScript",
|
"language": &"GDScript",
|
||||||
"path": "res://player_controller.gd"
|
"path": "res://player_controller.gd"
|
||||||
}, {
|
}, {
|
||||||
|
"base": "Control",
|
||||||
|
"class": &"SelectionPrompt",
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://user_interface/selection_prompt.gd"
|
||||||
|
}, {
|
||||||
"base": "Node",
|
"base": "Node",
|
||||||
"class": &"Settings",
|
"class": &"Settings",
|
||||||
"language": &"GDScript",
|
"language": &"GDScript",
|
||||||
|
@ -34,18 +49,33 @@ _global_script_classes=[{
|
||||||
"language": &"GDScript",
|
"language": &"GDScript",
|
||||||
"path": "res://terrain/terrain_instance_3d.gd"
|
"path": "res://terrain/terrain_instance_3d.gd"
|
||||||
}, {
|
}, {
|
||||||
|
"base": "Node",
|
||||||
|
"class": &"TerrainMapCanvas",
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://terrain/terrain_map_canvas.gd"
|
||||||
|
}, {
|
||||||
"base": "Resource",
|
"base": "Resource",
|
||||||
"class": &"TerrainPaint",
|
"class": &"TerrainPaint",
|
||||||
"language": &"GDScript",
|
"language": &"GDScript",
|
||||||
"path": "res://terrain/paints/terrain_paint.gd"
|
"path": "res://terrain/paints/terrain_paint.gd"
|
||||||
|
}, {
|
||||||
|
"base": "Control",
|
||||||
|
"class": &"WorkerPrompt",
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://user_interface/worker_prompt.gd"
|
||||||
}]
|
}]
|
||||||
_global_script_class_icons={
|
_global_script_class_icons={
|
||||||
|
"ActionPrompt": "",
|
||||||
"ItemSelection": "",
|
"ItemSelection": "",
|
||||||
"MapEditorTerrainCanvas": "",
|
"MapEditorMenu": "",
|
||||||
|
"MeshGrid": "",
|
||||||
"PlayerController": "",
|
"PlayerController": "",
|
||||||
|
"SelectionPrompt": "",
|
||||||
"Settings": "",
|
"Settings": "",
|
||||||
"TerrainInstance3D": "",
|
"TerrainInstance3D": "",
|
||||||
"TerrainPaint": ""
|
"TerrainMapCanvas": "",
|
||||||
|
"TerrainPaint": "",
|
||||||
|
"WorkerPrompt": ""
|
||||||
}
|
}
|
||||||
|
|
||||||
[application]
|
[application]
|
||||||
|
@ -59,6 +89,7 @@ config/icon="res://icon.png"
|
||||||
[autoload]
|
[autoload]
|
||||||
|
|
||||||
GameSettings="*res://settings.gd"
|
GameSettings="*res://settings.gd"
|
||||||
|
LocalPlayer="*res://local_player.scn"
|
||||||
|
|
||||||
[debug]
|
[debug]
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,9 @@ const _SHADER := preload("res://terrain/terrain.gdshader")
|
||||||
##
|
##
|
||||||
enum PaintSlot {
|
enum PaintSlot {
|
||||||
ERASE,
|
ERASE,
|
||||||
RED,
|
PAINT_1,
|
||||||
GREEN,
|
PAINT_2,
|
||||||
BLUE,
|
PAINT_3,
|
||||||
}
|
}
|
||||||
|
|
||||||
var _albedo_maps: Array[Texture2D] = [null, null, null, null]
|
var _albedo_maps: Array[Texture2D] = [null, null, null, null]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
class_name MapEditorTerrainCanvas extends Node
|
class_name TerrainMapCanvas extends Node
|
||||||
|
|
||||||
##
|
##
|
||||||
## Blank sample value.
|
## Blank sample value.
|
||||||
|
@ -37,7 +37,11 @@ func clear(clear_elevation: float) -> void:
|
||||||
_editable_texture.update(_editable_image)
|
_editable_texture.update(_editable_image)
|
||||||
|
|
||||||
##
|
##
|
||||||
## Returns the canvas data as a [Texture2D].
|
## Returns the canvas data as a [Texture2D] where RGB channels are encoded in the red, green, and
|
||||||
|
## blue, component of each pixel and the height is the alpha component.
|
||||||
|
##
|
||||||
|
## The returned value is designed for use with a [TerrainInstance3D] or other class compatible with
|
||||||
|
## the encoded format specified above.
|
||||||
##
|
##
|
||||||
func get_texture() -> Texture2D:
|
func get_texture() -> Texture2D:
|
||||||
return _editable_texture
|
return _editable_texture
|
|
@ -0,0 +1,43 @@
|
||||||
|
@tool
|
||||||
|
class_name ActionPrompt extends Control
|
||||||
|
|
||||||
|
##
|
||||||
|
##
|
||||||
|
##
|
||||||
|
signal prompt_accepted()
|
||||||
kayomn marked this conversation as resolved
|
|||||||
|
|
||||||
|
##
|
||||||
|
##
|
||||||
|
##
|
||||||
|
signal prompted()
|
||||||
kayomn marked this conversation as resolved
kayomn
commented
Missing doc comment. Missing doc comment.
|
|||||||
|
|
||||||
|
@export
|
||||||
|
var _accept_button: BaseButton = null
|
||||||
|
|
||||||
|
@export
|
||||||
|
var initial_focus: Control = null
|
||||||
|
|
||||||
|
func _get_configuration_warnings() -> PackedStringArray:
|
||||||
|
var warnings := PackedStringArray()
|
||||||
|
|
||||||
|
if _accept_button == null:
|
||||||
|
warnings.append("`Accept Button` must point to a valid BaseButton instance")
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
assert(_accept_button != null, "accept button cannot be null")
|
||||||
|
|
||||||
|
_accept_button.pressed.connect(func () -> void:
|
||||||
|
prompt_accepted.emit()
|
||||||
|
hide())
|
||||||
|
|
||||||
|
##
|
||||||
|
##
|
||||||
|
##
|
||||||
|
func prompt() -> void:
|
||||||
kayomn marked this conversation as resolved
kayomn
commented
Missing doc comment. Missing doc comment.
|
|||||||
|
show()
|
||||||
|
prompted.emit()
|
||||||
|
|
||||||
|
if initial_focus != null:
|
||||||
|
initial_focus.grab_focus()
|
|
@ -0,0 +1,68 @@
|
||||||
|
@tool
|
||||||
|
class_name SelectionPrompt extends Control
|
||||||
|
|
||||||
|
##
|
||||||
|
##
|
||||||
|
##
|
||||||
|
signal prompted()
|
||||||
kayomn marked this conversation as resolved
kayomn
commented
Missing doc comment. Missing doc comment.
|
|||||||
|
|
||||||
|
##
|
||||||
|
##
|
||||||
|
##
|
||||||
|
signal prompt_selected(selected_item: int)
|
||||||
kayomn marked this conversation as resolved
kayomn
commented
Missing doc comment. Missing doc comment.
|
|||||||
|
|
||||||
|
@export
|
||||||
|
var _button_group: ButtonGroup = null
|
||||||
|
|
||||||
|
##
|
||||||
|
## If set to [code]true[/code], the selection prompt dismisses itself after a selection is made.
|
||||||
|
## Otherwise, if set to [code]false[/code], the prompt remains present after a selection is made.
|
||||||
|
##
|
||||||
|
@export
|
||||||
|
var dismiss_on_selection := false
|
||||||
|
|
||||||
|
func _get_configuration_warnings() -> PackedStringArray:
|
||||||
|
var warnings := PackedStringArray()
|
||||||
|
|
||||||
|
if _button_group == null:
|
||||||
|
warnings.append("`Button Group` must point to a valid BaseButton instance")
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
assert(_button_group != null, "button group cannot be null")
|
||||||
|
|
||||||
|
_button_group.pressed.connect(func (button: BaseButton) -> void:
|
||||||
|
var selected_item = _button_group.get_buttons().find(button)
|
||||||
|
|
||||||
|
assert(selected_item > -1, "selected item cannot be less than 0")
|
||||||
|
prompt_selected.emit(selected_item)
|
||||||
|
|
||||||
|
if dismiss_on_selection:
|
||||||
|
hide())
|
||||||
|
|
||||||
|
##
|
||||||
|
## Returns the [ButtonGroup] used by the selection prompt to handle selections.
|
||||||
|
##
|
||||||
|
func get_button_group() -> ButtonGroup:
|
||||||
|
return _button_group
|
||||||
|
|
||||||
|
##
|
||||||
|
## Displays the selection prompt, with [code]initial_selection[/code] as the initial value selected.
|
||||||
|
##
|
||||||
|
## Once a value has been manually selected in the prompt, [signal prompt_selected] is emitted.
|
||||||
|
##
|
||||||
|
## *Note* that [code]initial_selection[/code] *must* be a valid index in the range of the button
|
||||||
|
## group used by the selection prompt, which may be accessed by the calling logic via
|
||||||
|
## [member get_button_group]
|
||||||
|
##
|
||||||
|
func prompt(initial_selection: int) -> void:
|
||||||
|
assert(_button_group != null, "button group cannot be null")
|
||||||
|
|
||||||
|
var selected_button := _button_group.get_buttons()[initial_selection]
|
||||||
|
|
||||||
|
selected_button.button_pressed = true
|
||||||
|
|
||||||
|
show()
|
||||||
|
selected_button.grab_focus()
|
||||||
|
prompted.emit()
|
|
@ -0,0 +1,46 @@
|
||||||
|
class_name WorkerPrompt extends Control
|
||||||
|
|
||||||
|
##
|
||||||
|
##
|
||||||
|
##
|
||||||
|
signal prompted()
|
||||||
kayomn marked this conversation as resolved
kayomn
commented
Missing doc comment. Missing doc comment.
|
|||||||
|
|
||||||
|
var _worker_thread := Thread.new()
|
||||||
|
|
||||||
|
##
|
||||||
|
##
|
||||||
kayomn marked this conversation as resolved
kayomn
commented
Missing doc comment. Missing doc comment.
|
|||||||
|
##
|
||||||
|
@export
|
||||||
|
var label: Label = null
|
||||||
|
|
||||||
|
##
|
||||||
|
##
|
||||||
|
##
|
||||||
|
@export
|
||||||
kayomn marked this conversation as resolved
kayomn
commented
Missing doc comment. Missing doc comment.
|
|||||||
|
var progress: Range = null
|
||||||
|
|
||||||
|
##
|
||||||
|
##
|
||||||
|
##
|
||||||
|
func prompt(display_message: String, steps: Array) -> void:
|
||||||
kayomn marked this conversation as resolved
kayomn
commented
Missing doc comment. Missing doc comment.
|
|||||||
|
show()
|
||||||
|
prompted.emit()
|
||||||
|
|
||||||
|
if label != null:
|
||||||
|
label.text = display_message
|
||||||
|
|
||||||
|
_worker_thread.start(func () -> void:
|
||||||
|
var count := steps.size()
|
||||||
|
var total := float(count)
|
||||||
|
|
||||||
|
for i in count:
|
||||||
|
steps[i].call()
|
||||||
|
|
||||||
|
if progress != null:
|
||||||
|
progress.value = i / total)
|
||||||
|
|
||||||
|
while _worker_thread.is_alive():
|
||||||
|
await get_tree().process_frame
|
||||||
|
|
||||||
|
_worker_thread.wait_to_finish()
|
||||||
|
hide()
|
Loading…
Reference in New Issue
Missing doc comment.