keebie/scripts/game_key.gd

441 lines
11 KiB
GDScript

@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