Merge pull request 'Interior Maps' (#13) from interior-maps into main
Reviewed-on: #13
This commit is contained in:
commit
e6b115a9e2
|
@ -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)
|
|
@ -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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
Binary file not shown.
|
@ -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
|
Binary file not shown.
Binary file not shown.
|
@ -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
|
Binary file not shown.
|
@ -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
|
Binary file not shown.
|
@ -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
|
Binary file not shown.
|
@ -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
|
Binary file not shown.
Binary file not shown.
|
@ -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
|
Binary file not shown.
|
@ -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
|
Binary file not shown.
|
@ -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
|
Binary file not shown.
|
@ -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
|
Binary file not shown.
Binary file not shown.
|
@ -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
|
Binary file not shown.
|
@ -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
|
Binary file not shown.
BIN
map_editor.scn (Stored with Git LFS)
BIN
map_editor.scn (Stored with Git LFS)
Binary file not shown.
Binary file not shown.
BIN
map_editor/camera_boundary_mesh.res (Stored with Git LFS)
BIN
map_editor/camera_boundary_mesh.res (Stored with Git LFS)
Binary file not shown.
|
@ -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)
BIN
map_editor/menus_theme.res (Stored with Git LFS)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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,64 +62,102 @@ 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
|
||||
var _target_orientation := global_rotation.y
|
||||
|
||||
##
|
||||
## Smoothness applies to the interpolation toward rotation and movement target values.
|
||||
##
|
||||
@export
|
||||
var movement_smoothing := 0.5
|
||||
func _process(delta: float) -> void:
|
||||
if not(is_frozen()):
|
||||
var global_basis := global_transform.basis
|
||||
var camera_settings := GameSettings.camera_settings
|
||||
|
||||
##
|
||||
## Whether or not player movement input processed by the controller should be ignored.
|
||||
##
|
||||
@export
|
||||
var frozen := false
|
||||
var delta_speed :=\
|
||||
camera_settings.movement_speed_modifier * _MOVE_SPEED_BASE_MODIFIER * delta
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
_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)
|
||||
|
||||
_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)
|
||||
|
||||
_target_orientation += (Input.get_action_strength(_ROTATE_CCW) -\
|
||||
Input.get_action_strength(_ROTATE_CW)) * _ROTATE_SPEED_BASE *\
|
||||
camera_settings.rotation_speed_modifier * delta
|
||||
|
||||
global_transform = global_transform.interpolate_with(
|
||||
Transform3D(Basis(Vector3.UP, _target_orientation), _target_position),
|
||||
delta * _TRANSFORM_DELTA * _MOVE_SMOOTHING)
|
||||
|
||||
match _select_mode:
|
||||
SelectMode.NONE:
|
||||
_cursor_point = get_viewport().size / 2.0
|
||||
|
||||
SelectMode.MOUSE:
|
||||
_cursor_point = get_viewport().get_mouse_position()
|
||||
|
||||
func _ready() -> void:
|
||||
if _selection_area != null:
|
||||
_selection_area.mouse_entered.connect(func () -> void:
|
||||
if _select_mode != SelectMode.MOUSE:
|
||||
_select_mode = SelectMode.MOUSE
|
||||
|
||||
select_mode_changed.emit(SelectMode.MOUSE))
|
||||
|
||||
_selection_area.mouse_exited.connect(func () -> void:
|
||||
if _select_mode != SelectMode.NONE:
|
||||
_select_mode = 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:
|
||||
MOUSE_BUTTON_MIDDLE:
|
||||
self._is_drag_panning = event.is_pressed() and not(self.frozen)
|
||||
_is_drag_panning = event.is_pressed() and not(is_frozen())
|
||||
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if\
|
||||
self._is_drag_panning else Input.MOUSE_MODE_VISIBLE
|
||||
_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()
|
||||
if event.is_pressed():
|
||||
selection_started.emit(SelectAction.PRIMARY)
|
||||
|
||||
else:
|
||||
self.selection_stopped.emit()
|
||||
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 self._is_drag_panning:
|
||||
var global_basis := self.global_transform.basis
|
||||
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
|
||||
|
||||
self._target_position += dampened_speed.y * (-global_basis.z) *\
|
||||
_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) *\
|
||||
_target_position += dampened_speed.x * (-global_basis.x) *\
|
||||
event.relative.x * (-1.0 if camera_settings.is_x_inverted else 1.0)
|
||||
|
||||
return
|
||||
|
@ -112,68 +166,7 @@ func _input(event: InputEvent) -> void:
|
|||
return
|
||||
|
||||
if event is InputEventScreenTouch:
|
||||
return
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not(self.frozen):
|
||||
var global_basis := self.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) *\
|
||||
(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) *\
|
||||
(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) -\
|
||||
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)
|
||||
|
||||
match self._select_mode:
|
||||
SelectMode.NONE:
|
||||
self._cursor_point = self.get_viewport().size / 2.0
|
||||
|
||||
SelectMode.MOUSE:
|
||||
self._cursor_point = self.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
|
||||
|
||||
self.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
|
||||
|
||||
self.select_mode_changed.emit(SelectMode.NONE))
|
||||
|
||||
##
|
||||
##
|
||||
##
|
||||
func in_select_area() -> bool:
|
||||
return self._select_mode != SelectMode.NONE
|
||||
|
||||
##
|
||||
## 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
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
|
|
BIN
terrain/paints/default_terrain_paint.res (Stored with Git LFS)
BIN
terrain/paints/default_terrain_paint.res (Stored with Git LFS)
Binary file not shown.
BIN
terrain/paints/desert_sand_terrain_paint.res (Stored with Git LFS)
BIN
terrain/paints/desert_sand_terrain_paint.res (Stored with Git LFS)
Binary file not shown.
BIN
terrain/paints/dry_mud_terrain_paint.res (Stored with Git LFS)
BIN
terrain/paints/dry_mud_terrain_paint.res (Stored with Git LFS)
Binary file not shown.
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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()
|
|
@ -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)
|
|
@ -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()
|
|
@ -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()
|
||||
|
Loading…
Reference in New Issue