diff --git a/fish/bluegill.tres b/fish/bluegill.tres index b44c46a..572d202 100644 --- a/fish/bluegill.tres +++ b/fish/bluegill.tres @@ -1,8 +1,10 @@ -[gd_resource type="Resource" load_steps=2 format=3] +[gd_resource type="Resource" load_steps=3 format=3] [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] +[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [resource] script = ExtResource("1_fish_data") id = &"bluegill" display_name = "Bluegill" +catch_profile = ExtResource("2_profile") diff --git a/fish/fish_data.gd b/fish/fish_data.gd index cff104a..2ab3994 100644 --- a/fish/fish_data.gd +++ b/fish/fish_data.gd @@ -1,6 +1,11 @@ class_name FishData extends Resource +const CatchDifficultyProfileType = preload( + "res://fishing/catch_difficulty_profile.gd" +) + @export var id: StringName @export var display_name: String @export var icon: Texture2D +@export var catch_profile: CatchDifficultyProfileType diff --git a/fishing/catch_controller.gd b/fishing/catch_controller.gd new file mode 100644 index 0000000..29ab699 --- /dev/null +++ b/fishing/catch_controller.gd @@ -0,0 +1,314 @@ +class_name CatchController +extends Node + +const CatchDifficultyProfileType = preload( + "res://fishing/catch_difficulty_profile.gd" +) + +signal encounter_updated( + progress: float, + chase_progress: float, + barrier_positions: PackedFloat32Array, + barrier_health: PackedInt32Array, + barrier_max_health: PackedInt32Array, + active_barrier_index: int, + visible: bool, +) +signal caught +signal escaped + +enum CatchState { + IDLE, + REELING, + BLOCKED_BY_BARRIER, + CAUGHT, + ESCAPED, +} + +class Barrier: + extends RefCounted + + var position: float + var health: int + var maximum_health: int + var defeated: bool = false + + func _init( + barrier_position: float, + barrier_health: int, + ) -> void: + position = barrier_position + health = barrier_health + maximum_health = barrier_health + + +@export_category("Accessibility") +@export var auto_click_enabled: bool = false +@export_range(0.05, 2.0, 0.01) var auto_click_interval: float = 0.20 + +@export_category("Testing") +@export var use_deterministic_test_seed: bool = false +@export var deterministic_test_seed: int = 12345 + +var state: CatchState = CatchState.IDLE +var progress: float = 0.0 +var chase_progress: float = 0.0 +var encounter_seed: int = 0 + +var _barriers: Array[Barrier] = [] +var _active_barrier_index: int = -1 +var _reel_input_held: bool = false +var _reel_speed: float = 0.0 +var _click_power: int = 1 +var _chase_start_delay: float = 0.0 +var _chase_delay_remaining: float = 0.0 +var _chase_speed: float = 0.0 +var _failure_epsilon: float = 0.0001 +var _auto_click_accumulator: float = 0.0 +var _rng: RandomNumberGenerator = RandomNumberGenerator.new() + + +func _process(delta: float) -> void: + match state: + CatchState.REELING: + _update_free_reeling(delta) + CatchState.BLOCKED_BY_BARRIER: + _update_auto_click(delta) + _: + return + + if state in [CatchState.CAUGHT, CatchState.ESCAPED, CatchState.IDLE]: + return + if progress >= 1.0: + _resolve_catch() + return + + _update_chase(delta) + if _is_chase_failure(): + _resolve_escape() + return + _emit_encounter_update() + + +func start_encounter( + profile: CatchDifficultyProfileType, + reel_speed: float, + click_power: int, +) -> void: + reset() + if profile == null: + _resolve_escape() + return + + _reel_speed = maxf(reel_speed, 0.0) + _click_power = maxi(click_power, 1) + _chase_start_delay = maxf(profile.chase_start_delay, 0.0) + _chase_delay_remaining = _chase_start_delay + chase_progress = -maxf(profile.chase_start_offset, 0.01) + _chase_speed = maxf(profile.chase_speed, 0.0) + _seed_encounter_rng() + _generate_barriers(profile) + progress = 0.0 + state = CatchState.REELING + _emit_encounter_update() + + +func set_reel_input(held: bool) -> void: + _reel_input_held = held + if not held: + _auto_click_accumulator = 0.0 + + +func handle_primary_pressed() -> void: + if state not in [CatchState.REELING, CatchState.BLOCKED_BY_BARRIER]: + return + _reel_input_held = true + if state == CatchState.BLOCKED_BY_BARRIER and not auto_click_enabled: + _damage_active_barrier() + + +func reset() -> void: + state = CatchState.IDLE + progress = 0.0 + chase_progress = 0.0 + encounter_seed = 0 + _barriers.clear() + _active_barrier_index = -1 + _reel_input_held = false + _reel_speed = 0.0 + _click_power = 1 + _chase_start_delay = 0.0 + _chase_delay_remaining = 0.0 + _chase_speed = 0.0 + _auto_click_accumulator = 0.0 + emit_signal( + "encounter_updated", + 0.0, + 0.0, + PackedFloat32Array(), + PackedInt32Array(), + PackedInt32Array(), + -1, + false + ) + + +func get_barrier_count() -> int: + return _barriers.size() + + +func _update_free_reeling(delta: float) -> void: + if not _reel_input_held: + return + + var intended_progress: float = minf( + progress + _reel_speed * delta, + 1.0 + ) + var next_barrier: Barrier = _get_next_undefeated_barrier() + if next_barrier != null and intended_progress >= next_barrier.position: + progress = next_barrier.position + _active_barrier_index = _barriers.find(next_barrier) + state = CatchState.BLOCKED_BY_BARRIER + _auto_click_accumulator = 0.0 + else: + progress = intended_progress + + +func _update_chase(delta: float) -> void: + var active_delta: float = delta + if _chase_delay_remaining > 0.0: + var protected_time: float = minf(_chase_delay_remaining, active_delta) + _chase_delay_remaining -= protected_time + active_delta -= protected_time + if active_delta <= 0.0: + return + chase_progress = minf( + chase_progress + _chase_speed * active_delta, + 1.0 + ) + + +func _is_chase_failure() -> bool: + if _chase_delay_remaining > 0.0: + return false + return chase_progress + _failure_epsilon >= progress + + +func _update_auto_click(delta: float) -> void: + if not auto_click_enabled or not _reel_input_held: + return + _auto_click_accumulator += delta + var interval: float = maxf(auto_click_interval, 0.05) + while ( + _auto_click_accumulator >= interval + and state == CatchState.BLOCKED_BY_BARRIER + and _reel_input_held + ): + _auto_click_accumulator -= interval + _damage_active_barrier() + + +func _damage_active_barrier() -> void: + if ( + state != CatchState.BLOCKED_BY_BARRIER + or _active_barrier_index < 0 + or _active_barrier_index >= _barriers.size() + ): + return + + var barrier: Barrier = _barriers[_active_barrier_index] + if barrier.defeated: + return + barrier.health = maxi(barrier.health - _click_power, 0) + if barrier.health == 0: + barrier.defeated = true + _active_barrier_index = -1 + _auto_click_accumulator = 0.0 + state = CatchState.REELING + _emit_encounter_update() + + +func _get_next_undefeated_barrier() -> Barrier: + for barrier: Barrier in _barriers: + if not barrier.defeated and barrier.position >= progress: + return barrier + return null + + +func _seed_encounter_rng() -> void: + if use_deterministic_test_seed: + encounter_seed = deterministic_test_seed + _rng.seed = deterministic_test_seed + else: + _rng.randomize() + encounter_seed = _rng.seed + + +func _generate_barriers(profile: CatchDifficultyProfileType) -> void: + var count_range: Vector2i = profile.get_barrier_count_range() + var health_range: Vector2i = profile.get_barrier_health_range() + var interval: Vector2 = profile.get_generation_interval() + var spacing: float = maxf(profile.minimum_barrier_spacing, 0.01) + var available_distance: float = maxf(interval.y - interval.x, 0.0) + var maximum_fitting_count: int = 0 + if available_distance > 0.0: + maximum_fitting_count = int(floor(available_distance / spacing)) + 1 + var requested_count: int = _rng.randi_range(count_range.x, count_range.y) + var barrier_count: int = mini(requested_count, maximum_fitting_count) + if barrier_count <= 0: + return + + var random_offsets := PackedFloat32Array() + for _barrier_index: int in range(barrier_count): + random_offsets.append(_rng.randf()) + random_offsets.sort() + var required_spacing: float = spacing * float(barrier_count - 1) + var random_slack: float = maxf(available_distance - required_spacing, 0.0) + for barrier_index: int in range(barrier_count): + var position: float = ( + interval.x + + spacing * float(barrier_index) + + random_offsets[barrier_index] * random_slack + ) + var health: int = _rng.randi_range(health_range.x, health_range.y) + _barriers.append(Barrier.new(position, health)) + + +func _resolve_catch() -> void: + if state in [CatchState.CAUGHT, CatchState.ESCAPED, CatchState.IDLE]: + return + state = CatchState.CAUGHT + _reel_input_held = false + _auto_click_accumulator = 0.0 + _emit_encounter_update() + caught.emit() + + +func _resolve_escape() -> void: + if state in [CatchState.CAUGHT, CatchState.ESCAPED]: + return + state = CatchState.ESCAPED + _reel_input_held = false + _auto_click_accumulator = 0.0 + _emit_encounter_update() + escaped.emit() + + +func _emit_encounter_update() -> void: + var positions := PackedFloat32Array() + var health := PackedInt32Array() + var maximum_health := PackedInt32Array() + for barrier: Barrier in _barriers: + positions.append(barrier.position) + health.append(barrier.health) + maximum_health.append(barrier.maximum_health) + encounter_updated.emit( + progress, + chase_progress, + positions, + health, + maximum_health, + _active_barrier_index, + state != CatchState.IDLE + ) diff --git a/fishing/catch_controller.gd.uid b/fishing/catch_controller.gd.uid new file mode 100644 index 0000000..ff98438 --- /dev/null +++ b/fishing/catch_controller.gd.uid @@ -0,0 +1 @@ +uid://bp3fvh7q1jw07 diff --git a/fishing/catch_difficulty_profile.gd b/fishing/catch_difficulty_profile.gd new file mode 100644 index 0000000..3ad20ad --- /dev/null +++ b/fishing/catch_difficulty_profile.gd @@ -0,0 +1,29 @@ +class_name CatchDifficultyProfile +extends Resource + +@export_range(0, 12, 1) var barrier_count_min: int = 2 +@export_range(0, 12, 1) var barrier_count_max: int = 4 +@export_range(1, 100, 1) var barrier_health_min: int = 3 +@export_range(1, 100, 1) var barrier_health_max: int = 7 +@export_range(0.0, 0.45, 0.01) var first_barrier_margin: float = 0.15 +@export_range(0.0, 0.45, 0.01) var final_barrier_margin: float = 0.10 +@export_range(0.01, 1.0, 0.01) var minimum_barrier_spacing: float = 0.15 +@export_range(0.0, 5.0, 0.05) var chase_start_delay: float = 1.0 +@export_range(0.01, 0.5, 0.01) var chase_start_offset: float = 0.15 +@export_range(0.0, 2.0, 0.01) var chase_speed: float = 0.14 + + +func get_barrier_count_range() -> Vector2i: + var minimum: int = maxi(0, barrier_count_min) + return Vector2i(minimum, maxi(minimum, barrier_count_max)) + + +func get_barrier_health_range() -> Vector2i: + var minimum: int = maxi(1, barrier_health_min) + return Vector2i(minimum, maxi(minimum, barrier_health_max)) + + +func get_generation_interval() -> Vector2: + var start: float = clampf(first_barrier_margin, 0.0, 0.95) + var finish: float = clampf(1.0 - final_barrier_margin, start, 1.0) + return Vector2(start, finish) diff --git a/fishing/catch_difficulty_profile.gd.uid b/fishing/catch_difficulty_profile.gd.uid new file mode 100644 index 0000000..1d5af63 --- /dev/null +++ b/fishing/catch_difficulty_profile.gd.uid @@ -0,0 +1 @@ +uid://byhefytd4228q diff --git a/fishing/common_catch_profile.tres b/fishing/common_catch_profile.tres new file mode 100644 index 0000000..1169e2e --- /dev/null +++ b/fishing/common_catch_profile.tres @@ -0,0 +1,16 @@ +[gd_resource type="Resource" load_steps=2 format=3] + +[ext_resource type="Script" path="res://fishing/catch_difficulty_profile.gd" id="1_profile"] + +[resource] +script = ExtResource("1_profile") +barrier_count_min = 2 +barrier_count_max = 4 +barrier_health_min = 3 +barrier_health_max = 7 +first_barrier_margin = 0.15 +final_barrier_margin = 0.1 +minimum_barrier_spacing = 0.15 +chase_start_delay = 1.0 +chase_start_offset = 0.15 +chase_speed = 0.14 diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index e040a15..4a46fa8 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -2,11 +2,20 @@ class_name FishingSpot extends Node3D const FishDataType = preload("res://fish/fish_data.gd") +const CatchControllerType = preload("res://fishing/catch_controller.gd") const FishingPresentationType = preload("res://fishing/fishing_presentation.gd") const PlayerType = preload("res://player/player.gd") signal status_changed(status: String) -signal reel_progress_changed(progress: float, visible: bool) +signal catch_display_changed( + progress: float, + chase_progress: float, + barrier_positions: PackedFloat32Array, + barrier_health: PackedInt32Array, + barrier_max_health: PackedInt32Array, + active_barrier_index: int, + visible: bool, +) enum FishingState { READY, @@ -14,7 +23,7 @@ enum FishingState { CASTING, WAITING_FOR_BITE, BITE_ACTIVE, - REELING, + FIGHTING, COOLDOWN, } @@ -39,13 +48,11 @@ enum FishingState { @export_range(0.1, 30.0, 0.1) var wait_time: float = 2.0 @export_range(0.1, 10.0, 0.1) var bite_window_duration: float = 1.5 @export_range(0.1, 10.0, 0.1) var cooldown_duration: float = 1.0 -@export_range(0.01, 5.0, 0.01) var reel_gain_rate: float = 0.67 -@export_range(0.01, 5.0, 0.01) var reel_decay_rate: float = 0.2 -@export_range(0.0, 0.95, 0.05) var hooked_starting_progress: float = 0.25 @export_category("Catch") @export var catchable_fish: FishDataType +@onready var _catch_controller: CatchControllerType = %CatchController @onready var _presentation: FishingPresentationType = %FishingPresentation var state: FishingState = FishingState.READY @@ -62,14 +69,15 @@ var _withdrawal_progress: float = 0.0 var _withdrawal_input_held: bool = false var _new_cast_press_armed: bool = true var _bobber_water_position: Vector3 -var _reel_start_position: Vector3 -var _reel_progress: float = 0.0 -var _reel_input_held: bool = false +var _fight_start_position: Vector3 var _cooldown_status: String = "" var _fishable_query_shape: SphereShape3D = SphereShape3D.new() func _ready() -> void: + _catch_controller.encounter_updated.connect(_on_catch_encounter_updated) + _catch_controller.caught.connect(_on_catch_completed) + _catch_controller.escaped.connect(_on_catch_escaped) _presentation.cast_completed.connect(_on_cast_completed) @@ -92,10 +100,9 @@ func _process(delta: float) -> void: _state_time_remaining -= delta if _state_time_remaining <= 0.0: _miss_bite() - FishingState.REELING: - if _reel_input_held and not Input.is_action_pressed("fish_primary"): - _reel_input_held = false - _update_reel_progress(delta) + FishingState.FIGHTING: + if not Input.is_action_pressed("fish_primary"): + _catch_controller.set_reel_input(false) FishingState.COOLDOWN: _state_time_remaining -= delta if _state_time_remaining <= 0.0: @@ -111,7 +118,7 @@ func _unhandled_input(event: InputEvent) -> void: FishingState.CASTING, FishingState.WAITING_FOR_BITE, FishingState.BITE_ACTIVE, - FishingState.REELING, + FishingState.FIGHTING, ] ): _cancel_attempt() @@ -136,8 +143,8 @@ func _unhandled_input(event: InputEvent) -> void: ) FishingState.BITE_ACTIVE: _confirm_hook() - FishingState.REELING: - _reel_input_held = true + FishingState.FIGHTING: + _catch_controller.handle_primary_pressed() _presentation.set_line_mode( FishingPresentationType.LineMode.TAUT ) @@ -151,8 +158,8 @@ func _unhandled_input(event: InputEvent) -> void: _withdrawal_input_held = false _presentation.set_line_mode(FishingPresentationType.LineMode.SLACK) get_viewport().set_input_as_handled() - elif state == FishingState.REELING: - _reel_input_held = false + elif state == FishingState.FIGHTING: + _catch_controller.set_reel_input(false) get_viewport().set_input_as_handled() @@ -340,58 +347,80 @@ func _activate_bite() -> void: func _confirm_hook() -> void: - if state != FishingState.BITE_ACTIVE or _active_player == null: + if ( + state != FishingState.BITE_ACTIVE + or _active_player == null + or catchable_fish == null + or catchable_fish.catch_profile == null + ): + _cancel_attempt() return - state = FishingState.REELING + state = FishingState.FIGHTING _state_time_remaining = 0.0 - _reel_progress = clampf(hooked_starting_progress, 0.0, 0.95) - _reel_input_held = true - _reel_start_position = _bobber_water_position + _fight_start_position = _bobber_water_position status_changed.emit("Hold left click to reel") - reel_progress_changed.emit(_reel_progress, true) _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT) _presentation.begin_reeling() - _update_reel_presentation() + _catch_controller.start_encounter( + catchable_fish.catch_profile, + _active_player.reel_speed, + _active_player.click_power + ) + _catch_controller.set_reel_input(true) -func _update_reel_progress(delta: float) -> void: - if state != FishingState.REELING: +func _on_catch_encounter_updated( + progress: float, + chase_progress: float, + barrier_positions: PackedFloat32Array, + barrier_health: PackedInt32Array, + barrier_max_health: PackedInt32Array, + active_barrier_index: int, + visible: bool, +) -> void: + catch_display_changed.emit( + progress, + chase_progress, + barrier_positions, + barrier_health, + barrier_max_health, + active_barrier_index, + visible + ) + if state != FishingState.FIGHTING or not visible: return - if _reel_input_held: - _reel_progress = minf(_reel_progress + reel_gain_rate * delta, 1.0) - else: - _reel_progress = maxf(_reel_progress - reel_decay_rate * delta, 0.0) - reel_progress_changed.emit(_reel_progress, true) - _update_reel_presentation() - if is_equal_approx(_reel_progress, 1.0): - _resolve_catch() - elif is_zero_approx(_reel_progress): - _resolve_escape() - - -func _update_reel_presentation() -> void: - var desired_position: Vector3 = _reel_start_position.lerp( + var desired_position: Vector3 = _fight_start_position.lerp( _withdrawal_endpoint, - _reel_progress + progress ) - _bobber_water_position = _get_clamped_reel_position(desired_position) - _presentation.show_reel_position( - _bobber_water_position, - _reel_input_held - ) - - -func _get_clamped_reel_position(desired_position: Vector3) -> Vector3: - return find_last_fishable_position( - _reel_start_position, + _bobber_water_position = find_last_fishable_position( + _fight_start_position, desired_position ) + _presentation.show_reel_position( + _bobber_water_position, + Input.is_action_pressed("fish_primary") + ) + if ( + active_barrier_index >= 0 + and active_barrier_index < barrier_health.size() + and active_barrier_index < barrier_max_health.size() + ): + status_changed.emit( + "Barrier: %d / %d" + % [ + barrier_health[active_barrier_index], + barrier_max_health[active_barrier_index], + ] + ) + else: + status_changed.emit("Hold left click to reel") -func _resolve_catch() -> void: - if state != FishingState.REELING or _active_player == null or catchable_fish == null: +func _on_catch_completed() -> void: + if state != FishingState.FIGHTING or _active_player == null or catchable_fish == null: _cancel_attempt() return @@ -399,8 +428,8 @@ func _resolve_catch() -> void: _cleanup_attempt("Caught %s!" % catchable_fish.display_name, &"catch") -func _resolve_escape() -> void: - if state != FishingState.REELING: +func _on_catch_escaped() -> void: + if state != FishingState.FIGHTING: return var fish_name: String = "The fish" @@ -432,10 +461,8 @@ func _cleanup_attempt( _withdrawal_progress = 0.0 _withdrawal_input_held = false _bobber_water_position = Vector3.ZERO - _reel_start_position = Vector3.ZERO - _reel_input_held = false - _reel_progress = 0.0 - reel_progress_changed.emit(0.0, false) + _fight_start_position = Vector3.ZERO + _catch_controller.reset() if visual_outcome.is_empty(): _presentation.cleanup() else: diff --git a/fishing/fishing_spot.tscn b/fishing/fishing_spot.tscn index fbed769..a2870cd 100644 --- a/fishing/fishing_spot.tscn +++ b/fishing/fishing_spot.tscn @@ -1,8 +1,9 @@ -[gd_scene load_steps=11 format=3] +[gd_scene load_steps=12 format=3] [ext_resource type="Script" path="res://fishing/fishing_spot.gd" id="1_spot"] [ext_resource type="Resource" path="res://fish/bluegill.tres" id="2_bluegill"] [ext_resource type="Script" path="res://fishing/fishing_presentation.gd" id="3_presentation"] +[ext_resource type="Script" path="res://fishing/catch_controller.gd" id="4_catch"] [sub_resource type="SphereMesh" id="BobberMesh"] radius = 0.12 @@ -42,6 +43,10 @@ size = Vector3(0.72, 0.06, 0.12) script = ExtResource("1_spot") catchable_fish = ExtResource("2_bluegill") +[node name="CatchController" type="Node" parent="."] +unique_name_in_owner = true +script = ExtResource("4_catch") + [node name="FishingPresentation" type="Node3D" parent="."] unique_name_in_owner = true script = ExtResource("3_presentation") diff --git a/player/player.gd b/player/player.gd index f407b21..d7a837a 100644 --- a/player/player.gd +++ b/player/player.gd @@ -14,6 +14,10 @@ const FishInventoryType = preload("res://inventory/fish_inventory.gd") @export_category("Control") @export var local_control_enabled: bool = true +@export_category("Fishing Stats") +@export_range(0.01, 2.0, 0.01) var reel_speed: float = 0.32 +@export_range(1, 100, 1) var click_power: int = 1 + @export_category("Camera") @export var mouse_sensitivity: float = 0.005 @export var controller_camera_speed: float = 2.5 diff --git a/ui/game_ui.gd b/ui/game_ui.gd index fde6f27..e430751 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -6,13 +6,18 @@ const FishingSpotType = preload("res://fishing/fishing_spot.gd") @onready var _status_label: Label = %StatusLabel @onready var _bluegill_count_label: Label = %BluegillCountLabel -@onready var _reel_progress: ProgressBar = %ReelProgress +@onready var _catch_track: Control = %CatchTrack +@onready var _green_catch_progress: ProgressBar = %GreenCatchProgress +@onready var _red_chase_progress: ProgressBar = %RedChaseProgress +@onready var _barrier_markers: Control = %BarrierMarkers +@onready var _barrier_summary: Label = %BarrierSummary +@onready var _barrier_health: Label = %BarrierHealth func setup(inventory: FishInventoryType, fishing_spot: FishingSpotType) -> void: inventory.contents_changed.connect(_on_inventory_contents_changed) fishing_spot.status_changed.connect(_on_fishing_status_changed) - fishing_spot.reel_progress_changed.connect(_on_reel_progress_changed) + fishing_spot.catch_display_changed.connect(_on_catch_display_changed) _update_bluegill_count(inventory.get_count(&"bluegill")) @@ -26,9 +31,105 @@ func _on_fishing_status_changed(status: String) -> void: _status_label.visible = not status.is_empty() -func _on_reel_progress_changed(progress: float, visible: bool) -> void: - _reel_progress.value = progress * 100.0 - _reel_progress.visible = visible +func _on_catch_display_changed( + progress: float, + chase_progress: float, + barrier_positions: PackedFloat32Array, + barrier_health: PackedInt32Array, + barrier_max_health: PackedInt32Array, + active_barrier_index: int, + visible: bool, +) -> void: + _green_catch_progress.value = progress * 100.0 + _red_chase_progress.value = maxf(chase_progress, 0.0) * 100.0 + _catch_track.visible = visible + _barrier_summary.visible = visible + if not visible: + _barrier_health.visible = false + _clear_barrier_markers() + _barrier_summary.text = "" + _barrier_health.text = "" + return + + _update_barrier_markers( + barrier_positions, + barrier_health, + active_barrier_index + ) + var barrier_labels: PackedStringArray = [] + for barrier_index: int in range(barrier_positions.size()): + var defeated: bool = ( + barrier_index < barrier_health.size() + and barrier_health[barrier_index] <= 0 + ) + var marker: String = "✓" if defeated else "●" + if barrier_index == active_barrier_index: + marker = "[%s]" % marker + barrier_labels.append( + "%d%% %s" + % [roundi(barrier_positions[barrier_index] * 100.0), marker] + ) + _barrier_summary.text = "Barriers: %s" % " ".join(barrier_labels) + + if ( + active_barrier_index >= 0 + and active_barrier_index < barrier_health.size() + and active_barrier_index < barrier_max_health.size() + ): + _barrier_health.text = ( + "Barrier: %d / %d" + % [ + barrier_health[active_barrier_index], + barrier_max_health[active_barrier_index], + ] + ) + else: + _barrier_health.text = "" + + var chase_gap: float = progress - chase_progress + if chase_gap <= 0.05: + _barrier_health.text = ( + "%s%s" + % [ + _barrier_health.text, + "\nRed is closing in!", + ] + ).strip_edges() + _barrier_health.visible = ( + visible + and (active_barrier_index >= 0 or chase_gap <= 0.05) + ) + + +func _update_barrier_markers( + positions: PackedFloat32Array, + health: PackedInt32Array, + active_index: int, +) -> void: + _clear_barrier_markers() + for barrier_index: int in range(positions.size()): + var marker := ColorRect.new() + var defeated: bool = ( + barrier_index < health.size() + and health[barrier_index] <= 0 + ) + marker.color = Color(0.35, 0.38, 0.42, 0.8) if defeated else Color(1.0, 0.82, 0.2, 1.0) + if barrier_index == active_index: + marker.color = Color(1.0, 1.0, 1.0, 1.0) + marker.set_anchors_preset(Control.PRESET_TOP_LEFT) + marker.anchor_left = positions[barrier_index] + marker.anchor_right = positions[barrier_index] + marker.offset_left = -2.0 + marker.offset_right = 2.0 + marker.offset_top = 0.0 + marker.offset_bottom = 18.0 + marker.mouse_filter = Control.MOUSE_FILTER_IGNORE + _barrier_markers.add_child(marker) + + +func _clear_barrier_markers() -> void: + for marker: Node in _barrier_markers.get_children(): + marker.queue_free() func _update_bluegill_count(count: int) -> void: diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index 53ced13..6d4cc0d 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -1,7 +1,19 @@ -[gd_scene load_steps=2 format=3] +[gd_scene load_steps=6 format=3] [ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"] +[sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"] +bg_color = Color(0.08, 0.08, 0.1, 0.9) + +[sub_resource type="StyleBoxFlat" id="StyleBox_chase_fill"] +bg_color = Color(0.85, 0.12, 0.12, 0.95) + +[sub_resource type="StyleBoxFlat" id="StyleBox_catch_fill"] +bg_color = Color(0.16, 0.82, 0.28, 0.9) + +[sub_resource type="StyleBoxFlat" id="StyleBox_transparent"] +bg_color = Color(0, 0, 0, 0) + [node name="GameUI" type="CanvasLayer"] script = ExtResource("1_ui") @@ -49,9 +61,55 @@ visible = false text = "" horizontal_alignment = 1 -[node name="ReelProgress" type="ProgressBar" parent="FishingPanel/MarginContainer/VBoxContainer"] +[node name="CatchTrack" type="Control" parent="FishingPanel/MarginContainer/VBoxContainer"] unique_name_in_owner = true visible = false custom_minimum_size = Vector2(0, 18) + +[node name="GreenCatchProgress" type="ProgressBar" parent="FishingPanel/MarginContainer/VBoxContainer/CatchTrack"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +theme_override_styles/background = SubResource("StyleBox_chase_background") +theme_override_styles/fill = SubResource("StyleBox_catch_fill") value = 0.0 show_percentage = false + +[node name="RedChaseProgress" type="ProgressBar" parent="FishingPanel/MarginContainer/VBoxContainer/CatchTrack"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +theme_override_styles/background = SubResource("StyleBox_transparent") +theme_override_styles/fill = SubResource("StyleBox_chase_fill") +value = 0.0 +show_percentage = false + +[node name="BarrierMarkers" type="Control" parent="FishingPanel/MarginContainer/VBoxContainer/CatchTrack"] +unique_name_in_owner = true +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="BarrierSummary" type="Label" parent="FishingPanel/MarginContainer/VBoxContainer"] +unique_name_in_owner = true +visible = false +horizontal_alignment = 1 + +[node name="BarrierHealth" type="Label" parent="FishingPanel/MarginContainer/VBoxContainer"] +unique_name_in_owner = true +visible = false +horizontal_alignment = 1