Add barrier-based catch and chase mechanic
This commit is contained in:
parent
ad848f9431
commit
ae2f724fe2
12 changed files with 631 additions and 68 deletions
314
fishing/catch_controller.gd
Normal file
314
fishing/catch_controller.gd
Normal file
|
|
@ -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
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue