keebie/scripts/game_keyboard.gd

351 lines
9.3 KiB
GDScript

class_name GameKeyboard extends Node3D
signal key_press_changed(game_key: GameKey, event: InputEventKey)
#region variables
@export_group("Node references")
@export var _keys_holder: Node3D
@export var _sfx_player: AudioStreamPlayer3D
@export_group("Key params")
@export var key_size: float = 1
@export var key_gap: float = 0.1
@export_group("Animation")
@export var _rot_sod_fzr: Vector3 = Vector3(3, 0.1, 2)
@export var _time_scale: float = 0.5
@export var _pressing_lean_deg := Vector2(0.15, 0.7)
@export_group("SFX")
@export var _layout_swap_sfx: AudioStream
var alt_visual_layout: bool
var pressed_positions: Dictionary[Vector3, bool]
var anim_time: float
var _rot_sod: SecondOrderDynamics
@onready var _gap_to_size_ratio: float
@onready var _pressing_lean_rad := Vector2(
deg_to_rad(_pressing_lean_deg.x), deg_to_rad(_pressing_lean_deg.y)
)
@onready var _default_rotation: Vector3 = rotation
@onready
var _polyphonic := _sfx_player.get_stream_playback() as AudioStreamPlaybackPolyphonic
#endregion
#region builtins
func _ready() -> void:
_generate_keys(LayoutConfig.layout_rows)
_reset_animations()
func _process(delta: float) -> void:
_animate(delta)
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("reset_animations"):
_reset_animations()
return
if event is not InputEventKey:
return
var event_key := event as InputEventKey
if event_key.echo:
return
if (
event_key.is_pressed()
and LayoutConfig.is_configuring
and event_key.physical_keycode >= KEY_KP_0
and event_key.physical_keycode <= KEY_KP_9
):
_swap_layout(event_key.physical_keycode)
return
var keycode := event_key.get_physical_keycode_with_modifiers()
if (
event_key.is_pressed()
and (keycode == KEY_SHIFT | KEY_MASK_ALT or keycode == KEY_ALT | KEY_MASK_SHIFT)
):
alt_visual_layout = not alt_visual_layout
print("changed visual layout! " + str(alt_visual_layout))
#endregion
#region public
func emit_key_press(game_key: GameKey, event: InputEventKey) -> void:
key_press_changed.emit(game_key, event)
#endregion
#region key generation
func _generate_keys(layout_rows: Array[Array]) -> void:
print("generating keys... '%s'" % LayoutConfig.current_layout.name())
_iterate_keys(_generate_key, layout_rows, {})
func _generate_key(
key_props: KeyProps,
key_pos: Vector3,
angle_deg: float,
_current_keys: Dictionary[Vector2i, Array]
) -> void:
var game_key := GameKey.instantiate_with_props(key_props, key_pos, angle_deg, self)
_keys_holder.add_child(game_key, true)
func _iterate_keys(
iter_function: Callable,
layout_rows: Array[Array],
current_keys: Dictionary[Vector2i, Array]
) -> void:
_set_gap_to_size_ratio()
for row: Array[KeyProps] in layout_rows:
_set_row_key_scales_with_gaps(row)
var layout_size := _get_layout_size(layout_rows)
var key_pos_initial := _get_layout_key_initial_pos(layout_size)
var row_y: float = 0
var pivot := Vector2.ZERO
var angle: float = 0
for row: Array[KeyProps] in layout_rows:
var result := _iterate_row(
iter_function, row, row_y, pivot, angle, key_pos_initial, current_keys
)
row_y = result.x
pivot.x = result.y
pivot.y = result.z
angle = result.w
func _iterate_row(
iter_function: Callable,
row: Array[KeyProps],
row_y: float,
pivot: Vector2,
angle: float,
key_pos_initial: Vector2,
current_keys: Dictionary[Vector2i, Array]
) -> Vector4:
var key_pos_current := key_pos_initial + pivot
key_pos_current.y += row_y
for key_props in row:
if key_props.has_pivot_x:
pivot.x = key_props.pivot_x
if key_props.has_pivot_y:
pivot.y = key_props.pivot_y
if key_props.has_pivot_x or key_props.has_pivot_y:
key_pos_current = key_pos_initial + pivot
if key_props.has_angle:
angle = key_props.angle
var key_pos := Vector3(
key_pos_current.x + key_size / 2, 0, key_pos_current.y + key_size / 2
)
key_pos.x += (
(key_size * key_props.width - key_size) / 2 + key_size * key_props.x
)
key_pos.z += key_props.y * key_size
key_pos = _rotate_key_pos_from_pivot(key_pos, key_pos_initial + pivot, angle)
iter_function.call(key_props, key_pos, angle, current_keys)
key_pos_current.x += (
key_props.width * key_size + key_props.x * key_size + key_gap
)
key_pos_current.y += key_props.y * key_size
row_y = key_pos_current.y - key_pos_initial.y - pivot.y + key_size + key_gap
return Vector4(row_y, pivot.x, pivot.y, angle)
func _set_row_key_scales_with_gaps(row: Array[KeyProps]) -> void:
for key_props in row:
key_props.width = _get_scale_with_gaps(key_props.width)
key_props.height = _get_scale_with_gaps(key_props.height)
key_props.x = _get_scale_with_gaps(key_props.x) + _gap_to_size_ratio
key_props.y = _get_scale_with_gaps(key_props.y) + _gap_to_size_ratio
key_props.width2 = _get_scale_with_gaps(key_props.width2)
key_props.height2 = _get_scale_with_gaps(key_props.height2)
key_props.x2 = _get_scale_with_gaps(key_props.x2) + _gap_to_size_ratio
key_props.y2 = _get_scale_with_gaps(key_props.y2) + _gap_to_size_ratio
key_props.pivot_x = _get_scale_with_gaps(key_props.pivot_x) + _gap_to_size_ratio
key_props.pivot_y = _get_scale_with_gaps(key_props.pivot_y) + _gap_to_size_ratio
func _get_scale_with_gaps(key_scale: float) -> float:
return key_scale + _gap_to_size_ratio * (key_scale - 1)
func _get_layout_size(layout_rows: Array[Array]) -> Vector2:
var max_width: float = 0
var total_height: float = 0
var pivot := Vector2.ZERO
for row: Array[KeyProps] in layout_rows:
var width: float = 0
var height: float = 1
for key_props in row:
width += key_size * key_props.width + key_size * key_props.x
if key_props.has_pivot_y:
print(key_props.pivot_y)
var diff := key_props.pivot_y - pivot.y
height = (
-total_height
+ key_props.pivot_y
+ (-diff / 2 if diff >= 0.0 else -_gap_to_size_ratio)
)
pivot = Vector2(key_props.pivot_x, key_props.pivot_y)
height += key_props.y
var width_with_gaps := width + (row.size() - 1) * key_gap
max_width = maxf(max_width, width_with_gaps)
print(height)
total_height += height + _gap_to_size_ratio
return Vector2(max_width, key_size * (total_height - _gap_to_size_ratio))
func _get_layout_key_initial_pos(layout_size: Vector2) -> Vector2:
return Vector2(-layout_size.x / 2, -layout_size.y / 2)
func _set_gap_to_size_ratio() -> void:
_gap_to_size_ratio = key_gap / key_size
func _rotate_key_pos_from_pivot(
key_pos: Vector3, pivot: Vector2, angle_deg: float
) -> Vector3:
var pivot_pos := Vector3(pivot.x, 0, pivot.y)
return (key_pos - pivot_pos).rotated(Vector3.UP, -deg_to_rad(angle_deg)) + pivot_pos
#endregion
#region layout swapping
func _swap_layout(kp_key: Key) -> void:
if kp_key == KEY_KP_0:
return
var index := kp_key - KEY_KP_1
if index < 0 or index >= LayoutConfig.layouts.size():
return
var new_layout := (
(LayoutConfig.layouts.values() as Array[AbstractLayout])[index].name()
)
if new_layout == LayoutConfig.current_layout.name():
return
LayoutConfig.swap_layout(new_layout)
_regenerate_keys(LayoutConfig.layout_rows)
_play_sfx(_layout_swap_sfx)
func _regenerate_keys(layout_rows: Array[Array]) -> void:
print("REgenerating keys... '%s'" % LayoutConfig.current_layout.name())
var current_keys: Dictionary[Vector2i, Array]
for node in _keys_holder.get_children():
if node is not GameKey:
continue
var game_key := node as GameKey
var key_props := game_key.props
var dict_key := Vector2i(key_props.physical_keycode, key_props.location)
if current_keys.has(dict_key):
current_keys[dict_key].append(game_key)
else:
current_keys[dict_key] = [game_key] as Array[GameKey]
_iterate_keys(_regenerate_key, layout_rows, current_keys)
for game_keys: Array[GameKey] in current_keys.values():
for game_key in game_keys:
game_key.queue_free()
func _regenerate_key(
key_props: KeyProps,
key_pos: Vector3,
angle_deg: float,
current_keys: Dictionary[Vector2i, Array]
) -> void:
var dict_key := Vector2i(key_props.physical_keycode, key_props.location)
if current_keys.has(dict_key):
var game_keys := current_keys[dict_key] as Array[GameKey]
var game_key_idx: int
var min_dist: float = 0
for i in range(game_keys.size()):
var dist := game_keys[i].position.distance_to(key_pos)
if min_dist == 0 or dist <= min_dist:
min_dist = dist
game_key_idx = i
game_keys[game_key_idx].load_props(key_props, key_pos, angle_deg)
game_keys.remove_at(game_key_idx)
if game_keys.size() == 0:
current_keys.erase(dict_key)
else:
var props_dict := LayoutConfig.get_key_config_dict(key_props.physical_keycode)
if props_dict:
key_props.chars_from_dict(props_dict)
var game_key := GameKey.instantiate_with_props(
key_props, key_pos, angle_deg, self
)
_keys_holder.add_child(game_key, true)
#endregion
#region animation
func _reset_animations() -> void:
anim_time = 0
_rot_sod = SecondOrderDynamics.new(_rot_sod_fzr, _default_rotation)
func _animate(delta: float) -> void:
anim_time += delta * _time_scale
rotation = _rot_sod.process(delta, _animate_rotation())
func _animate_rotation() -> Vector3:
var new_rotation := _default_rotation
for pos: Vector3 in pressed_positions.keys():
var pos_pressed := pressed_positions[pos]
if not pos_pressed:
continue
new_rotation.z -= (pos.x / key_size) * _pressing_lean_rad.x
new_rotation.x += (pos.z / key_size) * _pressing_lean_rad.y
return new_rotation
#endregion
#region sounds
func _play_sfx(stream: AudioStream) -> void:
_polyphonic.play_stream(stream)
#endregion