Merge pull request 'Interior Maps' (#13) from interior-maps into main

Reviewed-on: #13
This commit is contained in:
kayomn 2023-02-01 01:16:15 +01:00
commit e6b115a9e2
54 changed files with 1208 additions and 255 deletions

339
interior/interior_map.gd Normal file
View File

@ -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)

29
interior/interior_tile.gd Normal file
View File

@ -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

BIN
interior/tiles/dungeon_01_pipehole_tile.res (Stored with Git LFS) Normal file

Binary file not shown.

BIN
interior/tiles/dungeon_01_tiling_tile.res (Stored with Git LFS) Normal file

Binary file not shown.

BIN
interior/tiles/limestone_dungeon/hole_albedo.png (Stored with Git LFS) Executable file

Binary file not shown.

View File

@ -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

BIN
interior/tiles/limestone_dungeon/hole_height.png (Stored with Git LFS) Executable file

Binary file not shown.

View File

@ -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

BIN
interior/tiles/limestone_dungeon/hole_material.res (Stored with Git LFS) Normal file

Binary file not shown.

BIN
interior/tiles/limestone_dungeon/hole_normal.png (Stored with Git LFS) Executable file

Binary file not shown.

View File

@ -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

BIN
interior/tiles/limestone_dungeon/hole_orm.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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

BIN
interior/tiles/limestone_dungeon/piping_albedo.png (Stored with Git LFS) Executable file

Binary file not shown.

View File

@ -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

BIN
interior/tiles/limestone_dungeon/piping_height.png (Stored with Git LFS) Executable file

Binary file not shown.

View File

@ -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

BIN
interior/tiles/limestone_dungeon/piping_material.res (Stored with Git LFS) Normal file

Binary file not shown.

BIN
interior/tiles/limestone_dungeon/piping_normal.png (Stored with Git LFS) Executable file

Binary file not shown.

View File

@ -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

BIN
interior/tiles/limestone_dungeon/piping_orm.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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

BIN
interior/tiles/limestone_dungeon/tiling_albedo.png (Stored with Git LFS) Executable file

Binary file not shown.

View File

@ -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

BIN
interior/tiles/limestone_dungeon/tiling_height.png (Stored with Git LFS) Executable file

Binary file not shown.

View File

@ -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

BIN
interior/tiles/limestone_dungeon/tiling_material.res (Stored with Git LFS) Normal file

Binary file not shown.

BIN
interior/tiles/limestone_dungeon/tiling_normal.png (Stored with Git LFS) Executable file

Binary file not shown.

View File

@ -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

BIN
interior/tiles/limestone_dungeon/tiling_orm.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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

BIN
local_player.scn (Stored with Git LFS) Normal file

Binary file not shown.

BIN
map_editor.scn (Stored with Git LFS)

Binary file not shown.

BIN
map_editor/brush_selection_button_group.res (Stored with Git LFS) Normal file

Binary file not shown.

BIN
map_editor/camera_boundary_mesh.res (Stored with Git LFS)

Binary file not shown.

View File

@ -0,0 +1,7 @@
class_name MapEditorMenu extends VBoxContainer
##
## Resets the state of the menu.
##
func reset() -> void:
pass

BIN
map_editor/menus_theme.res (Stored with Git LFS)

Binary file not shown.

BIN
map_editor/paint_selector_button_group.res (Stored with Git LFS) Normal file

Binary file not shown.

BIN
map_editor/selection_area_mesh.res (Stored with Git LFS) Normal file

Binary file not shown.

BIN
map_editor/structure_button_group.res (Stored with Git LFS) Normal file

Binary file not shown.

BIN
map_editor/tile_cursor_object_material.res (Stored with Git LFS) Normal file

Binary file not shown.

