@tool class_name GameKey extends Node3D #region variables @export_group("References") @export var player_pos_marker: Node3D @export var _skeleton_primary: Skeleton3D @export var _skeleton_secondary: Skeleton3D @export var _nub_mesh: MeshInstance3D @export var _upper_left_label: Label3D @export var _upper_right_label: Label3D @export var _lower_left_label: Label3D @export var _lower_right_label: Label3D @export var _center_label: Label3D @export var _press_light: OmniLight3D @export var _adjacency_light: OmniLight3D @export var _sfx_player: AudioStreamPlayer3D @export_group("Bones") @export var _bone_top_left: String = "TopLeft" @export var _bone_top_right: String = "TopRight" @export var _bone_bottom_left: String = "BottomLeft" @export var _bone_bottom_right: String = "BottomRight" @export_group("Light") @export_group("Animation") @export var _pos_sod_fzr: Vector3 = Vector3(5, 0.15, 0) @export var _light_fade_time: float = 0.25 @export var _star_pop_offset: Vector3 = Vector3(0, -2.5, 0) @export var _rotation_damp: float = 10 @export_subgroup("Idle") @export var _idle_amplitude: float = 0.4 @export var _idle_frequency := Vector2(0.25, -0.25) @export var _press_offset: float = 0.4 @export_subgroup("Player") @export var _hop_land_offset: float = 0.8 @export_group("SFX") @export var _press_sfx: AudioStream @export var _release_sfx: AudioStream var props: KeyProps var player_transform: Transform3D var _keyboard: GameKeyboard var _is_pressed: bool var _light_timer: float var _init_position: Vector3 var _default_position: Vector3 var _default_rotation: Vector3 var _pos_sod: SecondOrderDynamics @onready var _polyphonic: AudioStreamPlaybackPolyphonic @onready var _light_energy := _press_light.light_energy #endregion #region static static func instantiate_with_props( _props: KeyProps, init_pos: Vector3, default_angle: float, keyboard: GameKeyboard, scene: PackedScene ) -> GameKey: var node := scene.instantiate() as GameKey node._keyboard = keyboard node.load_props(_props, init_pos, default_angle) if _props.keycode != KEY_NONE: node.name += " " + OS.get_keycode_string(_props.keycode) if _props.location != KEY_LOCATION_UNSPECIFIED: node.name += ( " " + ("Left" if _props.location == KEY_LOCATION_LEFT else "Right") ) return node #endregion #region builtins func _ready() -> void: if not _keyboard or not props: return _adjacency_light.visible = false _keyboard.layout_size_changed.connect(_on_keyboard_layout_size_changed) _keyboard.keys_requested.connect(_on_keyboard_keys_requested) _keyboard.is_configuring_changed.connect(_on_keyboard_is_configuring_changed) _keyboard.player_key_changed.connect(_on_keyboard_player_key_changed) _keyboard.player_finished_move.connect(_on_keyboard_player_finished_move) _set_labels() if Engine.is_editor_hint(): return _sfx_player.play() _polyphonic = _sfx_player.get_stream_playback() func _process(delta: float) -> void: if Engine.is_editor_hint(): return _animate(delta) _animate_light(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 not InputEventKey: return var event_key := event as InputEventKey if ( event_key.physical_keycode != props.keycode or event_key.echo or (event_key.physical_keycode == KEY_NONE or props.keycode == KEY_NONE) or ( event_key.location != KEY_LOCATION_UNSPECIFIED and props.location != KEY_LOCATION_UNSPECIFIED and event_key.location != props.location ) ): return _set_pressing_from_event(event_key) if _keyboard.is_configuring and event_key.is_pressed(): var new_char_type: KeyProps.Char if not _keyboard.alt_visual_layout: if not event_key.shift_pressed: new_char_type = KeyProps.Char.MAIN else: new_char_type = KeyProps.Char.SHIFT else: if not event_key.shift_pressed: new_char_type = KeyProps.Char.ALT else: new_char_type = KeyProps.Char.ALT_SHIFT var chars_dict: Dictionary[KeyProps.Char, String] = { new_char_type: char(event_key.unicode).to_upper() } _set_labels(chars_dict) func _exit_tree() -> void: if _keyboard: _erase_keyboard_pressed_position() #endregion #region public func load_props(_props: KeyProps, init_pos: Vector3, default_angle: float) -> void: _init_position = init_pos _default_rotation.y = default_angle if props: _props.chars_from_dict(props.chars_to_dict()) props = _props _skeleton_primary.visible = true _skeleton_secondary.visible = props.has_secondary_rect() _set_bones() _nub_mesh.visible = props.homing_nub func get_default_transform() -> Transform3D: return Transform3D(Quaternion.from_euler(_default_rotation), _default_position) func is_adjacent(to: GameKey) -> bool: return ( KeyHelper.ADJACENCY_MAP.has(props.keycode) and (to.props.keycode in KeyHelper.ADJACENCY_MAP[props.keycode]) ) #endregion #region pressing func _set_pressing_from_event(event: InputEventKey) -> void: _set_pressing(event.is_pressed()) _keyboard.emit_key_press(self, event) func _set_pressing(is_pressed: bool) -> void: if _is_pressed == is_pressed: return _is_pressed = is_pressed _set_keyboard_pressed_position(_is_pressed) if _is_pressed: _play_sfx(_press_sfx) else: _play_sfx(_release_sfx) func _set_keyboard_pressed_position(pressed: bool) -> void: _keyboard.pressed_positions[_default_position] = pressed func _erase_keyboard_pressed_position() -> void: _keyboard.pressed_positions.erase(_default_position) #endregion #region labels func _set_labels(chars_dict: Dictionary[KeyProps.Char, String] = {}) -> void: _upper_left_label.text = "" _upper_right_label.text = "" _lower_left_label.text = "" _lower_right_label.text = "" _center_label.text = "" _set_labels_text(chars_dict) _upper_left_label.visible = _upper_left_label.text != "" _upper_right_label.visible = _upper_right_label.text != "" _lower_left_label.visible = _lower_left_label.text != "" _lower_right_label.visible = _lower_right_label.text != "" _center_label.visible = _center_label.text != "" func _set_labels_text(chars_dict: Dictionary[KeyProps.Char, String]) -> void: if Engine.is_editor_hint() or props.keycode == KEY_SPACE or not props.is_unicode(): _center_label.text = OS.get_keycode_string(props.keycode) return if chars_dict: props.chars_from_dict(chars_dict, false) if props.main_char and not props.shift_char: _upper_left_label.text = props.main_char if props.main_char and props.shift_char: _upper_left_label.text = props.shift_char _lower_left_label.text = props.main_char if not props.main_char and props.shift_char: _upper_left_label.text = props.shift_char if props.alt_char: _lower_right_label.text = props.alt_char if props.alt_shift_char: _upper_right_label.text = props.alt_shift_char #endregion #region bones func _set_bones() -> void: var rect := Rect2( -(_keyboard.key_size * props.width) / 2, -_keyboard.key_size / 2, _keyboard.key_size * props.width, _keyboard.key_size * props.height ) var rect_center := rect.get_center() _center_label.position.x = rect_center.x _center_label.position.z = rect_center.y _set_bones_from_rect(rect, _skeleton_primary) if props.has_secondary_rect(): var rect_secondary := Rect2( rect.position.x + props.x2 * _keyboard.key_size, rect.position.y + props.y2 * _keyboard.key_size, _keyboard.key_size * props.width2, _keyboard.key_size * props.height2 ) _set_bones_from_rect(rect_secondary, _skeleton_secondary) func _set_bones_from_rect(rect: Rect2, skeleton: Skeleton3D) -> void: var top_left_idx := skeleton.find_bone(_bone_top_left) skeleton.set_bone_pose_position( top_left_idx, Vector3(rect.position.x, 0, rect.position.y) ) var top_right_idx := skeleton.find_bone(_bone_top_right) skeleton.set_bone_pose_position( top_right_idx, Vector3(rect.end.x, 0, rect.position.y) ) var bottom_left_idx := skeleton.find_bone(_bone_bottom_left) skeleton.set_bone_pose_position( bottom_left_idx, Vector3(rect.position.x, 0, rect.end.y) ) var bottom_right_idx := skeleton.find_bone(_bone_bottom_right) skeleton.set_bone_pose_position( bottom_right_idx, Vector3(rect.end.x, 0, rect.end.y) ) #endregion #region animation func _reset_animations() -> void: _pos_sod = SecondOrderDynamics.new( _pos_sod_fzr, _default_position + _star_pop_offset ) func _animate(delta: float) -> void: position = _pos_sod.process(delta, _animate_position()) rotation = lerp(rotation, _default_rotation, delta * _rotation_damp) func _animate_position() -> Vector3: var new_position := _default_position new_position.y += ( sin( ( _keyboard.anim_time + (_default_position.x / _keyboard.key_size) * _idle_frequency.x + (_default_position.z / _keyboard.key_size) * _idle_frequency.y ) ) * _idle_amplitude * _keyboard.key_size ) new_position.y -= (_press_offset * _keyboard.key_size) if _is_pressed else 0.0 return new_position func _shake(force: float) -> void: _pos_sod.y = ( _pos_sod.y + ( Vector3( randf_range(-force, force), randf_range(-force, force), randf_range(-force, force) ) * _keyboard.key_size ) ) func _push(force: Vector3) -> void: _pos_sod.y = (_pos_sod.y + force * _keyboard.key_size) func _push_radial(force: float) -> void: _pos_sod.y = (Vector3( _pos_sod.y.x * (force + 1), _pos_sod.y.y, _pos_sod.y.z * (force + 1) )) func _animate_light(delta: float) -> void: if _is_pressed: _light_timer = _light_fade_time if _light_timer <= 0: _press_light.visible = false return _press_light.visible = true _press_light.light_energy = (_light_timer / _light_fade_time) * _light_energy _light_timer -= delta #endregion #region sounds func _play_sfx(stream: AudioStream) -> void: _polyphonic.play_stream(stream) #endregion #region event handlers func _on_keyboard_layout_size_changed(rect: Rect2) -> void: _erase_keyboard_pressed_position() var center := rect.get_center() _default_position = _init_position _default_position.x -= center.x _default_position.z -= center.y _set_keyboard_pressed_position(_is_pressed) if Engine.is_editor_hint(): position = _default_position return if not _pos_sod: _reset_animations() func _on_keyboard_keys_requested(query_func: Callable) -> void: if query_func.call(self): _keyboard.key_query_respond(self) func _on_keyboard_is_configuring_changed(value: bool) -> void: if value: _push_radial(0.2) else: _shake(0.2) _push_radial(-0.05) func _on_keyboard_player_key_changed(game_key: GameKey) -> void: _adjacency_light.visible = is_adjacent(game_key) func _on_keyboard_player_finished_move(game_key: GameKey) -> void: if game_key == self: _push(Vector3.DOWN * _hop_land_offset) #endregion