426 lines
11 KiB
GDScript
426 lines
11 KiB
GDScript
@tool
|
|
class_name GameKeyboard extends Node3D
|
|
|
|
#region variables
|
|
signal key_press_changed(game_key: GameKey, event: InputEventKey)
|
|
signal layout_size_changed(rect: Rect2)
|
|
signal prompt_page_turned(page: int)
|
|
signal keys_requested(query_func: Callable)
|
|
signal is_configuring_changed(value: bool)
|
|
signal player_key_changed(game_key: GameKey)
|
|
signal player_finished_move(game_key: GameKey)
|
|
|
|
@export_tool_button("Generate keys")
|
|
var generate_editor_keys_btn := _generate_editor_keys
|
|
@export_tool_button("Delete keys") var delete_editor_keys_btn := _delete_editor_keys
|
|
@export_tool_button("Request keys") var query_keys_btn := _query_keys
|
|
|
|
@export_group("References")
|
|
@export var _keys_holder: Node3D
|
|
@export var _sfx_player: AudioStreamPlayer3D
|
|
@export var _rect_mesh: MeshInstance3D
|
|
@export var _key_scene: PackedScene
|
|
|
|
@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 is_configuring: bool
|
|
var alt_visual_layout: bool
|
|
var layout_rect: Rect2
|
|
var pressed_positions: Dictionary[Vector3, bool]
|
|
var anim_time: float
|
|
|
|
var prompt_page: int = 0
|
|
var prompt_pages_total: int = 0
|
|
|
|
var _rot_sod: SecondOrderDynamics
|
|
|
|
var _queried_keys: Array[GameKey] = []
|
|
var _queried_keys_limit: int = 0
|
|
|
|
@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 = _keys_holder.rotation
|
|
@onready var _polyphonic: AudioStreamPlaybackPolyphonic
|
|
|
|
#endregion
|
|
|
|
|
|
#region builtins
|
|
func _ready() -> void:
|
|
if Engine.is_editor_hint():
|
|
_generate_editor_keys()
|
|
return
|
|
_polyphonic = _sfx_player.get_stream_playback()
|
|
prompt_pages_total = ceili(LayoutConfig.layouts.size() / 9.0)
|
|
_generate_keys()
|
|
_reset_animations()
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if Engine.is_editor_hint():
|
|
return
|
|
_animate(delta)
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if Engine.is_editor_hint():
|
|
return
|
|
if event.is_action_pressed("reset_animations"):
|
|
_reset_animations()
|
|
return
|
|
if event.is_action_pressed("toggle_configuring"):
|
|
is_configuring = not is_configuring
|
|
print("now configuring: %s" % is_configuring)
|
|
is_configuring_changed.emit(is_configuring)
|
|
return
|
|
|
|
if event is not InputEventKey:
|
|
return
|
|
|
|
var event_key := event as InputEventKey
|
|
|
|
if event_key.echo:
|
|
return
|
|
|
|
if (
|
|
event_key.is_pressed()
|
|
and 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)
|
|
|
|
|
|
func emit_player_key_change(game_key: GameKey) -> void:
|
|
player_key_changed.emit(game_key)
|
|
|
|
|
|
func emit_player_finished_move(game_key: GameKey) -> void:
|
|
player_finished_move.emit(game_key)
|
|
|
|
|
|
func query_key_by_keycode(keycode: Key) -> GameKey:
|
|
var found_key: GameKey
|
|
var result := query_keys(
|
|
func(game_key: GameKey) -> bool: return game_key.props.keycode == keycode, 1
|
|
)
|
|
if result:
|
|
found_key = result[0]
|
|
return found_key
|
|
|
|
|
|
func query_keys(query_func: Callable, limit: int = 0) -> Array[GameKey]:
|
|
_queried_keys = []
|
|
_queried_keys_limit = limit
|
|
|
|
keys_requested.emit(query_func)
|
|
var queried_keys := _queried_keys
|
|
|
|
_queried_keys = []
|
|
_queried_keys_limit = 0
|
|
|
|
queried_keys.sort_custom(KeyHelper.sort_func)
|
|
return queried_keys
|
|
|
|
|
|
func key_query_respond(game_key: GameKey) -> void:
|
|
if _queried_keys_limit == 0 or _queried_keys.size() < _queried_keys_limit:
|
|
_queried_keys.append(game_key)
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
#region key generation
|
|
func _generate_keys() -> void:
|
|
print("generating keys...")
|
|
_iterate_keys(_generate_key, LayoutConfig.layout_rows)
|
|
|
|
|
|
func _generate_key(
|
|
key_props: KeyProps,
|
|
key_pos: Vector3,
|
|
angle: float,
|
|
_current_keys: Dictionary[Vector2i, Array]
|
|
) -> void:
|
|
var game_key := GameKey.instantiate_with_props(
|
|
key_props, key_pos, angle, self, _key_scene
|
|
)
|
|
_keys_holder.add_child(game_key, true)
|
|
|
|
|
|
func _iterate_keys(
|
|
iter_function: Callable,
|
|
layout_rows: Array[Array],
|
|
current_keys: Dictionary[Vector2i, Array] = {}
|
|
) -> void:
|
|
_gap_to_size_ratio = key_gap / key_size
|
|
for row: Array[KeyProps] in layout_rows:
|
|
_set_row_key_scales_with_gaps(row)
|
|
|
|
var pos: Vector2 = Vector2.ZERO
|
|
var pivot := Vector2.ZERO
|
|
var angle: float = 0
|
|
var rect: Rect2 = Rect2(0, 0, 0, 0)
|
|
|
|
for row: Array[KeyProps] in layout_rows:
|
|
var result := _iterate_row(
|
|
iter_function, row, pos, pivot, angle, rect, current_keys
|
|
)
|
|
pos = result[0]
|
|
pos.y += 1 + key_gap
|
|
pivot = result[1]
|
|
angle = result[2]
|
|
rect = result[3]
|
|
|
|
layout_size_changed.emit(rect)
|
|
_rect_mesh.scale = Vector3(rect.size.x, 1, rect.size.y)
|
|
_rect_mesh.position.x = rect.position.x + rect.size.x / 2 - rect.get_center().x
|
|
_rect_mesh.position.z = rect.position.x + rect.size.y / 2 - rect.get_center().y
|
|
|
|
|
|
func _iterate_row(
|
|
iter_function: Callable,
|
|
row: Array[KeyProps],
|
|
pos: Vector2,
|
|
pivot: Vector2,
|
|
angle: float,
|
|
rect: Rect2,
|
|
current_keys: Dictionary[Vector2i, Array]
|
|
) -> Array[Variant]:
|
|
pos.x = pivot.x
|
|
|
|
for key_props in row:
|
|
if key_props.has_angle():
|
|
angle = -deg_to_rad(key_props.angle)
|
|
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():
|
|
pos = pivot
|
|
|
|
var key_pos := Vector3(pos.x + key_size / 2, 0, pos.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 = KeyHelper.get_rotated_key_pos(key_pos, pivot, angle)
|
|
|
|
iter_function.call(key_props, key_pos, angle, current_keys)
|
|
|
|
if pos.x < rect.position.x:
|
|
rect.position.x = pos.x
|
|
if pos.y < rect.position.y:
|
|
rect.position.y = pos.y
|
|
|
|
pos.x += key_props.width * key_size + key_props.x * key_size + key_gap
|
|
pos.y += key_props.y * key_size
|
|
|
|
if pos.x - key_gap > rect.end.x:
|
|
rect.end.x = pos.x - key_gap
|
|
if pos.y + key_size * key_props.height > rect.end.y:
|
|
rect.end.y = pos.y + key_size * key_props.height
|
|
|
|
return [pos, pivot, angle, rect]
|
|
|
|
|
|
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
|
|
if key_props.has_pivot_x():
|
|
key_props.pivot_x = (
|
|
_get_scale_with_gaps(key_props.pivot_x) + _gap_to_size_ratio
|
|
)
|
|
if key_props.has_pivot_y():
|
|
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 _generate_editor_keys() -> void:
|
|
_delete_editor_keys()
|
|
_iterate_keys(_generate_key, LayoutANSI.new().get_key_props_rows())
|
|
|
|
|
|
func _delete_editor_keys() -> void:
|
|
pressed_positions = {}
|
|
var nodes := _keys_holder.get_children()
|
|
if not nodes:
|
|
return
|
|
print("deleting keys...")
|
|
for node in nodes:
|
|
node.queue_free()
|
|
|
|
|
|
func _query_keys() -> void:
|
|
print(query_key_by_keycode(KEY_SHIFT))
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
#region layout swapping
|
|
func _swap_layout(kp_key: Key) -> void:
|
|
if kp_key == KEY_KP_0:
|
|
prompt_page = wrapi(prompt_page + 1, 0, prompt_pages_total)
|
|
prompt_page_turned.emit(prompt_page)
|
|
return
|
|
|
|
var index := kp_key - KEY_KP_1 + prompt_page * 9
|
|
if index < 0 or index >= LayoutConfig.layouts.size():
|
|
return
|
|
|
|
var new_layout := (
|
|
(LayoutConfig.layouts.values() as Array[AbstractLayout])[index].get_name()
|
|
)
|
|
|
|
if new_layout == LayoutConfig.current_layout.get_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...")
|
|
|
|
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.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: float,
|
|
current_keys: Dictionary[Vector2i, Array]
|
|
) -> void:
|
|
var dict_key := Vector2i(key_props.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)
|
|
|
|
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.keycode)
|
|
if props_dict:
|
|
key_props.chars_from_dict(props_dict)
|
|
|
|
var game_key := GameKey.instantiate_with_props(
|
|
key_props, key_pos, angle, self, _key_scene
|
|
)
|
|
_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
|
|
_keys_holder.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
|
|
|
|
|
|
#region input prompt
|
|
func _set_prompt_pages() -> void:
|
|
prompt_page = 0
|
|
prompt_pages_total = ceili(LayoutConfig.layouts.size() / 9.0)
|
|
|
|
#endregion
|