BIN
map_editor/tile_selector_button_group.res (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,5 +1,16 @@
class_name PlayerController extends Node3D 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. ## Supported selection input devices.
## ##
@ -14,9 +25,12 @@ enum SelectMode {
signal select_mode_changed(mode: 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. ## 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 _DRAG_SPEED_BASE_MODIFIER := 0.15
const _MOVE_SMOOTHING := 0.5
const _MOVE_SPEED_BASE_MODIFIER := 50.0 const _MOVE_SPEED_BASE_MODIFIER := 50.0
const _ROTATE_SPEED_BASE := 5.0 const _ROTATE_SPEED_BASE := 5.0
@ -46,134 +62,111 @@ const _TRANSFORM_DELTA := 10.0
@export @export
var _camera: Camera3D = null var _camera: Camera3D = null
var _control_override_count := 0
var _cursor_point := Vector2.ZERO var _cursor_point := Vector2.ZERO
var _is_drag_panning := false var _is_drag_panning := false
var _is_selecting := false
var _select_mode := SelectMode.NONE var _select_mode := SelectMode.NONE
@export @export
var _selection_area: Control = null var _selection_area: Control = null
@onready @onready
var _target_position := self.position var _target_position := position
@onready @onready
var _target_orientation := self.global_rotation.y var _target_orientation := 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
func _process(delta: float) -> void: func _process(delta: float) -> void:
if not(self.frozen): if not(is_frozen()):
var global_basis := self.global_transform.basis var global_basis := global_transform.basis
var camera_settings := GameSettings.camera_settings var camera_settings := GameSettings.camera_settings
var delta_speed :=\ var delta_speed :=\
camera_settings.movement_speed_modifier * _MOVE_SPEED_BASE_MODIFIER * delta 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)) *\ (Input.get_action_strength(_FORWARD) - Input.get_action_strength(_BACKWARD)) *\
(-1.0 if camera_settings.is_y_inverted else 1.0) (-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)) *\ (Input.get_action_strength(_LEFT) - Input.get_action_strength(_RIGHT)) *\
(-1.0 if camera_settings.is_y_inverted else 1.0) (-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 *\ Input.get_action_strength(_ROTATE_CW)) * _ROTATE_SPEED_BASE *\
camera_settings.rotation_speed_modifier * delta camera_settings.rotation_speed_modifier * delta
self.global_transform = self.global_transform.interpolate_with( global_transform = global_transform.interpolate_with(
Transform3D(Basis(Vector3.UP, self._target_orientation), self._target_position), Transform3D(Basis(Vector3.UP, _target_orientation), _target_position),
delta * _TRANSFORM_DELTA * self.movement_smoothing) delta * _TRANSFORM_DELTA * _MOVE_SMOOTHING)
match self._select_mode: match _select_mode:
SelectMode.NONE: SelectMode.NONE:
self._cursor_point = self.get_viewport().size / 2.0 _cursor_point = get_viewport().size / 2.0
SelectMode.MOUSE: SelectMode.MOUSE:
self._cursor_point = self.get_viewport().get_mouse_position() _cursor_point = get_viewport().get_mouse_position()
func _ready() -> void: func _ready() -> void:
if self._selection_area != null: if _selection_area != null:
self._selection_area.mouse_entered.connect(func () -> void: _selection_area.mouse_entered.connect(func () -> void:
if self._select_mode != SelectMode.MOUSE: if _select_mode != SelectMode.MOUSE:
self._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: _selection_area.mouse_exited.connect(func () -> void:
if self._select_mode != SelectMode.NONE: if _select_mode != SelectMode.NONE:
self._select_mode = SelectMode.NONE _select_mode = SelectMode.NONE
self.select_mode_changed.emit(SelectMode.NONE)) select_mode_changed.emit(SelectMode.NONE))
## _selection_area.gui_input.connect(func (event: InputEvent) -> void:
## if event is InputEventMouseButton:
## match event.button_index:
func in_select_area() -> bool: MOUSE_BUTTON_MIDDLE:
return self._select_mode != SelectMode.NONE _is_drag_panning = event.is_pressed() and not(is_frozen())
## Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if\
## Returns [code]true[/code] if the player controller is currently selecting a location on the _is_drag_panning else Input.MOUSE_MODE_VISIBLE
## screen, otherwise [code]false[/code].
## MOUSE_BUTTON_LEFT:
## *Note* that it is discouraged that this be continuously polled for single-fire events. Instead, if event.is_pressed():
## see [signal selection_started] and [signal selection_stopped]. selection_started.emit(SelectAction.PRIMARY)
##
func is_selecting() -> bool: else:
return self._is_selecting 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. ## 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 ## 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. ## states - including gamepad - this will be the middle of screenspace at all times.
## ##
func get_cursor_point() -> Vector2: func get_plane_cursor() -> Vector2:
return self._cursor_point 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 ## Returns [code]true[/code] if the player controller is currently frozen, otherwise
## coordinate relative to an infinite ground plane at y offset [code]0.0[/code]. ## [code]false[/code].
## ##
## Successful point intersection will return the plane coordinates in a [class Vector2], otherwise func is_frozen() -> bool:
## [code]null[/code] is returned. 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): ## Overrides the controls of the player controller. When [code]unlock_signal[/code] is emitted, the
if self._camera != null: ## override is released.
var plane_target = Plane(Vector3.UP, 0.0).intersects_ray( ##
self._camera.global_position, self._camera.project_ray_normal(screen_point)) 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: _control_override_count += 1
return Vector2(plane_target.x, plane_target.z)
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)

View File

@ -8,60 +8,23 @@
config_version=5 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] [application]
config/name="Protectorate" config/name="Protectorate"
run/main_scene="res://map_editor.scn" run/main_scene="res://map_editor.scn"
config/use_custom_user_dir=true config/use_custom_user_dir=true
config/features=PackedStringArray("4.0", "Forward Plus") config/features=PackedStringArray("4.0", "Forward Plus")
boot_splash/show_image=false
config/icon="res://icon.png" config/icon="res://icon.png"
[autoload] [autoload]
GameSettings="*res://settings.gd" GameSettings="*res://settings.gd"
LocalPlayer="*res://local_player.scn"
[debug] [debug]
gdscript/warnings/integer_division=0
gdscript/warnings/assert_always_true=0 gdscript/warnings/assert_always_true=0
gdscript/warnings/assert_always_false=0 gdscript/warnings/assert_always_false=0
@ -85,32 +48,32 @@ enabled=PackedStringArray()
player_controller_left={ player_controller_left={
"deadzone": 0.5, "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={ player_controller_right={
"deadzone": 0.5, "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={ player_controller_forward={
"deadzone": 0.5, "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={ player_controller_backward={
"deadzone": 0.5, "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={ player_controller_rotate_cw={
"deadzone": 0.5, "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={ player_controller_rotate_ccw={
"deadzone": 0.5, "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={ 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] [rendering]
lights_and_shadows/directional_shadow/soft_shadow_filter_quality=3 lights_and_shadows/directional_shadow/soft_shadow_filter_quality=3

BIN
terrain/paints/default_terrain_paint.res (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

BIN
terrain/paints/dry_mud_terrain_paint.res (Stored with Git LFS)

Binary file not shown.

View File

@ -10,18 +10,18 @@ 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] = []
var _material := ShaderMaterial.new() var _material := ShaderMaterial.new()
var _mesh := PlaneMesh.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]. ## 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 size = value
func _init() -> void: func _init() -> void:
var paint_channels := PaintSlot.values().size()
_albedo_maps.resize(paint_channels)
_normal_maps.resize(paint_channels)
_material.shader = _SHADER _material.shader = _SHADER
_mesh.surface_set_material(0, _material) _mesh.surface_set_material(0, _material)

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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()