keebie/scripts/game_key.gd
2025-07-29 19:40:36 +10:00

266 lines
6.0 KiB
GDScript

class_name GameKey extends Node3D
#region variables
static var _scene := preload("res://scenes/game_key.tscn")
@export_group("Node references")
@export var _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 _sfx_player: AudioStreamPlayer3D
@export_group("Light")
@export var _light_energy: float = 3
@export var _light_fade_time: float = 0.25
@export_group("Animation")
@export var _pos_sod_fzr: Vector3 = Vector3(5, 0.15, 4)
@export_subgroup("Idle")
@export var _idle_amplitude: float = 0.1
@export var _idle_frequency_x: float = 1
@export var _idle_frequency_z: float = -1
@export_group("Pressing")
@export var _press_offset: float = 0.1
@export_group("SFX")
@export var _press_sfx: AudioStream
@export var _release_sfx: AudioStream
var props: KeyProps
var keyboard: GameKeyboard
var input_event_init: InputEventKey
var _is_pressed: bool
var _input_event_main: InputEventKey
var _input_event_shift: InputEventKey
var _input_event_alt: InputEventKey
var _input_event_alt_shift: InputEventKey
var _light_timer: float
var _default_position: Vector3
@onready
var _polyphonic := _sfx_player.get_stream_playback() as AudioStreamPlaybackPolyphonic
@onready var _pos_sod := SecondOrderDynamics.new(_pos_sod_fzr, _default_position)
#endregion
#region static methods
static func instantiate_with_props(
_props: KeyProps, default_position: Vector3, _keyboard: GameKeyboard
) -> GameKey:
var input_event := InputEventKey.new()
input_event.physical_keycode = _props.physical_keycode
input_event.location = _props.location
var node := _scene.instantiate() as GameKey
node.load_props(_props, default_position)
node.keyboard = _keyboard
node.input_event_init = input_event
node.name = node.name + " " + node.input_event_init.as_text_physical_keycode()
if _props.location != KEY_LOCATION_UNSPECIFIED:
node.name += " " + node.input_event_init.as_text_location()
return node
#endregion
#region builtins
func _ready() -> void:
_set_labels()
func _process(delta: float) -> void:
_animate(delta)
_animate_light(delta)
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("reset_animations"):
_pos_sod = SecondOrderDynamics.new(_pos_sod_fzr, _default_position)
return
if event is not InputEventKey:
return
var event_key := event as InputEventKey
if (
event_key.physical_keycode != props.physical_keycode
or (
event_key.location != KEY_LOCATION_UNSPECIFIED
and event_key.location != props.location
)
or event_key.echo
):
return
_is_pressed = event_key.is_pressed()
if _is_pressed:
_play_sfx(_press_sfx)
else:
_play_sfx(_release_sfx)
keyboard.pressed_positions[_default_position] = _is_pressed
if LayoutConfig.is_configuring and _is_pressed:
if not keyboard.alt_layout:
if not event_key.shift_pressed:
_input_event_main = event_key
else:
_input_event_shift = event_key
else:
if not event_key.shift_pressed:
_input_event_alt = event_key
else:
_input_event_alt_shift = event_key
_set_labels()
#endregion
#region public
func load_props(_props: KeyProps, default_position: Vector3) -> void:
_default_position = default_position
if props:
_props.from_dictionary(props.to_dictionary())
props = _props
_mesh.scale.x = props.width_ratio
#endregion
#region labels
func _set_labels() -> void:
_upper_left_label.text = ""
_upper_right_label.text = ""
_lower_left_label.text = ""
_lower_right_label.text = ""
_center_label.text = ""
_set_labels_text()
_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() -> void:
props.main_char = (
char(_input_event_main.unicode).to_upper()
if _input_event_main
else props.main_char
)
props.shift_char = (
char(_input_event_shift.unicode).to_upper()
if _input_event_shift
else props.shift_char
)
props.alt_char = (
char(_input_event_alt.unicode).to_upper()
if _input_event_alt
else props.alt_char
)
props.alt_shift_char = (
char(_input_event_alt_shift.unicode).to_upper()
if _input_event_alt_shift
else props.alt_shift_char
)
if not props.is_char():
_center_label.text = input_event_init.as_text_physical_keycode()
return
if (
(props.main_char and not props.shift_char)
or props.main_char == props.shift_char
):
_upper_left_label.text = props.main_char
if (props.main_char and props.shift_char) and props.main_char != 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)
and props.main_char != props.shift_char
):
_upper_left_label.text = props.shift_char
if props.alt_char and props.alt_char != props.main_char:
_lower_right_label.text = props.alt_char
if (
props.alt_shift_char
and props.alt_shift_char != props.alt_char
and props.alt_shift_char != props.shift_char
):
_upper_right_label.text = props.alt_shift_char
#endregion
#region animation
func _animate(delta: float) -> void:
position = _pos_sod.process(delta, _animate_position())
func _animate_position() -> Vector3:
var new_position := _default_position
new_position.y += (
sin(
(
keyboard.anim_time
+ position.x * _idle_frequency_x
+ position.z * _idle_frequency_z
)
)
* _idle_amplitude
)
new_position.y -= _press_offset if _is_pressed else 0.0
return new_position
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