441 lines
11 KiB
GDScript
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
|