mirror of
https://github.com/drwhut/tabletop-club.git
synced 2025-05-05 15:32:56 +00:00
427 lines
15 KiB
GDScript
427 lines
15 KiB
GDScript
# tabletop-club
|
|
# Copyright (c) 2020-2024 Benjamin 'drwhut' Beddows.
|
|
# Copyright (c) 2021-2024 Tabletop Club contributors (see game/CREDITS.tres).
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in all
|
|
# copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
extends Node
|
|
|
|
enum {
|
|
COLLISION_CONVEX,
|
|
COLLISION_MULTI_CONVEX,
|
|
COLLISION_CONCAVE
|
|
}
|
|
|
|
enum {
|
|
COM_ADJUST_OFF,
|
|
COM_ADJUST_VOLUME,
|
|
COM_ADJUST_GEOMETRY
|
|
}
|
|
|
|
# Build a piece using an entry from the AssetDB.
|
|
# Returns: The piece corresponding to the given entry.
|
|
# piece_entry: The entry to create the piece with.
|
|
# extra_nodes: If true, also include nodes that provide extra functionality,
|
|
# e.g. sound effects.
|
|
func build_piece(piece_entry: Dictionary, extra_nodes: bool = true) -> Piece:
|
|
var piece = ResourceManager.load_res(piece_entry["scene_path"]).instance()
|
|
|
|
# If the scene is not a piece (e.g. when importing a scene from the assets
|
|
# folder), make it a piece so it can interact with other objects.
|
|
if not piece is Piece:
|
|
var scene_dir = piece_entry["scene_path"].get_base_dir()
|
|
var build: Piece = null
|
|
|
|
if scene_dir.ends_with("containers"):
|
|
build = PieceContainer.new()
|
|
|
|
build.contact_monitor = true
|
|
build.contacts_reported = 2
|
|
|
|
var pieces_node = Spatial.new()
|
|
pieces_node.name = "Pieces"
|
|
build.add_child(pieces_node)
|
|
pieces_node.owner = build
|
|
elif scene_dir.ends_with("speakers") or scene_dir.ends_with("timers"):
|
|
if extra_nodes:
|
|
if scene_dir.ends_with("speakers"):
|
|
build = SpeakerPiece.new()
|
|
else:
|
|
build = TimerPiece.new()
|
|
|
|
var audio_player_node = AudioStreamPlayer.new()
|
|
audio_player_node.name = "AudioStreamPlayer"
|
|
build.add_child(audio_player_node)
|
|
audio_player_node.owner = build
|
|
else:
|
|
# Speakers rely on their audio player, so convert to a vanilla
|
|
# piece if we don't want the audio player.
|
|
build = Piece.new()
|
|
else:
|
|
var parent_dir = scene_dir.get_base_dir()
|
|
if parent_dir.ends_with("dice"):
|
|
build = Dice.new()
|
|
|
|
if extra_nodes:
|
|
# Dice, along with cards and tokens, have their own unique
|
|
# sound effects which are implemented here.
|
|
build.contact_monitor = true
|
|
build.contacts_reported = 1
|
|
|
|
var effect_player = AudioStreamPlayer3D.new()
|
|
effect_player.name = "EffectPlayer"
|
|
effect_player.bus = "Effects"
|
|
effect_player.unit_size = 20.0
|
|
build.add_child(effect_player)
|
|
effect_player.owner = build
|
|
|
|
build.effect_player_path = NodePath("EffectPlayer")
|
|
|
|
build.table_collide_fast_sounds = preload("res://Sounds/Dice/DiceTableFastSounds.tres")
|
|
build.shake_sounds = preload("res://Sounds/Dice/DiceShakeSounds.tres")
|
|
else:
|
|
build = Piece.new()
|
|
|
|
_extract_and_shape_mesh_instances(build, piece, Transform.IDENTITY,
|
|
piece_entry["collision_mode"])
|
|
|
|
if not piece.get_parent():
|
|
ResourceManager.free_object(piece)
|
|
piece = build
|
|
|
|
piece.mass = piece_entry["mass"]
|
|
piece.piece_entry = piece_entry
|
|
|
|
scale_piece(piece, piece_entry["scale"])
|
|
_adjust_centre_of_mass(piece, piece_entry)
|
|
|
|
if piece_entry.has("texture_path") and piece_entry["texture_path"] is String:
|
|
var texture: Texture = ResourceManager.load_res(piece_entry["texture_path"])
|
|
piece.apply_texture(texture)
|
|
|
|
# Check if the entry has textures for more than one surface.
|
|
var surface = 1
|
|
while piece_entry.has("texture_path_" + str(surface)):
|
|
var texture_path: String = piece_entry["texture_path_" + str(surface)]
|
|
var optional_texture: Texture
|
|
if not texture_path.empty():
|
|
optional_texture = ResourceManager.load_res(texture_path)
|
|
else:
|
|
optional_texture = preload("res://Images/BlackTexture.png")
|
|
piece.apply_texture(optional_texture, surface)
|
|
surface += 1
|
|
|
|
if piece.is_albedo_color_exposed():
|
|
piece.set_albedo_color_client(piece_entry["color"])
|
|
|
|
if extra_nodes:
|
|
piece.setup_outline_material()
|
|
|
|
var inbuilt_sfx = false
|
|
match piece_entry["scene_path"]:
|
|
"res://Pieces/Card.tscn":
|
|
inbuilt_sfx = true
|
|
"res://Pieces/Tokens/Cube.tscn":
|
|
inbuilt_sfx = true
|
|
"res://Pieces/Tokens/Cylinder.tscn":
|
|
inbuilt_sfx = true
|
|
|
|
var provided_sfx = false
|
|
if piece_entry.has("sfx"):
|
|
if piece_entry["sfx"] is String:
|
|
if piece_entry["sfx"] in AssetDB.SFX_AUDIO_STREAMS:
|
|
provided_sfx = true
|
|
|
|
if inbuilt_sfx or provided_sfx:
|
|
if piece.effect_player_path.empty():
|
|
var effect_player = AudioStreamPlayer3D.new()
|
|
effect_player.name = "EffectPlayer"
|
|
effect_player.bus = "Effects"
|
|
effect_player.unit_size = 20
|
|
|
|
piece.add_child(effect_player)
|
|
effect_player.owner = piece
|
|
piece.effect_player_path = NodePath("EffectPlayer")
|
|
|
|
# Need to enable contact monitoring so the piece knows when to
|
|
# play the sound effects.
|
|
if not piece.contact_monitor:
|
|
piece.contact_monitor = true
|
|
piece.contacts_reported = 1
|
|
|
|
if provided_sfx:
|
|
var sounds = AssetDB.SFX_AUDIO_STREAMS[piece_entry["sfx"]]
|
|
piece.table_collide_fast_sounds = sounds["fast"]
|
|
piece.table_collide_slow_sounds = sounds["slow"]
|
|
|
|
return piece
|
|
|
|
# Build a table using a table entry from the AssetDB.
|
|
# Returns: The table corresponding to the given entry.
|
|
# table_entry: The entry to create the table with.
|
|
func build_table(table_entry: Dictionary) -> RigidBody:
|
|
var scene: Spatial = ResourceManager.load_res(table_entry["scene_path"]).instance()
|
|
|
|
var table = RigidBody.new()
|
|
_extract_and_shape_mesh_instances(table, scene, Transform.IDENTITY,
|
|
table_entry["collision_mode"])
|
|
if not scene.get_parent():
|
|
ResourceManager.free_object(scene)
|
|
|
|
table.mass = 100000 # = 10kg
|
|
table.mode = RigidBody.MODE_STATIC
|
|
|
|
var physics_material = PhysicsMaterial.new()
|
|
physics_material.bounce = table_entry["bounce"]
|
|
table.physics_material_override = physics_material
|
|
|
|
# Since the table is a vanilla RigidBody, it doesn't have a "table_entry"
|
|
# property like pieces do, so we'll store the table entry in it's metadata.
|
|
table.set_meta("table_entry", table_entry)
|
|
|
|
_adjust_centre_of_mass(table, table_entry, true)
|
|
|
|
return table
|
|
|
|
# Fill a stack with pieces using an entry from the AssetDB.
|
|
# stack: The stack to fill.
|
|
# stack_entry: The stack entry to use.
|
|
func fill_stack(stack: Stack, stack_entry: Dictionary) -> void:
|
|
var stack_entry_dir = stack_entry["entry_path"].get_base_dir()
|
|
for entry_name in stack_entry["entry_names"]:
|
|
var entry_path = stack_entry_dir + "/" + entry_name
|
|
var piece_entry = AssetDB.search_path(entry_path)
|
|
if piece_entry.empty():
|
|
push_error("Entry (%s) was not found!" % entry_path)
|
|
continue
|
|
|
|
stack.add_piece(piece_entry, Transform.IDENTITY, Stack.STACK_BOTTOM,
|
|
Stack.FLIP_NO)
|
|
|
|
# Create an array of MeshInstances from a piece, which can also be inserted
|
|
# into stacks.
|
|
# Returns: An array of MeshInstances representing the piece's meshes, but with
|
|
# extra metadata.
|
|
# piece: The piece to get the mesh instances from.
|
|
func get_piece_meshes(piece: Piece) -> Array:
|
|
var out = []
|
|
for mesh_instance in piece.get_mesh_instances():
|
|
var piece_mesh = MeshInstance.new()
|
|
piece_mesh.name = piece.name
|
|
piece_mesh.transform = mesh_instance.transform
|
|
|
|
piece_mesh.set_meta("piece_entry", piece.piece_entry)
|
|
|
|
piece_mesh.mesh = mesh_instance.mesh
|
|
for surface in range(mesh_instance.get_surface_material_count()):
|
|
var material = mesh_instance.get_surface_material(surface)
|
|
piece_mesh.set_surface_material(surface, material)
|
|
|
|
out.append(piece_mesh)
|
|
|
|
return out
|
|
|
|
# Scale a piece by changing the scale of its children collision shapes.
|
|
# piece: The piece to scale.
|
|
# scale: How much to scale the piece by.
|
|
func scale_piece(piece: Piece, scale: Vector3) -> void:
|
|
for child in piece.get_collision_shapes():
|
|
child.scale_object_local(scale)
|
|
|
|
# If the given piece entry has bounding box information, adjust the centre of
|
|
# mass of a piece to be the centre of the bounding box.
|
|
# piece: The piece whose centre of mass to adjust.
|
|
# piece_entry: The piece entry of the piece.
|
|
# keep_pos: Should the piece's position stay the same?
|
|
func _adjust_centre_of_mass(piece: RigidBody, piece_entry: Dictionary,
|
|
keep_pos: bool = false) -> void:
|
|
|
|
if not piece_entry.has("com_adjust"):
|
|
return
|
|
|
|
var adjust_method: int = piece_entry["com_adjust"]
|
|
if adjust_method == COM_ADJUST_OFF:
|
|
return
|
|
|
|
var centre_of_mass: Vector3
|
|
match adjust_method:
|
|
COM_ADJUST_VOLUME:
|
|
var box_min: Vector3 = piece_entry["bounding_box"][0]
|
|
var box_max: Vector3 = piece_entry["bounding_box"][1]
|
|
centre_of_mass = 0.5 * (box_min + box_max)
|
|
COM_ADJUST_GEOMETRY:
|
|
centre_of_mass = piece_entry["avg_point"]
|
|
_:
|
|
push_error("Invalid 'com_adjust' value %d!" % adjust_method)
|
|
return
|
|
|
|
# NOTE: The reason we offset all the collision shapes is because the
|
|
# Bullet physics engine defines the centre of mass as the origin of the
|
|
# rigidbody, and there is currently no way to manually define the
|
|
# centre of mass of a rigidbody in Godot. See:
|
|
# https://github.com/godotengine/godot-proposals/issues/945
|
|
for child in piece.get_children():
|
|
if child is CollisionShape:
|
|
child.transform.origin -= centre_of_mass
|
|
|
|
if keep_pos:
|
|
piece.transform.origin += centre_of_mass
|
|
|
|
# Extract mesh instances from a tree, define collision shapes for each mesh
|
|
# instance, and add them to a node.
|
|
# add_to: The node to add the collision shapes + mesh instances to.
|
|
# from: Where to start recursing from.
|
|
# transform: The transform up to that point in the recursion.
|
|
# collision_mode: The type of collision shape to make - see the enum above for
|
|
# possible values.
|
|
func _extract_and_shape_mesh_instances(add_to: Node, from: Node,
|
|
transform: Transform, collision_mode: int) -> void:
|
|
|
|
for child in from.get_children():
|
|
var new_basis = transform.basis
|
|
var new_origin = transform.origin
|
|
|
|
if from is Spatial:
|
|
new_basis = from.transform.basis * new_basis
|
|
new_origin = from.transform.origin + new_origin
|
|
|
|
var new_transform = Transform(new_basis, new_origin)
|
|
_extract_and_shape_mesh_instances(add_to, child, new_transform,
|
|
collision_mode)
|
|
|
|
if from is MeshInstance:
|
|
var parent = from.get_parent()
|
|
if parent:
|
|
parent.remove_child(from)
|
|
|
|
# We also want to make sure that the mesh instance has it's own unique
|
|
# material that isn't shared with the other instances, so when e.g. the
|
|
# instance is being selected, not all of the instances look like they
|
|
# are selected (see #20).
|
|
var num_materials = from.get_surface_material_count()
|
|
var num_surfaces = from.mesh.get_surface_count()
|
|
if num_materials < num_surfaces:
|
|
push_warning("Mesh '%s' has %d surfaces, but only %d materials!" %
|
|
[from.mesh.resource_path, num_surfaces, num_materials])
|
|
|
|
var num_verts = 0
|
|
for surface in range(num_surfaces):
|
|
# A number of arrays make up the surface - the first of them being
|
|
# the vertex array (which is guaranteed to be there).
|
|
num_verts += from.mesh.surface_get_arrays(surface)[0].size()
|
|
|
|
# NOTE: We always assume that imported meshes put their materials
|
|
# into the mesh itself, not the mesh instance.
|
|
var material = from.mesh.surface_get_material(surface)
|
|
if material:
|
|
material = material.duplicate()
|
|
|
|
# There seems to be a bug on OSX where the default cull mode
|
|
# inverts the normals of the mesh.
|
|
# See: https://github.com/godotengine/godot/issues/39936
|
|
if OS.get_name() == "OSX":
|
|
if material is SpatialMaterial:
|
|
material.params_cull_mode = SpatialMaterial.CULL_BACK
|
|
|
|
from.set_surface_material(surface, material)
|
|
|
|
# Real-time global illumination is coming in Godot 4, so while
|
|
# we wait we can approximate emissive materials using
|
|
# OmniLights (see #34).
|
|
# EDIT: Having a ton of these lights is causing serious issues
|
|
# on Linux with the NVIDIA Driver (520.56.06), so I'm going to
|
|
# disable them for now - I can always enable them again if there
|
|
# is enough demand, or if the NVIDIA driver has been updated.
|
|
"""
|
|
if material is SpatialMaterial:
|
|
if material.emission_enabled:
|
|
var omnilight = OmniLight.new()
|
|
omnilight.light_color = material.emission
|
|
omnilight.light_energy = material.emission_energy
|
|
from.add_child(omnilight)
|
|
"""
|
|
|
|
# Don't bother making a collision shape if there's no vertices.
|
|
if num_verts > 0:
|
|
var collision_shape_arr = []
|
|
|
|
if collision_mode == COLLISION_MULTI_CONVEX:
|
|
from.create_multiple_convex_collisions()
|
|
|
|
var static_body_index = -1
|
|
for index in range(from.get_child_count()):
|
|
var child = from.get_child(index)
|
|
if child is StaticBody:
|
|
if static_body_index < 0:
|
|
static_body_index = index
|
|
else:
|
|
push_error("Multiple StaticBody children generated!")
|
|
return
|
|
|
|
if static_body_index < 0:
|
|
push_error("Could not find generated StaticBody!")
|
|
return
|
|
var static_body: StaticBody = from.get_child(static_body_index)
|
|
|
|
for collision_shape in static_body.get_children():
|
|
if collision_shape is CollisionShape:
|
|
static_body.remove_child(collision_shape)
|
|
collision_shape_arr.append(collision_shape)
|
|
|
|
from.remove_child(static_body)
|
|
ResourceManager.free_object(static_body)
|
|
else:
|
|
var collision_shape = CollisionShape.new()
|
|
|
|
if collision_mode == COLLISION_CONVEX:
|
|
collision_shape.shape = from.mesh.create_convex_shape()
|
|
elif collision_mode == COLLISION_CONCAVE:
|
|
collision_shape.shape = from.mesh.create_trimesh_shape()
|
|
else:
|
|
push_error("Invalid collision mode %d!" % collision_mode)
|
|
|
|
collision_shape_arr.append(collision_shape)
|
|
|
|
# The collision shape's transform needs to match up with the mesh
|
|
# instance's, but they can't both use the same transform, otherwise
|
|
# the transform of the mesh instance will be wrong.
|
|
var collision_transform = transform
|
|
collision_transform.basis = from.transform.basis * collision_transform.basis
|
|
collision_transform.origin = from.transform.origin + collision_transform.origin
|
|
|
|
from.transform = Transform.IDENTITY
|
|
for collision_shape in collision_shape_arr:
|
|
collision_shape.transform = collision_transform
|
|
add_to.add_child(collision_shape)
|
|
|
|
# Set the collision shape's owner to the add_to node, so the
|
|
# PieceCache can store this scene in it's entirety if it wants.
|
|
collision_shape.owner = add_to
|
|
|
|
if not collision_shape_arr.empty():
|
|
collision_shape_arr[0].add_child(from)
|
|
from.owner = add_to
|
|
|
|
# We may have added extra nodes to the mesh instance above,
|
|
# for example, OmniLights.
|
|
for sub_child in from.get_children():
|
|
sub_child.owner = add_to
|
|
else:
|
|
ResourceManager.free_object(from)
|