diff --git a/interior/interior_map.gd b/interior/interior_map.gd new file mode 100644 index 0000000..09c9b3a --- /dev/null +++ b/interior/interior_map.gd @@ -0,0 +1,339 @@ +class_name InteriorMap extends Node3D + +const _CHUNK_SIZE := 32 + +const _GRID_ORIGIN := Vector2(0.5, 0.5) + +## +## Cardinal orientation referencing the direction of a tile. +## +enum Orientation { + NORTH, + EAST, + SOUTH, + WEST, +} + +## +## Baked block of meshes making up a segment of the grid. +## +class Chunk: + var _floor_meshes: Array[Mesh] = [] + + var _floor_orientation := PackedInt32Array() + + var _multi_mesh_instances: Array[MultiMeshInstance] = [] + + var _wall_meshes: Array[Mesh] = [] + + var _wall_orientation := PackedInt32Array() + + func _init() -> void: + var buffer_size := _CHUNK_SIZE * _CHUNK_SIZE + + _floor_meshes.resize(buffer_size) + _floor_orientation.resize(buffer_size) + _wall_meshes.resize(buffer_size) + _wall_orientation.resize(buffer_size) + + ## + ## Invalidates the mesh block, re-baking its contents from the current mesh data set. + ## + func invalidate(interior_map: InteriorMap, coordinate: Vector2i) -> void: + # TODO: Once this is all lowered into native code, look for ways to parallelize the loops. + for multi_mesh_instance in _multi_mesh_instances: + RenderingServer.free_rid(multi_mesh_instance._instance_rid) + RenderingServer.free_rid(multi_mesh_instance._multimesh_rid) + + _multi_mesh_instances.clear() + + # Normalize mesh instance data for the chunk. + var transforms_by_mesh := {} + var grid_size := interior_map.size + var half_pi := PI / 2 + + for i in _floor_meshes.size(): + var mesh := _floor_meshes[i] + + if mesh != null: + if not(mesh in transforms_by_mesh): + transforms_by_mesh[mesh] = [] + + transforms_by_mesh[mesh].append(Transform3D( + Basis.IDENTITY.rotated(Vector3.UP, half_pi * _floor_orientation[i]), Vector3( + (float(coordinate.x * _CHUNK_SIZE) + (i % _CHUNK_SIZE)) - + (float(grid_size.x) * _GRID_ORIGIN.x) + 0.5, 0.0, + (float(coordinate.y * _CHUNK_SIZE) + (i / _CHUNK_SIZE)) - + (float(grid_size.y) * _GRID_ORIGIN.y) + 0.5))) + + for i in _wall_meshes.size(): + var mesh := _wall_meshes[i] + + if mesh != null: + if not(mesh in transforms_by_mesh): + transforms_by_mesh[mesh] = [] + + transforms_by_mesh[mesh].append(Transform3D( + Basis.IDENTITY.rotated(Vector3.UP, half_pi * _wall_orientation[i]), Vector3( + (float(coordinate.x * _CHUNK_SIZE) + (i % _CHUNK_SIZE)) - + (float(grid_size.x) * _GRID_ORIGIN.x) + 0.5, 0.0, + (float(coordinate.y * _CHUNK_SIZE) + (i / _CHUNK_SIZE)) - + (float(grid_size.y) * _GRID_ORIGIN.y) + 0.5))) + + # (Re)-bake into multimesh instances for the chunk. + var scenario_rid := interior_map.get_world_3d().scenario + var global_transform := interior_map.global_transform + + for chunk_mesh in transforms_by_mesh: + var multi_mesh_instance := MultiMeshInstance.new( + scenario_rid, chunk_mesh.get_rid(), transforms_by_mesh[chunk_mesh]) + + multi_mesh_instance.set_offset(global_transform) + _multi_mesh_instances.append(multi_mesh_instance) + + ## + ## Sets the floor mesh in the chunk at the location relative [code]coordinatess[/code] to + ## [code]mesh[/code]. + ## + ## *Note* that [method Chunk.invalidate] must be called at some point after to visually update + ## the chunk. + ## + func set_floor_mesh(coordinates: Vector2i, orientation: Orientation, mesh: Mesh) -> void: + var index := (_CHUNK_SIZE * coordinates.y) + coordinates.x + + _floor_orientation[index] = orientation + _floor_meshes[index] = mesh + + ## + ## Sets the wall mesh in the chunk at the location relative [code]coordinatess[/code] to + ## [code]mesh[/code]. + ## + ## *Note* that [method Chunk.invalidate] must be called at some point after to visually update + ## the chunk. + ## + func set_wall_mesh(coordinates: Vector2i, orientation: Orientation, mesh: Mesh) -> void: + var index := (_CHUNK_SIZE * coordinates.y) + coordinates.x + + _wall_orientation[index] = orientation + _wall_meshes[index] = mesh + +## +## Specialized multi-mesh instance convenience for use within the tile grid and its chunks. +## +class MultiMeshInstance: + var _instance_rid := RID() + + var _multimesh_rid := RID() + + func _init(scenario_rid: RID, mesh_rid: RID, transforms: Array) -> void: + _multimesh_rid = RenderingServer.multimesh_create() + _instance_rid = RenderingServer.instance_create2(_multimesh_rid, scenario_rid) + + RenderingServer.multimesh_set_mesh(_multimesh_rid, mesh_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]) + + ## + ## Sets the transform of the instance to [code]offset[/code]. + ## + func set_offset(offset: Transform3D) -> void: + RenderingServer.instance_set_transform(_instance_rid, offset) + + ## + ## Sets the visibility of the instance to [code]is_visible[/code]. + ## + func set_visible(is_visible: bool) -> void: + RenderingServer.instance_set_visible(_instance_rid, is_visible) + +var _chunks: Array[Chunk] = [] + +var _chunks_size := Vector2i.ZERO + +## +## Size of the tile grid (in engine units). +## +@export +var size: Vector2i: + set(value): + var chunk_size_factor := float(_CHUNK_SIZE) + + _chunks.resize(int(ceilf(value.x / chunk_size_factor) * ceilf(value.y / chunk_size_factor))) + + for i in _chunks.size(): + _chunks[i] = Chunk.new() + + _chunks_size = Vector2i((value / float(_CHUNK_SIZE)).ceil()) + size = value + +func _get_chunk(chunk_coordinate: Vector2i) -> Chunk: + return _chunks[(_chunks_size.x * chunk_coordinate.y) + chunk_coordinate.x] + +func _notification(what: int) -> void: + match what: + NOTIFICATION_TRANSFORM_CHANGED: + for chunk in _chunks: + for multi_mesh_instance in chunk._multi_mesh_instances: + multi_mesh_instance.set_offset_transform(global_transform) + + NOTIFICATION_VISIBILITY_CHANGED: + for chunk in _chunks: + for multi_mesh_instance in chunk._multi_mesh_instances: + multi_mesh_instance.set_visible(visible) + +## +## Clears the entirety of the tile grid to only contain tiles of instance [code]tile[/code] pointing +## in the orientation of [code]orientation[/code]. +## +## For clearing a specific region of the mesh grid, see [method fill_mesh]. +## +func clear_tiles(orientation: Orientation, tile: InteriorTile) -> void: + if tile == null: + for y in size.y: + for x in size.x: + var coordinates := Vector2i(x, y) + var chunk := _get_chunk(coordinates / _CHUNK_SIZE) + var chunk_coordinates := coordinates % _CHUNK_SIZE + + chunk.set_floor_mesh(chunk_coordinates, orientation, null) + chunk.set_wall_mesh(chunk_coordinates, orientation, null) + + else: + var mesh := tile.mesh + + match tile.kind: + InteriorTile.Kind.FLOOR: + for y in size.y: + for x in size.x: + var coordinate := Vector2i(x, y) + + _get_chunk(coordinate / _CHUNK_SIZE).\ + set_floor_mesh(coordinate % _CHUNK_SIZE, orientation, mesh) + + InteriorTile.Kind.WALL: + for y in size.y: + for x in size.x: + var coordinate := Vector2i(x, y) + + _get_chunk(coordinate / _CHUNK_SIZE).\ + set_floor_mesh(coordinate % _CHUNK_SIZE, orientation, mesh) + + for y in _chunks_size.y: + for x in _chunks_size.x: + var chunk_coordinate := Vector2i(x, y) + + _get_chunk(chunk_coordinate).invalidate(self, chunk_coordinate) + +## +## Clears the region of the tile grid at [code]area[/code] to only contain instances of +## [code]tile[/code] pointing in the orientation of [code]orientation[/code]. +## +## *Note* that [code]area[/code] *must* be within the area of the of the mesh grid, where +## [code]Vector2i.ZERO[/code] is the top-left and [member size] [code]- 1[/code] is the bottom- +## right. +## +## For clearing the whole of the mesh grid, see [method clear_mesh]. +## +func fill_tiles(area: Rect2i, orientation: Orientation, tile: InteriorTile) -> void: + assert(get_area().encloses(area), "area must be within grid") + + var filled_chunks := BitMap.new() + + filled_chunks.resize(_chunks_size) + + if tile == null: + for y in range(area.position.y, area.end.y): + for x in range(area.position.x, area.end.x): + var coordinates := Vector2i(x, y) + var chunk_coordinates := coordinates / _CHUNK_SIZE + var chunk := _get_chunk(coordinates / _CHUNK_SIZE) + var local_coordinates := coordinates % _CHUNK_SIZE + + chunk.set_floor_mesh(local_coordinates, orientation, null) + chunk.set_wall_mesh(local_coordinates, orientation, null) + filled_chunks.set_bitv(chunk_coordinates, true) + + else: + var mesh := tile.mesh + + if tile.fixed_orientation: + orientation = Orientation.NORTH + + match tile.kind: + InteriorTile.Kind.FLOOR: + for y in range(area.position.y, area.end.y): + for x in range(area.position.x, area.end.x): + var coordinate := Vector2i(x, y) + var chunk_coordinate := coordinate / _CHUNK_SIZE + + _get_chunk(chunk_coordinate).set_floor_mesh( + coordinate % _CHUNK_SIZE, orientation, mesh) + + filled_chunks.set_bitv(chunk_coordinate, true) + + InteriorTile.Kind.WALL: + for y in range(area.position.y, area.end.y): + for x in range(area.position.x, area.end.x): + var coordinate := Vector2i(x, y) + var chunk_coordinate := coordinate / _CHUNK_SIZE + + _get_chunk(chunk_coordinate).set_wall_mesh( + coordinate % _CHUNK_SIZE, orientation, mesh) + + filled_chunks.set_bitv(chunk_coordinate, true) + + for y in _chunks_size.y: + for x in _chunks_size.x: + if filled_chunks.get_bit(x, y): + var chunk_coordinate := Vector2i(x, y) + + _get_chunk(chunk_coordinate).invalidate(self, chunk_coordinate) + +## +## Returns the area of the interior map based on its current size. +## +func get_area() -> Rect2i: + return Rect2i(Vector2i.ZERO, size) + +## +## Plots a single tile at [code]coordinates[/code] to be an instance of [code]tile[/code] pointing +## in the orientation of [code]orientation[/code], overwriting whatever it previously contained. +## +## *Note* that [code]coordinates[/code] *must* be within the area of the of the mesh grid, where +## [code]Vector2i.ZERO[/code] is the top-left and [member size] [code]- 1[/code] is the bottom- +## right. +## +## For bulk-setting many meshes at once, see [method fill_mesh] and [method clear_mesh]. +## +func plot_tile(coordinates: Vector2i, orientation: Orientation, tile: InteriorTile) -> void: + assert(get_area().has_point(coordinates), "coordinate must be within grid") + + var chunk_coordinates := coordinates / _CHUNK_SIZE + var chunk := _get_chunk(chunk_coordinates) + + if tile == null: + var local_coordinates := coordinates % _CHUNK_SIZE + + chunk.set_floor_mesh(local_coordinates, orientation, null) + chunk.set_wall_mesh(local_coordinates, orientation, null) + chunk.invalidate(self, chunk_coordinates) + + else: + var mesh := tile.mesh + + if tile.fixed_orientation: + orientation = Orientation.NORTH + + match tile.kind: + InteriorTile.Kind.FLOOR: + chunk.set_floor_mesh(coordinates % _CHUNK_SIZE, orientation, mesh) + chunk.invalidate(self, chunk_coordinates) + + InteriorTile.Kind.WALL: + chunk.set_wall_mesh(coordinates % _CHUNK_SIZE, orientation, mesh) + chunk.invalidate(self, chunk_coordinates) diff --git a/interior/interior_tile.gd b/interior/interior_tile.gd new file mode 100644 index 0000000..7668d34 --- /dev/null +++ b/interior/interior_tile.gd @@ -0,0 +1,29 @@ +class_name InteriorTile extends Resource + +## +## Identifier for the kind of interior tile. +## +enum Kind { + FLOOR, + WALL, +} + +## +## Flag for determining if the tile is affected by orientations specified in an [InteriorMap]. +## +@export +var fixed_orientation := false + +## +## See [enum Kind]. +## +## Interior tiles of different kinds will not conflict when placing them. +## +@export +var kind := Kind.FLOOR + +## +## Used to visualize the interior tile. +## +@export +var mesh: Mesh = null diff --git a/interior/tiles/dungeon_01_pipehole_tile.res b/interior/tiles/dungeon_01_pipehole_tile.res new file mode 100644 index 0000000..f684c7c --- /dev/null +++ b/interior/tiles/dungeon_01_pipehole_tile.res @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50d1e57c78aa55883394de1f91ff5aa507a2e3a68c4289dd9c3c4294dd0aa6dc +size 131987 diff --git a/interior/tiles/dungeon_01_tiling_tile.res b/interior/tiles/dungeon_01_tiling_tile.res new file mode 100644 index 0000000..2cafb06 --- /dev/null +++ b/interior/tiles/dungeon_01_tiling_tile.res @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:935de0b7ca77b8e921044bda3ad3db0de8eaee338719927a39a782e0579bc16c +size 479 diff --git a/interior/tiles/limestone_dungeon/hole_albedo.png b/interior/tiles/limestone_dungeon/hole_albedo.png new file mode 100755 index 0000000..9522f38 --- /dev/null +++ b/interior/tiles/limestone_dungeon/hole_albedo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65be5005d966729b3249802ba1ee1b075c7f03b14376e2e6a81a3b015d85e884 +size 144301 diff --git a/interior/tiles/limestone_dungeon/hole_albedo.png.import b/interior/tiles/limestone_dungeon/hole_albedo.png.import new file mode 100644 index 0000000..48d5411 --- /dev/null +++ b/interior/tiles/limestone_dungeon/hole_albedo.png.import @@ -0,0 +1,36 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://3m7gvnsmb4g" +path.s3tc="res://.godot/imported/hole_albedo.png-ef88dfc602c85c58e849d6654804eb67.s3tc.ctex" +path.etc2="res://.godot/imported/hole_albedo.png-ef88dfc602c85c58e849d6654804eb67.etc2.ctex" +metadata={ +"imported_formats": ["s3tc", "etc2"], +"vram_texture": true +} + +[deps] + +source_file="res://interior/tiles/limestone_dungeon/hole_albedo.png" +dest_files=["res://.godot/imported/hole_albedo.png-ef88dfc602c85c58e849d6654804eb67.s3tc.ctex", "res://.godot/imported/hole_albedo.png-ef88dfc602c85c58e849d6654804eb67.etc2.ctex"] + +[params] + +compress/mode=2 +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/bptc_ldr=0 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +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=0 diff --git a/interior/tiles/limestone_dungeon/hole_height.png b/interior/tiles/limestone_dungeon/hole_height.png new file mode 100755 index 0000000..22ac595 --- /dev/null +++ b/interior/tiles/limestone_dungeon/hole_height.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f37d7351c25c1d701726f311957dccd380f74dd918f12f328674467a3a552044 +size 113592 diff --git a/interior/tiles/limestone_dungeon/hole_height.png.import b/interior/tiles/limestone_dungeon/hole_height.png.import new file mode 100644 index 0000000..69b8b57 --- /dev/null +++ b/interior/tiles/limestone_dungeon/hole_height.png.import @@ -0,0 +1,36 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c8p7qvcrfsio7" +path.s3tc="res://.godot/imported/hole_height.png-875fd265b9470b3f52656ba603fc8eae.s3tc.ctex" +path.etc2="res://.godot/imported/hole_height.png-875fd265b9470b3f52656ba603fc8eae.etc2.ctex" +metadata={ +"imported_formats": ["s3tc", "etc2"], +"vram_texture": true +} + +[deps] + +source_file="res://interior/tiles/limestone_dungeon/hole_height.png" +dest_files=["res://.godot/imported/hole_height.png-875fd265b9470b3f52656ba603fc8eae.s3tc.ctex", "res://.godot/imported/hole_height.png-875fd265b9470b3f52656ba603fc8eae.etc2.ctex"] + +[params] + +compress/mode=2 +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/bptc_ldr=0 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +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=0 diff --git a/interior/tiles/limestone_dungeon/hole_material.res b/interior/tiles/limestone_dungeon/hole_material.res new file mode 100644 index 0000000..3f994c5 --- /dev/null +++ b/interior/tiles/limestone_dungeon/hole_material.res @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac4a618b5ae0291a16ed9b957943e273ed841076bc48cb3df7d2f206de528b25 +size 1098 diff --git a/interior/tiles/limestone_dungeon/hole_normal.png b/interior/tiles/limestone_dungeon/hole_normal.png new file mode 100755 index 0000000..fac6c12 --- /dev/null +++ b/interior/tiles/limestone_dungeon/hole_normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4db5e7fa31cc065fa3fbac525c04ddf74824d5e4f7a891c5aa84689fb530ffb9 +size 343721 diff --git a/interior/tiles/limestone_dungeon/hole_normal.png.import b/interior/tiles/limestone_dungeon/hole_normal.png.import new file mode 100644 index 0000000..bf6c59b --- /dev/null +++ b/interior/tiles/limestone_dungeon/hole_normal.png.import @@ -0,0 +1,36 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c4eby3i7q2wts" +path.s3tc="res://.godot/imported/hole_normal.png-9c2f1eac05804bab609d1c67adcf7dba.s3tc.ctex" +path.etc2="res://.godot/imported/hole_normal.png-9c2f1eac05804bab609d1c67adcf7dba.etc2.ctex" +metadata={ +"imported_formats": ["s3tc", "etc2"], +"vram_texture": true +} + +[deps] + +source_file="res://interior/tiles/limestone_dungeon/hole_normal.png" +dest_files=["res://.godot/imported/hole_normal.png-9c2f1eac05804bab609d1c67adcf7dba.s3tc.ctex", "res://.godot/imported/hole_normal.png-9c2f1eac05804bab609d1c67adcf7dba.etc2.ctex"] + +[params] + +compress/mode=2 +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/bptc_ldr=0 +compress/normal_map=1 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=1 +roughness/src_normal="res://tiles/tiles/limestone_dungeon/hole_normal.png" +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=0 diff --git a/interior/tiles/limestone_dungeon/hole_orm.png b/interior/tiles/limestone_dungeon/hole_orm.png new file mode 100644 index 0000000..b7800a2 --- /dev/null +++ b/interior/tiles/limestone_dungeon/hole_orm.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9bd0e2cf8d124f047a835fc143563107b41b0a45a427d1f767d8be5d7470db24 +size 56105 diff --git a/interior/tiles/limestone_dungeon/hole_orm.png.import b/interior/tiles/limestone_dungeon/hole_orm.png.import new file mode 100644 index 0000000..39cf7c3 --- /dev/null +++ b/interior/tiles/limestone_dungeon/hole_orm.png.import @@ -0,0 +1,36 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dvqqmbf6r1jfm" +path.s3tc="res://.godot/imported/hole_orm.png-00b1427da2cca528c854a97655dfff24.s3tc.ctex" +path.etc2="res://.godot/imported/hole_orm.png-00b1427da2cca528c854a97655dfff24.etc2.ctex" +metadata={ +"imported_formats": ["s3tc", "etc2"], +"vram_texture": true +} + +[deps] + +source_file="res://interior/tiles/limestone_dungeon/hole_orm.png" +dest_files=["res://.godot/imported/hole_orm.png-00b1427da2cca528c854a97655dfff24.s3tc.ctex", "res://.godot/imported/hole_orm.png-00b1427da2cca528c854a97655dfff24.etc2.ctex"] + +[params] + +compress/mode=2 +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/bptc_ldr=0 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +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=0 diff --git a/interior/tiles/limestone_dungeon/piping_albedo.png b/interior/tiles/limestone_dungeon/piping_albedo.png new file mode 100755 index 0000000..ff4666c --- /dev/null +++ b/interior/tiles/limestone_dungeon/piping_albedo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d289f998cf9317afa37ed8519a2a7ab06638aa92a8ce57dc242cf20b60066b36 +size 623939 diff --git a/interior/tiles/limestone_dungeon/piping_albedo.png.import b/interior/tiles/limestone_dungeon/piping_albedo.png.import new file mode 100644 index 0000000..ba5e271 --- /dev/null +++ b/interior/tiles/limestone_dungeon/piping_albedo.png.import @@ -0,0 +1,36 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://yu0p5ryoh7gr" +path.s3tc="res://.godot/imported/piping_albedo.png-e55409f9baf27938796d954b957086ac.s3tc.ctex" +path.etc2="res://.godot/imported/piping_albedo.png-e55409f9baf27938796d954b957086ac.etc2.ctex" +metadata={ +"imported_formats": ["s3tc", "etc2"], +"vram_texture": true +} + +[deps] + +source_file="res://interior/tiles/limestone_dungeon/piping_albedo.png" +dest_files=["res://.godot/imported/piping_albedo.png-e55409f9baf27938796d954b957086ac.s3tc.ctex", "res://.godot/imported/piping_albedo.png-e55409f9baf27938796d954b957086ac.etc2.ctex"] + +[params] + +compress/mode=2 +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/bptc_ldr=0 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +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=0 diff --git a/interior/tiles/limestone_dungeon/piping_height.png b/interior/tiles/limestone_dungeon/piping_height.png new file mode 100755 index 0000000..5469c2e --- /dev/null +++ b/interior/tiles/limestone_dungeon/piping_height.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da8a57606972d7c93e21f14d20b08a35f3af7618fd76cc27db650f789db013f2 +size 241457 diff --git a/interior/tiles/limestone_dungeon/piping_height.png.import b/interior/tiles/limestone_dungeon/piping_height.png.import new file mode 100644 index 0000000..2bf651a --- /dev/null +++ b/interior/tiles/limestone_dungeon/piping_height.png.import @@ -0,0 +1,36 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://byw6qkgod61ip" +path.s3tc="res://.godot/imported/piping_height.png-d458ffb714345b35977ee0de9b01359f.s3tc.ctex" +path.etc2="res://.godot/imported/piping_height.png-d458ffb714345b35977ee0de9b01359f.etc2.ctex" +metadata={ +"imported_formats": ["s3tc", "etc2"], +"vram_texture": true +} + +[deps] + +source_file="res://interior/tiles/limestone_dungeon/piping_height.png" +dest_files=["res://.godot/imported/piping_height.png-d458ffb714345b35977ee0de9b01359f.s3tc.ctex", "res://.godot/imported/piping_height.png-d458ffb714345b35977ee0de9b01359f.etc2.ctex"] + +[params] + +compress/mode=2 +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/bptc_ldr=0 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +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=0 diff --git a/interior/tiles/limestone_dungeon/piping_material.res b/interior/tiles/limestone_dungeon/piping_material.res new file mode 100644 index 0000000..e6bc1cc --- /dev/null +++ b/interior/tiles/limestone_dungeon/piping_material.res @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67ef7e571aa11cd7b9e57a14977af2fcc656aefb5ed8c0f2de3fb7969565be16 +size 1097 diff --git a/interior/tiles/limestone_dungeon/piping_normal.png b/interior/tiles/limestone_dungeon/piping_normal.png new file mode 100755 index 0000000..fca4ab2 --- /dev/null +++ b/interior/tiles/limestone_dungeon/piping_normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:028cb62c715456b29a27a5b306e7c66b61525f5fd90799df79587f8381499030 +size 755707 diff --git a/interior/tiles/limestone_dungeon/piping_normal.png.import b/interior/tiles/limestone_dungeon/piping_normal.png.import new file mode 100644 index 0000000..1ed7908 --- /dev/null +++ b/interior/tiles/limestone_dungeon/piping_normal.png.import @@ -0,0 +1,36 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bypqkn1tm036p" +path.s3tc="res://.godot/imported/piping_normal.png-4b6b5fc6d6cbc5e05f054f9e19463644.s3tc.ctex" +path.etc2="res://.godot/imported/piping_normal.png-4b6b5fc6d6cbc5e05f054f9e19463644.etc2.ctex" +metadata={ +"imported_formats": ["s3tc", "etc2"], +"vram_texture": true +} + +[deps] + +source_file="res://interior/tiles/limestone_dungeon/piping_normal.png" +dest_files=["res://.godot/imported/piping_normal.png-4b6b5fc6d6cbc5e05f054f9e19463644.s3tc.ctex", "res://.godot/imported/piping_normal.png-4b6b5fc6d6cbc5e05f054f9e19463644.etc2.ctex"] + +[params] + +compress/mode=2 +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/bptc_ldr=0 +compress/normal_map=1 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=1 +roughness/src_normal="res://tiles/tiles/limestone_dungeon/piping_normal.png" +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=0 diff --git a/interior/tiles/limestone_dungeon/piping_orm.png b/interior/tiles/limestone_dungeon/piping_orm.png new file mode 100644 index 0000000..50c5416 --- /dev/null +++ b/interior/tiles/limestone_dungeon/piping_orm.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d453be063b9e2f82aca00deb9ac54df40ffe4fe55fd59b8078be7417f354e7d6 +size 109618 diff --git a/interior/tiles/limestone_dungeon/piping_orm.png.import b/interior/tiles/limestone_dungeon/piping_orm.png.import new file mode 100644 index 0000000..7f8fade --- /dev/null +++ b/interior/tiles/limestone_dungeon/piping_orm.png.import @@ -0,0 +1,36 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bkpucv0ydae5v" +path.s3tc="res://.godot/imported/piping_orm.png-7131df07403ecb346d6a4793ca7929cb.s3tc.ctex" +path.etc2="res://.godot/imported/piping_orm.png-7131df07403ecb346d6a4793ca7929cb.etc2.ctex" +metadata={ +"imported_formats": ["s3tc", "etc2"], +"vram_texture": true +} + +[deps] + +source_file="res://interior/tiles/limestone_dungeon/piping_orm.png" +dest_files=["res://.godot/imported/piping_orm.png-7131df07403ecb346d6a4793ca7929cb.s3tc.ctex", "res://.godot/imported/piping_orm.png-7131df07403ecb346d6a4793ca7929cb.etc2.ctex"] + +[params] + +compress/mode=2 +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/bptc_ldr=0 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +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=0 diff --git a/interior/tiles/limestone_dungeon/tiling_albedo.png b/interior/tiles/limestone_dungeon/tiling_albedo.png new file mode 100755 index 0000000..8ff1619 --- /dev/null +++ b/interior/tiles/limestone_dungeon/tiling_albedo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:770bae54ae4a1152791a26dfd1f38e0a0201cf3ec36ed85fe6f8122de4e76ed0 +size 1488729 diff --git a/interior/tiles/limestone_dungeon/tiling_albedo.png.import b/interior/tiles/limestone_dungeon/tiling_albedo.png.import new file mode 100644 index 0000000..5238382 --- /dev/null +++ b/interior/tiles/limestone_dungeon/tiling_albedo.png.import @@ -0,0 +1,36 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bydc62557j2d0" +path.s3tc="res://.godot/imported/tiling_albedo.png-1570a8acdedf3400258f8b15e49e5d5b.s3tc.ctex" +path.etc2="res://.godot/imported/tiling_albedo.png-1570a8acdedf3400258f8b15e49e5d5b.etc2.ctex" +metadata={ +"imported_formats": ["s3tc", "etc2"], +"vram_texture": true +} + +[deps] + +source_file="res://interior/tiles/limestone_dungeon/tiling_albedo.png" +dest_files=["res://.godot/imported/tiling_albedo.png-1570a8acdedf3400258f8b15e49e5d5b.s3tc.ctex", "res://.godot/imported/tiling_albedo.png-1570a8acdedf3400258f8b15e49e5d5b.etc2.ctex"] + +[params] + +compress/mode=2 +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/bptc_ldr=0 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +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=0 diff --git a/interior/tiles/limestone_dungeon/tiling_height.png b/interior/tiles/limestone_dungeon/tiling_height.png new file mode 100755 index 0000000..44b0087 --- /dev/null +++ b/interior/tiles/limestone_dungeon/tiling_height.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c43403b399253578c9e2bfd1b686d4f45fe45712be7de31405fbd53c40b70c43 +size 419947 diff --git a/interior/tiles/limestone_dungeon/tiling_height.png.import b/interior/tiles/limestone_dungeon/tiling_height.png.import new file mode 100644 index 0000000..7090396 --- /dev/null +++ b/interior/tiles/limestone_dungeon/tiling_height.png.import @@ -0,0 +1,36 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://didp3nfacohwd" +path.s3tc="res://.godot/imported/tiling_height.png-2ead72b43d4a6de847e50bcbd8e8c7f7.s3tc.ctex" +path.etc2="res://.godot/imported/tiling_height.png-2ead72b43d4a6de847e50bcbd8e8c7f7.etc2.ctex" +metadata={ +"imported_formats": ["s3tc", "etc2"], +"vram_texture": true +} + +[deps] + +source_file="res://interior/tiles/limestone_dungeon/tiling_height.png" +dest_files=["res://.godot/imported/tiling_height.png-2ead72b43d4a6de847e50bcbd8e8c7f7.s3tc.ctex", "res://.godot/imported/tiling_height.png-2ead72b43d4a6de847e50bcbd8e8c7f7.etc2.ctex"] + +[params] + +compress/mode=2 +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/bptc_ldr=0 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=7 +roughness/src_normal="res://tiles/tiles/limestone_dungeon/tiling_normal.png" +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=0 diff --git a/interior/tiles/limestone_dungeon/tiling_material.res b/interior/tiles/limestone_dungeon/tiling_material.res new file mode 100644 index 0000000..49762db --- /dev/null +++ b/interior/tiles/limestone_dungeon/tiling_material.res @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e917b37d179fb8c78deaf4a2003fa07e8d023aab38fa84119e9af9bcab09422c +size 1096 diff --git a/interior/tiles/limestone_dungeon/tiling_normal.png b/interior/tiles/limestone_dungeon/tiling_normal.png new file mode 100755 index 0000000..329738b --- /dev/null +++ b/interior/tiles/limestone_dungeon/tiling_normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa6c031538c5459b5394d5e8ef7bfe5a58d8e5da62013ea4b3c9c5c54cdbeebd +size 1264636 diff --git a/interior/tiles/limestone_dungeon/tiling_normal.png.import b/interior/tiles/limestone_dungeon/tiling_normal.png.import new file mode 100644 index 0000000..bcc28d9 --- /dev/null +++ b/interior/tiles/limestone_dungeon/tiling_normal.png.import @@ -0,0 +1,36 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cv8yiluvc6mol" +path.s3tc="res://.godot/imported/tiling_normal.png-877f08b7359b5aba3ea7d5681fd4e5a9.s3tc.ctex" +path.etc2="res://.godot/imported/tiling_normal.png-877f08b7359b5aba3ea7d5681fd4e5a9.etc2.ctex" +metadata={ +"imported_formats": ["s3tc", "etc2"], +"vram_texture": true +} + +[deps] + +source_file="res://interior/tiles/limestone_dungeon/tiling_normal.png" +dest_files=["res://.godot/imported/tiling_normal.png-877f08b7359b5aba3ea7d5681fd4e5a9.s3tc.ctex", "res://.godot/imported/tiling_normal.png-877f08b7359b5aba3ea7d5681fd4e5a9.etc2.ctex"] + +[params] + +compress/mode=2 +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/bptc_ldr=0 +compress/normal_map=1 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=1 +roughness/src_normal="res://tiles/tiles/limestone_dungeon/tiling_normal.png" +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=0 diff --git a/interior/tiles/limestone_dungeon/tiling_orm.png b/interior/tiles/limestone_dungeon/tiling_orm.png new file mode 100644 index 0000000..5a4878f --- /dev/null +++ b/interior/tiles/limestone_dungeon/tiling_orm.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94cc0c47f71b977eaa12a70147cf85b48b51cbf0106e984d6e62a056a9e1c0d4 +size 431771 diff --git a/interior/tiles/limestone_dungeon/tiling_orm.png.import b/interior/tiles/limestone_dungeon/tiling_orm.png.import new file mode 100644 index 0000000..cd95373 --- /dev/null +++ b/interior/tiles/limestone_dungeon/tiling_orm.png.import @@ -0,0 +1,36 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://leoab2fjmxgk" +path.s3tc="res://.godot/imported/tiling_orm.png-48bc1a5d8b2eae43d49ed818fa664d0d.s3tc.ctex" +path.etc2="res://.godot/imported/tiling_orm.png-48bc1a5d8b2eae43d49ed818fa664d0d.etc2.ctex" +metadata={ +"imported_formats": ["s3tc", "etc2"], +"vram_texture": true +} + +[deps] + +source_file="res://interior/tiles/limestone_dungeon/tiling_orm.png" +dest_files=["res://.godot/imported/tiling_orm.png-48bc1a5d8b2eae43d49ed818fa664d0d.s3tc.ctex", "res://.godot/imported/tiling_orm.png-48bc1a5d8b2eae43d49ed818fa664d0d.etc2.ctex"] + +[params] + +compress/mode=2 +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/bptc_ldr=0 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +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=0 diff --git a/local_player.scn b/local_player.scn new file mode 100644 index 0000000..f21c4f3 --- /dev/null +++ b/local_player.scn @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4f0319e09b163eb3c6519b5c72c18ebfa85aac35304cf6b3530f2c0d679c73b +size 679 diff --git a/map_editor.scn b/map_editor.scn index ee8ad61..75269bf 100644 --- a/map_editor.scn +++ b/map_editor.scn @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e4df576b189d644adb94f3fb8f29b9eb20ca41226e3a7b40b979aea2835b18c -size 8510 +oid sha256:d40ab3a5c636b299cab66b325eebee44880a13467cd078577f39a72a9a7f05ab +size 13276 diff --git a/map_editor/brush_selection_button_group.res b/map_editor/brush_selection_button_group.res new file mode 100644 index 0000000..b00a106 --- /dev/null +++ b/map_editor/brush_selection_button_group.res @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0755c5adedfce65aecf47ce200085de76ed8801f7a17ae387c04fbbec483eba +size 212 diff --git a/map_editor/camera_boundary_mesh.res b/map_editor/camera_boundary_mesh.res index 673c51f..c478941 100644 --- a/map_editor/camera_boundary_mesh.res +++ b/map_editor/camera_boundary_mesh.res @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce40e96135058eaaf87b5188862dc31e71c6e11a53f5afce3cffccf4320208d6 -size 645 +oid sha256:28046c63a066c0a2119714133b8aae4216b3dbd694749d3e8467d362c2cf74c5 +size 643 diff --git a/map_editor/edit_mode_button_group.res b/map_editor/edit_action_button_group.res similarity index 100% rename from map_editor/edit_mode_button_group.res rename to map_editor/edit_action_button_group.res diff --git a/map_editor/map_editor_menu.gd b/map_editor/map_editor_menu.gd new file mode 100644 index 0000000..38eddfd --- /dev/null +++ b/map_editor/map_editor_menu.gd @@ -0,0 +1,7 @@ +class_name MapEditorMenu extends VBoxContainer + +## +## Resets the state of the menu. +## +func reset() -> void: + pass diff --git a/map_editor/menus_theme.res b/map_editor/menus_theme.res index d240659..57edacb 100644 --- a/map_editor/menus_theme.res +++ b/map_editor/menus_theme.res @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16a64c4d989cb97515aeb631c435db2c29388fb701e9e513d26692d3bba10f65 -size 847 +oid sha256:313a4fd4690a9521a50eda4bb8e1776031c27d1a0ee411c31185c9ea6f84aedc +size 886 diff --git a/map_editor/paint_selector_button_group.res b/map_editor/paint_selector_button_group.res new file mode 100644 index 0000000..1c80674 --- /dev/null +++ b/map_editor/paint_selector_button_group.res @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a62c0405f427f5da97173b050f0baba2aab0b093b45fc283749ee252aab47f6e +size 208 diff --git a/map_editor/selection_area_mesh.res b/map_editor/selection_area_mesh.res new file mode 100644 index 0000000..d0f15ee --- /dev/null +++ b/map_editor/selection_area_mesh.res @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d30d1595da7c7bab44c9e72b88f8e0bfe946dff33e1a4903ab772104cd43657d +size 894 diff --git a/map_editor/structure_button_group.res b/map_editor/structure_button_group.res new file mode 100644 index 0000000..6097737 --- /dev/null +++ b/map_editor/structure_button_group.res @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6c88793bdb0c4c2c80b7a913b1b3fc98cea238405973ca04709f9c16bec1903 +size 199 diff --git a/map_editor/tile_cursor_object_material.res b/map_editor/tile_cursor_object_material.res new file mode 100644 index 0000000..b053994 --- /dev/null +++ b/map_editor/tile_cursor_object_material.res @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be95f7bcacaedc1e942b999a970ce648cb7e4efdf04a16b5c9c4f6ff95039f54 +size 708 diff --git a/map_editor/tile_selector_button_group.res b/map_editor/tile_selector_button_group.res new file mode 100644 index 0000000..15ec28f --- /dev/null +++ b/map_editor/tile_selector_button_group.res @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac378ba65f50a7aef2a17925716d12bc0d5074d063f6e6a42de46e8a1c36a846 +size 209 diff --git a/player_controller.gd b/player_controller.gd index 5eb9353..4d62cbb 100644 --- a/player_controller.gd +++ b/player_controller.gd @@ -1,5 +1,16 @@ class_name PlayerController extends Node3D +## +## Selection action performed. +## +## [code]PRIMARY[/code] refers to the default selection action, while [code]SECONDARY[/code] refers +## to the secondary selection action. +## +enum SelectAction { + PRIMARY, + SECONDARY, +} + ## ## Supported selection input devices. ## @@ -14,9 +25,12 @@ enum SelectMode { signal select_mode_changed(mode: SelectMode) ## -## Selection of a point on screenspace has started happening. +## Selection of a point on the screen space has started happening, with [code]select_action[/code] +## as selection action performed. ## -signal selection_started() +## See [enum SelectAction] for more details. +## +signal selection_started(select_action: SelectAction) ## ## Selection of a point on screenspace has stopped happening. @@ -37,6 +51,8 @@ const _ROTATE_CW := "player_controller_rotate_cw" const _DRAG_SPEED_BASE_MODIFIER := 0.15 +const _MOVE_SMOOTHING := 0.5 + const _MOVE_SPEED_BASE_MODIFIER := 50.0 const _ROTATE_SPEED_BASE := 5.0 @@ -46,134 +62,111 @@ const _TRANSFORM_DELTA := 10.0 @export var _camera: Camera3D = null +var _control_override_count := 0 + var _cursor_point := Vector2.ZERO var _is_drag_panning := false -var _is_selecting := false - var _select_mode := SelectMode.NONE @export var _selection_area: Control = null @onready -var _target_position := self.position +var _target_position := position @onready -var _target_orientation := self.global_rotation.y - -## -## Smoothness applies to the interpolation toward rotation and movement target values. -## -@export -var movement_smoothing := 0.5 - -## -## Whether or not player movement input processed by the controller should be ignored. -## -@export -var frozen := false - -func _input(event: InputEvent) -> void: - if event is InputEventMouseButton: - match event.button_index: - MOUSE_BUTTON_MIDDLE: - self._is_drag_panning = event.is_pressed() and not(self.frozen) - - Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if\ - self._is_drag_panning else Input.MOUSE_MODE_VISIBLE - - MOUSE_BUTTON_LEFT: - self._is_selecting = event.is_pressed() - - if self._is_selecting: - self.selection_started.emit() - - else: - self.selection_stopped.emit() - - return - - if (event is InputEventMouseMotion) and self._is_drag_panning: - var global_basis := self.global_transform.basis - var camera_settings := GameSettings.camera_settings - var dampened_speed := camera_settings.movement_speed_modifier * _DRAG_SPEED_BASE_MODIFIER - - self._target_position += dampened_speed.y * (-global_basis.z) *\ - event.relative.y * (-1.0 if camera_settings.is_y_inverted else 1.0) - - self._target_position += dampened_speed.x * (-global_basis.x) *\ - event.relative.x * (-1.0 if camera_settings.is_x_inverted else 1.0) - - return - - if event is InputEventScreenDrag: - return - - if event is InputEventScreenTouch: - return +var _target_orientation := global_rotation.y func _process(delta: float) -> void: - if not(self.frozen): - var global_basis := self.global_transform.basis + if not(is_frozen()): + var global_basis := global_transform.basis var camera_settings := GameSettings.camera_settings var delta_speed :=\ camera_settings.movement_speed_modifier * _MOVE_SPEED_BASE_MODIFIER * delta - self._target_position += delta_speed.y * (-global_basis.z) *\ + _target_position += delta_speed.y * (-global_basis.z) *\ (Input.get_action_strength(_FORWARD) - Input.get_action_strength(_BACKWARD)) *\ (-1.0 if camera_settings.is_y_inverted else 1.0) - self._target_position += delta_speed.x * (-global_basis.x) *\ + _target_position += delta_speed.x * (-global_basis.x) *\ (Input.get_action_strength(_LEFT) - Input.get_action_strength(_RIGHT)) *\ (-1.0 if camera_settings.is_y_inverted else 1.0) - self._target_orientation += (Input.get_action_strength(_ROTATE_CCW) -\ + _target_orientation += (Input.get_action_strength(_ROTATE_CCW) -\ Input.get_action_strength(_ROTATE_CW)) * _ROTATE_SPEED_BASE *\ camera_settings.rotation_speed_modifier * delta - self.global_transform = self.global_transform.interpolate_with( - Transform3D(Basis(Vector3.UP, self._target_orientation), self._target_position), - delta * _TRANSFORM_DELTA * self.movement_smoothing) + global_transform = global_transform.interpolate_with( + Transform3D(Basis(Vector3.UP, _target_orientation), _target_position), + delta * _TRANSFORM_DELTA * _MOVE_SMOOTHING) - match self._select_mode: + match _select_mode: SelectMode.NONE: - self._cursor_point = self.get_viewport().size / 2.0 + _cursor_point = get_viewport().size / 2.0 SelectMode.MOUSE: - self._cursor_point = self.get_viewport().get_mouse_position() + _cursor_point = get_viewport().get_mouse_position() func _ready() -> void: - if self._selection_area != null: - self._selection_area.mouse_entered.connect(func () -> void: - if self._select_mode != SelectMode.MOUSE: - self._select_mode = SelectMode.MOUSE + if _selection_area != null: + _selection_area.mouse_entered.connect(func () -> void: + if _select_mode != SelectMode.MOUSE: + _select_mode = SelectMode.MOUSE - self.select_mode_changed.emit(SelectMode.MOUSE)) + select_mode_changed.emit(SelectMode.MOUSE)) - self._selection_area.mouse_exited.connect(func () -> void: - if self._select_mode != SelectMode.NONE: - self._select_mode = SelectMode.NONE + _selection_area.mouse_exited.connect(func () -> void: + if _select_mode != SelectMode.NONE: + _select_mode = SelectMode.NONE - self.select_mode_changed.emit(SelectMode.NONE)) + select_mode_changed.emit(SelectMode.NONE)) -## -## -## -func in_select_area() -> bool: - return self._select_mode != SelectMode.NONE + _selection_area.gui_input.connect(func (event: InputEvent) -> void: + if event is InputEventMouseButton: + match event.button_index: + MOUSE_BUTTON_MIDDLE: + _is_drag_panning = event.is_pressed() and not(is_frozen()) -## -## Returns [code]true[/code] if the player controller is currently selecting a location on the -## screen, otherwise [code]false[/code]. -## -## *Note* that it is discouraged that this be continuously polled for single-fire events. Instead, -## see [signal selection_started] and [signal selection_stopped]. -## -func is_selecting() -> bool: - return self._is_selecting + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if\ + _is_drag_panning else Input.MOUSE_MODE_VISIBLE + + MOUSE_BUTTON_LEFT: + if event.is_pressed(): + selection_started.emit(SelectAction.PRIMARY) + + else: + selection_stopped.emit() + + MOUSE_BUTTON_RIGHT: + if event.is_pressed(): + selection_started.emit(SelectAction.SECONDARY) + + else: + selection_stopped.emit() + + return + + if (event is InputEventMouseMotion) and _is_drag_panning: + var global_basis := global_transform.basis + var camera_settings := GameSettings.camera_settings + var dampened_speed := camera_settings.movement_speed_modifier * _DRAG_SPEED_BASE_MODIFIER + + _target_position += dampened_speed.y * (-global_basis.z) *\ + event.relative.y * (-1.0 if camera_settings.is_y_inverted else 1.0) + + _target_position += dampened_speed.x * (-global_basis.x) *\ + event.relative.x * (-1.0 if camera_settings.is_x_inverted else 1.0) + + return + + if event is InputEventScreenDrag: + return + + if event is InputEventScreenTouch: + return) ## ## Returns the position of the selection cursor with respect to the select current mode being used. @@ -182,22 +175,34 @@ func is_selecting() -> bool: ## touchscreen interactions, this will be the location of an active gesture. Finally, for all other ## states - including gamepad - this will be the middle of screenspace at all times. ## -func get_cursor_point() -> Vector2: - return self._cursor_point +func get_plane_cursor() -> Vector2: + if _camera != null: + var plane_cursor = Plane(Vector3.UP, 0.0).intersects_ray( + _camera.global_position, _camera.project_ray_normal(_cursor_point)) + + if plane_cursor is Vector3: + return Vector2(plane_cursor.x, plane_cursor.z) + + return Vector2.ZERO ## -## Attempts to convert the screen coordinates in [code]screen_point[/code] into 2D worldspace -## coordinate relative to an infinite ground plane at y offset [code]0.0[/code]. +## Returns [code]true[/code] if the player controller is currently frozen, otherwise +## [code]false[/code]. ## -## Successful point intersection will return the plane coordinates in a [class Vector2], otherwise -## [code]null[/code] is returned. +func is_frozen() -> bool: + assert(_control_override_count > -1, "control override count cannot be less than 0") + + return _control_override_count != 0 + ## -func screen_to_plane(screen_point: Vector2): - if self._camera != null: - var plane_target = Plane(Vector3.UP, 0.0).intersects_ray( - self._camera.global_position, self._camera.project_ray_normal(screen_point)) +## Overrides the controls of the player controller. When [code]unlock_signal[/code] is emitted, the +## override is released. +## +func override_controls(unlock_signal: Signal) -> void: + assert(_control_override_count > -1, "control override count cannot be less than 0") - if plane_target is Vector3: - return Vector2(plane_target.x, plane_target.z) + _control_override_count += 1 - return null + unlock_signal.connect(func () -> void: + assert(_control_override_count > 0, "control override count cannot be less than 1") + _control_override_count -= 1, Object.CONNECT_ONE_SHOT) diff --git a/project.godot b/project.godot index 233e611..230da9b 100644 --- a/project.godot +++ b/project.godot @@ -8,60 +8,23 @@ config_version=5 -_global_script_classes=[{ -"base": "HFlowContainer", -"class": &"ItemSelection", -"language": &"GDScript", -"path": "res://user_interface/button_selection.gd" -}, { -"base": "Node", -"class": &"MapEditorTerrainCanvas", -"language": &"GDScript", -"path": "res://map_editor/map_editor_terrain_canvas.gd" -}, { -"base": "Node3D", -"class": &"PlayerController", -"language": &"GDScript", -"path": "res://player_controller.gd" -}, { -"base": "Node", -"class": &"Settings", -"language": &"GDScript", -"path": "res://settings.gd" -}, { -"base": "GeometryInstance3D", -"class": &"TerrainInstance3D", -"language": &"GDScript", -"path": "res://terrain/terrain_instance_3d.gd" -}, { -"base": "Resource", -"class": &"TerrainPaint", -"language": &"GDScript", -"path": "res://terrain/paints/terrain_paint.gd" -}] -_global_script_class_icons={ -"ItemSelection": "", -"MapEditorTerrainCanvas": "", -"PlayerController": "", -"Settings": "", -"TerrainInstance3D": "", -"TerrainPaint": "" -} - [application] config/name="Protectorate" run/main_scene="res://map_editor.scn" config/use_custom_user_dir=true config/features=PackedStringArray("4.0", "Forward Plus") +boot_splash/show_image=false config/icon="res://icon.png" [autoload] GameSettings="*res://settings.gd" +LocalPlayer="*res://local_player.scn" [debug] +gdscript/warnings/integer_division=0 gdscript/warnings/assert_always_true=0 gdscript/warnings/assert_always_false=0 @@ -85,32 +48,32 @@ enabled=PackedStringArray() player_controller_left={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"unicode":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":0,"echo":false,"script":null) ] } player_controller_right={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"unicode":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":0,"echo":false,"script":null) ] } player_controller_forward={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"unicode":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":0,"echo":false,"script":null) ] } player_controller_backward={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"unicode":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":0,"echo":false,"script":null) ] } player_controller_rotate_cw={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"unicode":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":0,"echo":false,"script":null) ] } player_controller_rotate_ccw={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":81,"unicode":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":81,"key_label":0,"unicode":0,"echo":false,"script":null) ] } editor_paint={ @@ -119,6 +82,15 @@ editor_paint={ ] } +[layer_names] + +3d_render/layer_1="Physical" +3d_render/layer_2="Interface" + +[physics] + +common/physics_ticks_per_second=30 + [rendering] lights_and_shadows/directional_shadow/soft_shadow_filter_quality=3 diff --git a/terrain/paints/default_terrain_paint.res b/terrain/paints/default_terrain_paint.res index e8202f5..fb94070 100644 --- a/terrain/paints/default_terrain_paint.res +++ b/terrain/paints/default_terrain_paint.res @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f7bc9218bdd88690b9994999715acb1e1233299ba9fe008d835a6cc9ff592f0 -size 294 +oid sha256:e08b6ac8b5ebc3a2ef443939261f0c1b9414149f8e45dea3ca968fa6ae02e706 +size 299 diff --git a/terrain/paints/desert_sand_terrain_paint.res b/terrain/paints/desert_sand_terrain_paint.res index e60460e..8f738b1 100644 --- a/terrain/paints/desert_sand_terrain_paint.res +++ b/terrain/paints/desert_sand_terrain_paint.res @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1908a2b3087d64658ebe340148554aa109658b52c1fbf662d340dec961f36a6 -size 293 +oid sha256:7b8347472c2f8fc58b36e57427084fd2e39a289f13f7e0b2d92023f88e6cc074 +size 298 diff --git a/terrain/paints/dry_mud_terrain_paint.res b/terrain/paints/dry_mud_terrain_paint.res index e6ff33e..41d29f8 100644 --- a/terrain/paints/dry_mud_terrain_paint.res +++ b/terrain/paints/dry_mud_terrain_paint.res @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:555af3c288502203b9fa4b8a71f5acdb273a988262a0bffb929a218540158561 -size 289 +oid sha256:e94e02d26626e20c9ff337427b07e8663f04d6a840e456565625c71a464373b1 +size 294 diff --git a/terrain/terrain_instance_3d.gd b/terrain/terrain_instance_3d.gd index 4e4cefe..62c65fa 100644 --- a/terrain/terrain_instance_3d.gd +++ b/terrain/terrain_instance_3d.gd @@ -10,18 +10,18 @@ const _SHADER := preload("res://terrain/terrain.gdshader") ## enum PaintSlot { ERASE, - RED, - GREEN, - BLUE, + PAINT_1, + PAINT_2, + PAINT_3, } -var _albedo_maps: Array[Texture2D] = [null, null, null, null] +var _albedo_maps: Array[Texture2D] = [] var _material := ShaderMaterial.new() var _mesh := PlaneMesh.new() -var _normal_maps: Array[Texture2D] = [null, null, null, null] +var _normal_maps: Array[Texture2D] = [] ## ## Range of the height channel value, ranging from [code]0.0[/code] to [code]100.0[/code]. @@ -54,6 +54,11 @@ var size := Vector2i.ONE: size = value func _init() -> void: + var paint_channels := PaintSlot.values().size() + + _albedo_maps.resize(paint_channels) + _normal_maps.resize(paint_channels) + _material.shader = _SHADER _mesh.surface_set_material(0, _material) diff --git a/map_editor/map_editor_terrain_canvas.gd b/terrain/terrain_map_canvas.gd similarity index 92% rename from map_editor/map_editor_terrain_canvas.gd rename to terrain/terrain_map_canvas.gd index ae75b45..00cafad 100644 --- a/map_editor/map_editor_terrain_canvas.gd +++ b/terrain/terrain_map_canvas.gd @@ -1,4 +1,4 @@ -class_name MapEditorTerrainCanvas extends Node +class_name TerrainMapCanvas extends Node ## ## Blank sample value. @@ -37,7 +37,11 @@ func clear(clear_elevation: float) -> void: _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: return _editable_texture diff --git a/user_interface/action_prompt.gd b/user_interface/action_prompt.gd new file mode 100644 index 0000000..ca662d6 --- /dev/null +++ b/user_interface/action_prompt.gd @@ -0,0 +1,49 @@ +@tool +class_name ActionPrompt extends Control + +## +## The prompt accept button has been pressed. +## +signal prompt_accepted() + +## +## The prompt has been started. +## +signal prompted() + +@export +var _accept_button: BaseButton = null + +## +## The initial control focused on when it is prompted or [code]null[/code] for no default. +## +@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()) + +## +## Starts the prompt, emitting [signal prompted]. +## +## [signal prompt_accepted] is emitted when the accept button in the action prompt is pressed. +## +func prompt() -> void: + LocalPlayer.override_controls(hidden) + show() + prompted.emit() + + if initial_focus != null: + initial_focus.grab_focus() diff --git a/user_interface/button_selection.gd b/user_interface/button_selection.gd deleted file mode 100644 index f79eb5c..0000000 --- a/user_interface/button_selection.gd +++ /dev/null @@ -1,86 +0,0 @@ -@tool -class_name ItemSelection extends HFlowContainer - -## -## An item has been selected via GUI selection or [method select_item]. -## -signal item_selected(index: int) - -var _button_group := ButtonGroup.new() - -## -## Number of items in the selection. -## -var item_count: int: - get: - return _button_group.get_buttons().size() - -func _get_configuration_warnings() -> PackedStringArray: - var warnings := PackedStringArray() - var children := get_children() - - if not(children.is_empty()): - var self_class_name := get_class() - - for child in get_children(): - warnings.append( - "{0} can only have Button children, but {1} is of type {2}".format( - [self_class_name, child.name, child.get_class()])) - - return warnings - -## -## Adds a new item with no text and only [code]icon[/code] as the icon to the selection. -## -func add_icon_item(icon: Texture2D) -> void: - var child_count := get_child_count() - var button := Button.new() - - button.icon = icon - button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER - button.toggle_mode = true - button.button_group = _button_group - button.button_pressed = child_count == 0 - - button.pressed.connect(func () -> void: select_item(child_count)) - add_child(button) - -## -## Returns the icon used by the item at [code]index[/code]. -## -## An assertion is raised if [code]index[/code] is out of bounds from the item count. -## -func get_item_icon(index: int) -> Texture2D: - var buttons := _button_group.get_buttons() - - assert(index < buttons.size(), "index out of range") - - var button := buttons[index] as Button - - return null if (button == null) else button.icon - -## -## Returns the currently selected item index or [code]-1[/code] if no item is selected. -## -func get_selected_item() -> int: - var pressed_button := _button_group.get_pressed_button() - - return -1 if (pressed_button == null) else pressed_button.get_index() - -## -## Selects the item at [code]index[/code], emitting [signal item_selected] at the end. -## -## An assertion is raised if [code]index[/code] is out of bounds from the item count. -## -func select_item(index: int) -> void: - var buttons := _button_group.get_buttons() - - assert(index < buttons.size(), "index out of range") - - var pressed_button := _button_group.get_pressed_button() - - if pressed_button != null: - pressed_button.set_pressed_no_signal(false) - - buttons[index].set_pressed_no_signal(true) - item_selected.emit(index) diff --git a/user_interface/selection_prompt.gd b/user_interface/selection_prompt.gd new file mode 100644 index 0000000..3262c04 --- /dev/null +++ b/user_interface/selection_prompt.gd @@ -0,0 +1,69 @@ +@tool +class_name SelectionPrompt extends Control + +## +## The prompt has been started. +## +signal prompted() + +## +## The item at index [code]selected_item[/code] has been selected in the prompt. +## +signal prompt_selected(selected_item: int) + +@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: + LocalPlayer.override_controls(hidden) + 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() diff --git a/user_interface/worker_prompt.gd b/user_interface/worker_prompt.gd new file mode 100644 index 0000000..112ffa0 --- /dev/null +++ b/user_interface/worker_prompt.gd @@ -0,0 +1,56 @@ +class_name WorkerPrompt extends Control + +## +## The work being performed by the prompt has ended. +## +signal prompt_completed() + +## +## The prompt has been started. +## +signal prompted() + +var _worker_thread := Thread.new() + +## +## Used to display a message from the prompt or [code]null[/code] to not display anything. +## +@export +var label: Label = null + +## +## Used to display the progress of the prompt or [code]null[/code] to not display anything. +## +@export +var progress: Range = null + +## +## Starts the prompt, emitting [signal prompted]. +## +## [signal prompt_completed] is emitted when the work being performed by the prompt has ended. +## +func prompt(display_message: String, steps: Array) -> void: + LocalPlayer.override_controls(hidden) + 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() + prompt_completed.emit() + hide() +