extends Node3D ## Fish you can see in the lake, so it's clear there's something to catch. ## Purely visual (the catch comes from bobber.gd). ## ## - A few swim slow loops just under the surface over water at least 0.8 m ## deep, including the shelf near the shore where you'll be standing. ## - Every few seconds one jumps: a leap out of the water and a spreading ## splash ring. That's the cue that tells you where to cast. const SCALE := 1.6 ## drawn bigger than the item so they read at a distance var terrain: Node3D var _fish: Array = [] ## [MeshInstance3D, centre, radius, speed, phase, depth] var _rng := RandomNumberGenerator.new() var _jump_left := 3.0 var _jumping := -1 ## index of the fish mid-leap, or -1 var _jump_t := 0.0 var _rings: Array = [] ## [MeshInstance3D, age] static var _ring_mesh: Mesh func setup(t: Node3D, count: int = 14) -> void: terrain = t _rng.seed = t.noise_seed + 501 var lake: Vector2 = t.lake_center var tries := 0 while _fish.size() < count and tries < 600: tries += 1 var a := _rng.randf() * TAU # Bias toward the outer, shallower ring so some swim near the shore. var r: float = sqrt(_rng.randf()) * t.lake_radius * 0.7 var c := Vector2(lake.x + cos(a) * r, lake.y + sin(a) * r) var loop := _rng.randf_range(1.2, 3.0) var ok := true for k in range(8): var q := c + Vector2(cos(k * TAU / 8.0), sin(k * TAU / 8.0)) * loop if t.sample_height(q.x, q.y) > t.water_level - 0.8: ok = false break if not ok: continue var mi := MeshInstance3D.new() mi.mesh = ItemDB.mesh_for("trout" if _rng.randf() < 0.3 else "perch") mi.material_override = ItemDB.item_material() mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF mi.scale = Vector3.ONE * SCALE add_child(mi) _fish.append([mi, c, loop, _rng.randf_range(0.25, 0.5) * (1.0 if _rng.randf() < 0.5 else -1.0), _rng.randf() * TAU, _rng.randf_range(0.2, 0.35)]) ## Where fish `i` is in its loop at time `t`. func _swim_pos(i: int, t: float) -> Vector3: var f: Array = _fish[i] var a: float = t * f[3] + f[4] var p: Vector2 = (f[1] as Vector2) + Vector2(cos(a), sin(a)) * float(f[2]) return Vector3(p.x, terrain.water_level - f[5] + sin(t * 1.3 + f[4]) * 0.04, p.y) func _process(delta: float) -> void: var t := Time.get_ticks_msec() / 1000.0 for i in range(_fish.size()): if i == _jumping: continue var f: Array = _fish[i] var mi: MeshInstance3D = f[0] mi.position = _swim_pos(i, t) var a: float = t * f[3] + f[4] # Face along the loop (fish mesh's head is +X), with a little wiggle. var tangent := Vector2(-sin(a), cos(a)) * signf(f[3]) mi.rotation = Vector3(0, atan2(-tangent.y, tangent.x) + sin(t * 6.0 + f[4]) * 0.15, 0) _update_jump(delta, t) _update_rings(delta) func _update_jump(dt: float, t: float) -> void: if _fish.is_empty(): return if _jumping < 0: _jump_left -= dt if _jump_left <= 0.0: _jumping = _rng.randi() % _fish.size() _jump_t = 0.0 _ring(_swim_pos(_jumping, t)) return # A 0.8 s arc, half a metre up, nose up then nose down. _jump_t += dt var k := _jump_t / 0.8 var mi: MeshInstance3D = _fish[_jumping][0] var base := _swim_pos(_jumping, t) mi.position = base + Vector3(0, sin(clampf(k, 0.0, 1.0) * PI) * 0.55 + 0.25, 0) mi.rotation.z = lerpf(1.0, -1.0, clampf(k, 0.0, 1.0)) if k >= 1.0: _ring(base) mi.rotation.z = 0.0 _jumping = -1 _jump_left = _rng.randf_range(3.0, 8.0) ## A splash ring that spreads and fades on the surface. func _ring(at: Vector3) -> void: if _ring_mesh == null: var tm := TorusMesh.new() tm.inner_radius = 0.42 tm.outer_radius = 0.5 tm.rings = 24 tm.ring_segments = 4 var m := StandardMaterial3D.new() m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA m.albedo_color = Color(0.9, 0.95, 1.0, 0.8) tm.material = m _ring_mesh = tm var mi := MeshInstance3D.new() mi.mesh = _ring_mesh mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF mi.position = Vector3(at.x, terrain.water_level + 0.02, at.z) mi.scale = Vector3(0.4, 0.05, 0.4) add_child(mi) _rings.append([mi, 0.0]) func _update_rings(dt: float) -> void: for r in _rings.duplicate(): r[1] += dt var mi: MeshInstance3D = r[0] var s: float = 0.4 + r[1] * 1.6 mi.scale = Vector3(s, 0.05, s) mi.transparency = clampf(r[1] / 1.6, 0.0, 1.0) if r[1] >= 1.6: mi.queue_free() _rings.erase(r)