diff --git a/project.godot b/project.godot index affcf3e..2b31daa 100644 --- a/project.godot +++ b/project.godot @@ -187,12 +187,12 @@ select_putter={ } club_next={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194306,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } club_previous={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194306,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } ball_next={ @@ -205,6 +205,11 @@ ball_previous={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":71,"key_label":0,"unicode":71,"location":0,"echo":false,"script":null) ] } +fast_forward={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null) +] +} pause={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) diff --git a/src/game/game.gd b/src/game/game.gd index 2d2aaf9..7c1aac2 100644 --- a/src/game/game.gd +++ b/src/game/game.gd @@ -81,6 +81,8 @@ func _unload_content() -> void: func _finish_scene_load(instance: Node) -> void: # Unpause in case the previous scene was paused. get_tree().paused = false + # Reset time scale in case it's been changed. + Engine.time_scale = 1.0 content.add_child(instance) instance.reparent(content) diff --git a/src/game/game_settings.gd b/src/game/game_settings.gd index e87c83c..2ea1be6 100644 --- a/src/game/game_settings.gd +++ b/src/game/game_settings.gd @@ -21,6 +21,8 @@ var projection_gravity: bool var default_text_speed: float +var default_physics_ticks_per_second: int + func _init() -> void: ProjectSettings.settings_changed.connect(_read_settings) @@ -54,6 +56,9 @@ func _read_settings() -> void: ) default_text_speed = ProjectSettings.get_setting("game/config/text/default_text_speed") + default_physics_ticks_per_second = ProjectSettings.get_setting( + "physics/common/physics_ticks_per_second" + ) func _load_audio_bus_override() -> void: diff --git a/src/player/shot_setup/shot_setup.gd b/src/player/shot_setup/shot_setup.gd index 6c2f1dc..edffa8a 100644 --- a/src/player/shot_setup/shot_setup.gd +++ b/src/player/shot_setup/shot_setup.gd @@ -485,6 +485,8 @@ func _on_phase_change(new_phase: Phase) -> void: idle_prompt_timer.start() hud.hide_idle_prompts() + world.fast_forward = false + match new_phase: Phase.AIM: hud.show_hud() @@ -660,6 +662,9 @@ func _process(_delta: float) -> void: hud.stop_curve_bar() phase = Phase.DOWNSWING Phase.SHOT: + # Fast-forward when input is held: + world.fast_forward = Input.is_action_pressed("fast_forward", true) + if reset_enabled and Input.is_action_just_pressed("shot_reset", true): phase = Phase.SHOT_RESET reset_enabled = false @@ -750,3 +755,5 @@ func _on_idle_prompt_timer_timeout() -> void: hud.show_power_prompt() Phase.CURVE_ADJUST: hud.show_curve_prompt() + Phase.SHOT: + hud.show_shot_prompt() diff --git a/src/shaders/vcr_distortion.gdshader b/src/shaders/vcr_distortion.gdshader new file mode 100644 index 0000000..deb7ec6 --- /dev/null +++ b/src/shaders/vcr_distortion.gdshader @@ -0,0 +1,108 @@ +// https://www.shadertoy.com/view/MdffD7 +// Fork of FMS_Cat's VCR distortion shader +// Retrieved from https://godotshaders.com/shader/vhs-post-processing/ + +shader_type canvas_item; + +// TODO: Add uniforms for tape crease discoloration and image jiggle + +uniform sampler2D screen_texture: hint_screen_texture, filter_linear_mipmap, repeat_disable; + +uniform vec2 vhs_resolution = vec2(320.0, 240.0); + +uniform int samples = 2; +uniform float crease_noise: hint_range(0.0, 2.0, 0.1) = 1.0; +uniform float crease_opacity: hint_range(0.0, 1.0, 0.1) = 0.5; +uniform float filter_intensity: hint_range(0.0, 1.0, 0.1) = 0.1; + +group_uniforms tape_crease; +uniform float tape_crease_smear: hint_range(0.0, 2.0, 0.1) = 0.2; +uniform float tape_crease_intensity: hint_range(0.0, 1.0, 0.1) = 0.2; +uniform float tape_crease_jitter: hint_range(0.0, 1.0, 0.01) = 0.10; +uniform float tape_crease_speed: hint_range(-2.0, 2.0, 0.1) = 0.5; +uniform float tape_crease_discoloration: hint_range(0.0, 2.0, 0.1) = 1.0; + +group_uniforms bottom_border; +uniform float bottom_border_thickness: hint_range(0.0,32.0, 0.1) = 6.0; +uniform float bottom_border_jitter: hint_range(0.0, 24.0, 0.5) = 6.0; + +group_uniforms noise; +uniform float noise_intensity: hint_range(0.0, 1.0, 0.1) = 0.1; +uniform sampler2D noise_texture: filter_linear_mipmap, repeat_enable; + +float v2random(vec2 uv) { + return texture(noise_texture, mod(uv, vec2(1.0))).x; +} + +mat2 rotate2D(float t) { + return mat2(vec2(cos(t), sin(t)), vec2(-sin(t), cos(t))); +} + +vec3 rgb2yiq(vec3 rgb) { + return mat3(vec3(0.299, 0.596, 0.211), vec3(0.587, -0.274, -0.523), vec3(0.114, -0.322, 0.312)) * rgb; +} + +vec3 yiq2rgb(vec3 yiq) { + return mat3(vec3(1.0, 1.0, 1.0), vec3(0.956, -0.272, -1.106), vec3(0.621, -0.647, 1.703)) * yiq; +} + +vec3 vhx_tex_2D(sampler2D tex, vec2 uv, float rot) { + vec3 yiq = vec3(0.0); + for (int i = 0; i < samples; i++) { + yiq += rgb2yiq(texture(tex, uv - vec2(float(i), 0.0) / vhs_resolution).xyz) * + vec2(float(i), float(samples - 1 - i)).yxx / float(samples - 1) + / float(samples) * 2.0; + } + if (rot != 0.0) { + yiq.yz *= rotate2D(rot * tape_crease_discoloration); + } + return yiq2rgb(yiq); +} + +void fragment() { + vec2 uvn = UV; + vec3 col = vec3(0.0, 0.0, 0.0); + + // Tape wave. + uvn.x += (v2random(vec2(uvn.y / 10.0, TIME / 10.0) / 1.0) - 0.5) / vhs_resolution.x * 1.0; + uvn.x += (v2random(vec2(uvn.y, TIME * 10.0)) - 0.5) / vhs_resolution.x * 1.0; + + // tape crease + float tc_phase = smoothstep(0.9, 0.96, sin(uvn.y * 8.0 - (TIME * tape_crease_speed + tape_crease_jitter * v2random(TIME * vec2(0.67, 0.59))) * PI * 1.2)); + float tc_noise = smoothstep(0.3, 1.0, v2random(vec2(uvn.y * 4.77, TIME))); + float tc = tc_phase * tc_noise; + uvn.x = uvn.x - tc / vhs_resolution.x * 8.0 * tape_crease_smear; + + // switching noise + float sn_phase = smoothstep(1.0 - bottom_border_thickness / vhs_resolution.y, 1.0, uvn.y); + uvn.x += sn_phase * (v2random(vec2(UV.y * 100.0, TIME * 10.0)) - 0.5) / vhs_resolution.x * bottom_border_jitter; + + // fetch + col = vhx_tex_2D(screen_texture, uvn, tc_phase * 0.2 + sn_phase * 2.0); + + // crease noise + float cn = tc_noise * crease_noise * (0.7 * tc_phase * tape_crease_intensity + 0.3); + if (0.29 < cn) { + vec2 V = vec2(0.0, crease_opacity); + vec2 uvt = (uvn + V.yx * v2random(vec2(uvn.y, TIME))) * vec2(0.1, 1.0); + float n0 = v2random(uvt); + float n1 = v2random(uvt + V.yx / vhs_resolution.x); + if (n1 < n0) { + col = mix(col, 2.0 * V.yyy, pow(n0, 10.0)); + } + } + + // ac beat + col *= 1.0 + 0.1 * smoothstep(0.4, 0.6, v2random(vec2(0.0, 0.1 * (UV.y + TIME * 0.2)) / 10.0)); + + // color noise + col *= 1.0 - noise_intensity * 0.5 + noise_intensity * texture(noise_texture, mod(uvn * vec2(1.0, 1.0) + TIME * vec2(5.97, 4.45), vec2(1.0))).xyz; + col = clamp(col, 0.0, 1.0); + + // yiq + col = rgb2yiq(col); + col = vec3(0.9, 1.1, 1.5) * col + vec3(0.1, -0.1, 0.0) * filter_intensity; + col = yiq2rgb(col); + + COLOR = vec4(col, 1.0); +} \ No newline at end of file diff --git a/src/ui/menus/pause_menu/pause_menu.gd b/src/ui/menus/pause_menu/pause_menu.gd index 08bcd5c..de0e67d 100644 --- a/src/ui/menus/pause_menu/pause_menu.gd +++ b/src/ui/menus/pause_menu/pause_menu.gd @@ -16,6 +16,7 @@ var _freeze_input := false func _ready() -> void: Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) + Engine.time_scale = 1.0 func _unhandled_key_input(event: InputEvent) -> void: diff --git a/src/ui/shot_hud/shot_hud.gd b/src/ui/shot_hud/shot_hud.gd index 498593c..eb4bf31 100644 --- a/src/ui/shot_hud/shot_hud.gd +++ b/src/ui/shot_hud/shot_hud.gd @@ -17,6 +17,7 @@ var player: WorldPlayer var _aim_prompt_shown := false var _power_prompt_shown := false var _curve_prompt_shown := false +var _shot_prompt_shown := false var _free_cam_prompts_shown := false @onready var power_bar: TextureProgressBar = %PowerBar @@ -45,6 +46,7 @@ var _free_cam_prompts_shown := false @onready var _aim_prompt_animation: AnimationPlayer = %AimPromptAnimation @onready var _power_prompt_animation: AnimationPlayer = %PowerPromptAnimation @onready var _curve_prompt_animation: AnimationPlayer = %CurvePromptAnimation +@onready var _shot_prompt_animation: AnimationPlayer = %ShotPromptAnimation @onready var _free_cam_util_prompt_animation: AnimationPlayer = %FreeCamUtilPromptAnimation @onready var _free_cam_motion_prompt_animation: AnimationPlayer = %FreeCamMotionPromptAnimation @@ -181,10 +183,23 @@ func hide_free_cam_prompts() -> void: _free_cam_util_prompt_animation.play_backwards("show") +func show_shot_prompt() -> void: + if not _shot_prompt_shown: + _shot_prompt_shown = true + _shot_prompt_animation.play("show") + + +func hide_shot_prompt() -> void: + if _shot_prompt_shown: + _shot_prompt_shown = false + _shot_prompt_animation.play_backwards("show") + + func hide_idle_prompts() -> void: hide_aim_prompt() hide_power_prompt() hide_curve_prompt() + hide_shot_prompt() ## Set the value of the life bar, potentially playing some kind of effect in response. diff --git a/src/ui/shot_hud/shot_hud.tscn b/src/ui/shot_hud/shot_hud.tscn index b227964..f1f7ebb 100644 --- a/src/ui/shot_hud/shot_hud.tscn +++ b/src/ui/shot_hud/shot_hud.tscn @@ -1242,6 +1242,30 @@ libraries = { "": SubResource("AnimationLibrary_o70c6") } +[node name="ShotPrompt" type="MarginContainer" parent="Prompts"] +layout_mode = 1 +anchors_preset = -1 +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_bottom = 55.0 +grow_horizontal = 2 +grow_vertical = 0 +theme_override_constants/margin_right = 16 +theme_override_constants/margin_bottom = 16 + +[node name="InputPrompt" parent="Prompts/ShotPrompt" instance=ExtResource("14_ik4gg")] +layout_mode = 2 +text = "❓ - ACTION_fast_forward" +action = &"fast_forward" + +[node name="ShotPromptAnimation" type="AnimationPlayer" parent="Prompts/ShotPrompt"] +unique_name_in_owner = true +libraries = { +"": SubResource("AnimationLibrary_o70c6") +} + [node name="FreeCamUtilPrompts" type="MarginContainer" parent="Prompts"] layout_mode = 1 anchors_preset = -1 diff --git a/src/ui/world_ui.gd b/src/ui/world_ui.gd index f38f692..18e8c57 100644 --- a/src/ui/world_ui.gd +++ b/src/ui/world_ui.gd @@ -52,6 +52,9 @@ func _finish_winner_sequence() -> void: func pause() -> void: + # Turn off fast-forward, if it's on + world.fast_forward = false + # Switch to demo cam, if there is one. var democams: Array[Node] = get_tree().get_nodes_in_group(DEMO_CAMERA_GROUP) if democams: diff --git a/src/world/world.gd b/src/world/world.gd index 718c763..fd3dec2 100644 --- a/src/world/world.gd +++ b/src/world/world.gd @@ -6,18 +6,23 @@ class_name World extends Node const SCENE := "res://src/world/world.tscn" +const FF_TIME_SCALE := 4.0 + @export var initial_level: PackedScene @export var manager: PlayManager @export var spawn_group := "PlayerSpawn" +var fast_forward: bool: + set = _set_fast_forward var _spawns_available: Array[Node3D] = [] @onready var level: Node3D = %Level @onready var ui: WorldUI = %UI @onready var world_transition: AnimationPlayer = %WorldTransition +@onready var fast_forward_effect: Control = %FastForwardEffect @onready var game: Game = get_tree().get_first_node_in_group(Game.group) @@ -62,6 +67,26 @@ func _on_winner(_player: WorldPlayer) -> void: # TODO announce winner? +## Set the state of the world-wide fast-forward feature +func _set_fast_forward(value: bool) -> void: + if value == fast_forward: + # Ignore if not a change from the previous value + return + + fast_forward = value + + if fast_forward: + Engine.time_scale = FF_TIME_SCALE + Engine.physics_ticks_per_second = int( + Game.settings.default_physics_ticks_per_second * FF_TIME_SCALE + ) + fast_forward_effect.show() + else: + Engine.time_scale = 1.0 + Engine.physics_ticks_per_second = Game.settings.default_physics_ticks_per_second + fast_forward_effect.hide() + + func fade_to_title() -> void: world_transition.play("fade_to_title") diff --git a/src/world/world.tscn b/src/world/world.tscn index 33d8b3f..162e5eb 100644 --- a/src/world/world.tscn +++ b/src/world/world.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=16 format=3 uid="uid://cwnwcd8kushl3"] +[gd_scene load_steps=24 format=3 uid="uid://cwnwcd8kushl3"] [ext_resource type="Script" path="res://src/world/world.gd" id="1_ybjyx"] [ext_resource type="Script" path="res://src/player/world_player.gd" id="2_e743i"] @@ -10,6 +10,8 @@ [ext_resource type="PackedScene" uid="uid://byvjsvavbg5xe" path="res://src/ui/menus/pause_menu/pause_menu.tscn" id="7_0gd42"] [ext_resource type="PackedScene" uid="uid://biokiug3e0ipk" path="res://src/ui/shot_hud/death_alert.tscn" id="8_fuyxc"] [ext_resource type="PackedScene" uid="uid://dwyy7tt3nose1" path="res://src/ui/shot_hud/winner_alert.tscn" id="9_lln1k"] +[ext_resource type="FontFile" uid="uid://dwy8k2w7vt64x" path="res://assets/fonts/classic-better-vcr/classic-better-vcr.otf" id="11_uv5b5"] +[ext_resource type="Shader" path="res://src/shaders/vcr_distortion.gdshader" id="11_xag32"] [sub_resource type="Resource" id="Resource_mbhdy"] script = ExtResource("2_e743i") @@ -26,6 +28,68 @@ _balls = { script = ExtResource("5_h6mje") players = Array[ExtResource("2_e743i")]([ExtResource("3_pyw81"), SubResource("Resource_mbhdy")]) +[sub_resource type="Animation" id="Animation_xfpfm"] +resource_name = "blink" +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:visible") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.5), +"transitions": PackedFloat32Array(1, 1), +"update": 1, +"values": [true, false] +} + +[sub_resource type="Animation" id="Animation_6350f"] +length = 0.001 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:visible") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [true] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_7gn8c"] +_data = { +"RESET": SubResource("Animation_6350f"), +"blink": SubResource("Animation_xfpfm") +} + +[sub_resource type="FastNoiseLite" id="FastNoiseLite_txttc"] +noise_type = 5 +frequency = 1.0 + +[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_p8oo3"] +seamless = true +noise = SubResource("FastNoiseLite_txttc") + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_m76ua"] +shader = ExtResource("11_xag32") +shader_parameter/vhs_resolution = Vector2(320, 240) +shader_parameter/samples = 3 +shader_parameter/crease_noise = 1.4 +shader_parameter/crease_opacity = 0.5 +shader_parameter/filter_intensity = 0.2 +shader_parameter/tape_crease_smear = 0.2 +shader_parameter/tape_crease_intensity = 0.2 +shader_parameter/tape_crease_jitter = 0.21 +shader_parameter/tape_crease_speed = 0.5 +shader_parameter/tape_crease_discoloration = 1.0 +shader_parameter/bottom_border_thickness = 6.0 +shader_parameter/bottom_border_jitter = 6.0 +shader_parameter/noise_intensity = 0.2 +shader_parameter/noise_texture = SubResource("NoiseTexture2D_p8oo3") + [sub_resource type="Animation" id="Animation_ihq1m"] length = 0.001 tracks/0/type = "value" @@ -143,6 +207,55 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 +[node name="FastForwardEffect" type="Control" parent="UI/EffectContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="MarginContainer" type="MarginContainer" parent="UI/EffectContainer/FastForwardEffect"] +layout_mode = 0 +offset_right = 176.0 +offset_bottom = 96.0 +theme_override_constants/margin_left = 128 +theme_override_constants/margin_top = 128 + +[node name="HBoxContainer" type="HBoxContainer" parent="UI/EffectContainer/FastForwardEffect/MarginContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="UI/EffectContainer/FastForwardEffect/MarginContainer/HBoxContainer"] +layout_mode = 2 +theme_override_fonts/font = ExtResource("11_uv5b5") +theme_override_font_sizes/font_size = 96 +text = "FF" + +[node name="Label2" type="Label" parent="UI/EffectContainer/FastForwardEffect/MarginContainer/HBoxContainer"] +layout_mode = 2 +theme_override_fonts/font = ExtResource("11_uv5b5") +theme_override_font_sizes/font_size = 128 +text = "⏩" + +[node name="AnimationPlayer" type="AnimationPlayer" parent="UI/EffectContainer/FastForwardEffect/MarginContainer/HBoxContainer"] +libraries = { +"": SubResource("AnimationLibrary_7gn8c") +} +autoplay = "blink" +speed_scale = 0.25 + +[node name="Overlay" type="ColorRect" parent="UI/EffectContainer/FastForwardEffect"] +material = SubResource("ShaderMaterial_m76ua") +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + [node name="PauseContainer" type="Control" parent="UI"] unique_name_in_owner = true layout_mode = 1