@tool class_name GameKeyboard extends Node3D #region variables signal key_pressed(game_key: GameKey) signal key_input_event_emitted(game_key: GameKey, event: InputEventKey) signal layout_changed(rect: Rect2) signal prompt_page_turned(page: int) signal keys_queried(query_func: Callable) signal configuring_changed(is_configuring: bool) signal player_current_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_editor_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 parameters") @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 _anim_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_rot: 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_game_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) configuring_changed.emit(is_configuring) return if event is not InputEventKey: return var event_key := event as InputEventKey if event_key.echo or not event_key.is_pressed(): return if ( is_configuring and event_key.physical_keycode >= KEY_KP_0 and event_key.physical_keycode <= KEY_KP_9 ): _change_layout(event_key.physical_keycode) return var keycode := event_key.get_physical_keycode_with_modifiers() if 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 query_keys(query_func: Callable, limit: int = 0) -> Array[GameKey]: _queried_keys = [] _queried_keys_limit = limit keys_queried.emit(query_func) var queried_keys := _queried_keys _queried_keys = [] _queried_keys_limit = 0 queried_keys.sort_custom(KeyHelper.game_key_sort) return queried_keys 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_by_keycodes(keycodes: Array[Key]) -> Array[GameKey]: return query_keys( func(game_key: GameKey) -> bool: return game_key.props.keycode in keycodes ) 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) func emit_key_pressed(game_key: GameKey) -> void: key_pressed.emit(game_key) func emit_key_input_event_emitted(game_key: GameKey, event: InputEventKey) -> void: key_input_event_emitted.emit(game_key, event) func emit_player_current_key_changed(game_key: GameKey) -> void: player_current_key_changed.emit(game_key) func emit_player_finished_move(game_key: GameKey) -> void: player_finished_move.emit(game_key) #endregion #region key generation func _generate_game_keys() -> void: print("generating keys...") _iterate_key_props_rows(_generate_game_key, LayoutConfig.layout_key_props_rows) func _generate_game_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_key_props_rows( iter_func: Callable, key_props_rows: Array[Array], current_keys: Dictionary[Vector2i, Array] = {} ) -> void: _gap_to_size_ratio = key_gap / key_size for key_props_row: Array[KeyProps] in key_props_rows: _set_row_key_scales_with_gaps(key_props_row) var rect := KeyHelper.iterate_key_props_rows( iter_func, key_props_rows, key_size, key_gap, current_keys ) layout_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 _set_row_key_scales_with_gaps(key_props_row: Array[KeyProps]) -> void: for key_props in key_props_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) #endregion #region layout swapping func _change_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 layout_idx := kp_key - KEY_KP_1 + prompt_page * 9 if layout_idx < 0 or layout_idx >= LayoutConfig.layouts.size(): return var new_layout_name := ( (LayoutConfig.layouts.values() as Array[AbstractLayout])[layout_idx].get_name() ) if new_layout_name == LayoutConfig.current_layout.get_name(): return LayoutConfig.change_layout(new_layout_name) _regenerate_keys(LayoutConfig.layout_key_props_rows) _play_sfx(_layout_swap_sfx) func _regenerate_keys(key_props_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_key_props_rows(_regenerate_key, key_props_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 chars_dict := LayoutConfig.get_key_config_dict(key_props.keycode) if chars_dict and chars_dict is Dictionary: key_props.chars_from_dict(chars_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_rot) func _animate(delta: float) -> void: anim_time += delta * _anim_time_scale _keys_holder.rotation = _rot_sod.process(delta, _animate_rotation()) func _animate_rotation() -> Vector3: var new_rotation := _default_rot 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 prompt func _set_prompt_pages() -> void: prompt_page = 0 prompt_pages_total = ceili(LayoutConfig.layouts.size() / 9.0) #endregion #region editor func _generate_editor_keys() -> void: _delete_editor_keys() _iterate_key_props_rows(_generate_game_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_editor_keys() -> void: print(query_key_by_keycode(KEY_SHIFT)) #endregion