Lobby barrel-smash reward + roll/slam barrel destruction
Bull's boulder roll now homes on and ploughs through lobby barrels, and the slam shockwave bursts them within its radius. HUD tallies barrels on load and fires a confetti "WOooOoW!" + two gold bonus HP pips when the last one breaks; gates open from run state as fights are cleared. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,13 @@ var _ability_bar: HBoxContainer
|
||||
var _ability_panels: Array[PanelContainer] = []
|
||||
var _bull_bg: TextureRect
|
||||
|
||||
# Lobby barrel-smash reward — tally the barrels present on load; when the last one bursts,
|
||||
# throw confetti + a "WOooOoW!" and grant the bull two yellow bonus pips. Latched so it
|
||||
# fires once per lobby visit.
|
||||
var _barrels_remaining: int = 0
|
||||
var _barrel_reward_given: bool = false
|
||||
const _BARREL_BONUS_HP: int = 2
|
||||
|
||||
# Red damage vignette — a full-screen shader ColorRect flashed on every hit.
|
||||
const _VIGNETTE_SHADER := "res://damage_vignette.gdshader"
|
||||
var _vignette: ColorRect
|
||||
@@ -90,6 +97,7 @@ func _ready() -> void:
|
||||
_build_touch_controls()
|
||||
_update_control_hints()
|
||||
_find_spawner.call_deferred()
|
||||
_watch_barrels.call_deferred()
|
||||
|
||||
|
||||
# On-screen joystick + ability buttons for touch devices. Self-gating: the overlay
|
||||
@@ -213,6 +221,104 @@ func _find_spawner() -> void:
|
||||
_spawner.all_defeated.connect(_on_all_defeated)
|
||||
|
||||
|
||||
# ── Lobby barrel-smash reward ───────────────────────────────────────────────────
|
||||
|
||||
# Tally the barrels the current level loaded with and listen for each one bursting. Only
|
||||
# the lobby has barrels, so on every other level this simply finds none and stands down.
|
||||
func _watch_barrels() -> void:
|
||||
var barrels := get_tree().get_nodes_in_group(&"barrel")
|
||||
_barrels_remaining = barrels.size()
|
||||
if _barrels_remaining == 0:
|
||||
return
|
||||
for barrel: Node in barrels:
|
||||
barrel.destroyed.connect(_on_barrel_destroyed)
|
||||
|
||||
|
||||
func _on_barrel_destroyed() -> void:
|
||||
_barrels_remaining -= 1
|
||||
if _barrels_remaining > 0 or _barrel_reward_given:
|
||||
return
|
||||
_barrel_reward_given = true
|
||||
# Bank the reward run-wide (capped at +2). Only heal the bull for the pips that were
|
||||
# actually added — if it already carried the cap over from an earlier lobby, it keeps
|
||||
# the cheer but no new health.
|
||||
var granted := Run.add_bonus_hp(_BARREL_BONUS_HP)
|
||||
if granted > 0 and _player != null and _player.has_method(&"grant_bonus_hp"):
|
||||
_player.call(&"grant_bonus_hp", granted)
|
||||
_celebrate()
|
||||
|
||||
|
||||
# The "cleared the lobby" flourish: a burst of confetti and a big springy "WOooOoW!".
|
||||
func _celebrate() -> void:
|
||||
_spawn_confetti()
|
||||
_show_wow()
|
||||
|
||||
|
||||
func _show_wow() -> void:
|
||||
var wow := Label.new()
|
||||
wow.text = "WOooOoW!"
|
||||
wow.set_anchors_preset(Control.PRESET_CENTER)
|
||||
wow.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||
wow.grow_vertical = Control.GROW_DIRECTION_BOTH
|
||||
wow.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
wow.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
wow.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
wow.add_theme_font_override("font", UiFonts.title())
|
||||
wow.add_theme_font_size_override("font_size", 96)
|
||||
wow.add_theme_color_override("font_color", _HP_BONUS)
|
||||
wow.add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 0.9))
|
||||
wow.add_theme_constant_override("outline_size", 8)
|
||||
wow.pivot_offset = Vector2.ZERO
|
||||
wow.scale = Vector2(0.2, 0.2)
|
||||
add_child(wow)
|
||||
# Keep the pop growing from the label's centre once it has a real size.
|
||||
await get_tree().process_frame
|
||||
wow.pivot_offset = wow.size * 0.5
|
||||
|
||||
var tw := create_tween()
|
||||
tw.tween_property(wow, "scale", Vector2.ONE, 0.35).set_trans(Tween.TRANS_BACK).set_ease(
|
||||
Tween.EASE_OUT)
|
||||
tw.tween_interval(1.1)
|
||||
tw.parallel().tween_property(wow, "modulate:a", 0.0, 0.6)
|
||||
tw.parallel().tween_property(wow, "position:y", wow.position.y - 40.0, 0.6)
|
||||
tw.tween_callback(wow.queue_free)
|
||||
|
||||
|
||||
func _spawn_confetti() -> void:
|
||||
const _CONFETTI_COLORS: Array[Color] = [
|
||||
Color(0.98, 0.24, 0.28), # red
|
||||
Color(1.00, 0.78, 0.20), # gold
|
||||
Color(0.30, 0.80, 0.40), # green
|
||||
Color(0.30, 0.62, 1.00), # blue
|
||||
Color(0.85, 0.40, 0.95), # violet
|
||||
Color(1.00, 1.00, 1.00), # white
|
||||
]
|
||||
var vp := get_viewport().get_visible_rect().size
|
||||
# One emitter per colour, raining down across the top of the screen.
|
||||
for col: Color in _CONFETTI_COLORS:
|
||||
var p := CPUParticles2D.new()
|
||||
p.position = Vector2(vp.x * 0.5, -20.0)
|
||||
p.emission_shape = CPUParticles2D.EMISSION_SHAPE_RECTANGLE
|
||||
p.emission_rect_extents = Vector2(vp.x * 0.5, 6.0)
|
||||
p.amount = 40
|
||||
p.lifetime = 2.6
|
||||
p.one_shot = true
|
||||
p.explosiveness = 0.35
|
||||
p.direction = Vector2(0.0, 1.0)
|
||||
p.spread = 25.0
|
||||
p.gravity = Vector2(0.0, 420.0)
|
||||
p.initial_velocity_min = 120.0
|
||||
p.initial_velocity_max = 260.0
|
||||
p.angular_velocity_min = -360.0
|
||||
p.angular_velocity_max = 360.0
|
||||
p.scale_amount_min = 4.0
|
||||
p.scale_amount_max = 8.0
|
||||
p.color = col
|
||||
add_child(p)
|
||||
# Self-clean once the burst has fully fallen.
|
||||
get_tree().create_timer(p.lifetime + 0.5).timeout.connect(p.queue_free)
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and not event.echo:
|
||||
if _game_over and event.physical_keycode in [KEY_ENTER, KEY_KP_ENTER]:
|
||||
@@ -238,14 +344,12 @@ func _restart() -> void:
|
||||
get_tree().reload_current_scene()
|
||||
|
||||
|
||||
# A cleared level advances the run and drops the player into the lobby hub: commit the
|
||||
# win, flag the lobby as the active level, then reload the shell so it builds the lobby.
|
||||
# (Choosing the next map node happens from the lobby via trigger areas, added later.)
|
||||
# A cleared level drops the player back into the lobby hub: bank the win (which opens the
|
||||
# gates it unlocks) and reload the shell so it rebuilds the lobby.
|
||||
func _continue_to_lobby() -> void:
|
||||
get_tree().paused = false
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
Run.complete_current_level()
|
||||
Run.go_to_lobby()
|
||||
Run.return_to_lobby()
|
||||
get_tree().change_scene_to_file(Run.active_level().scene_path)
|
||||
|
||||
|
||||
@@ -422,6 +526,9 @@ func _build_game_over() -> void:
|
||||
const _HP_FULL: Color = Color(0.82, 0.16, 0.12)
|
||||
const _HP_EMPTY: Color = Color(0.35, 0.05, 0.05)
|
||||
const _HP_SOCKET: Color = Color(0.05, 0.02, 0.02, 0.92)
|
||||
# Bonus pips (barrel-smash reward) read as bright gold-yellow so they stand apart from the
|
||||
# red life bar.
|
||||
const _HP_BONUS: Color = Color(1.00, 0.84, 0.18)
|
||||
|
||||
const _HP_PIP_W: float = 22.0
|
||||
const _HP_PIP_H: float = 14.0
|
||||
@@ -500,10 +607,15 @@ func _on_health_changed(current: int, max_hp: int) -> void:
|
||||
var cur := maxi(current, 0)
|
||||
var frac := 0.0 if max_hp <= 0 else clampf(float(cur) / float(max_hp), 0.0, 1.0)
|
||||
var lit := _HP_FULL.lerp(_HP_EMPTY, 1.0 - frac)
|
||||
# Bonus pips sit at the high end of the bar; paint those top pips yellow when lit.
|
||||
var bonus := 0
|
||||
if _player != null and _player.has_method(&"get_bonus_hp"):
|
||||
bonus = _player.call(&"get_bonus_hp")
|
||||
var base_max := max_hp - bonus
|
||||
for i: int in _hp_seg_fills.size():
|
||||
var fill := _hp_seg_fills[i]
|
||||
fill.visible = i < cur
|
||||
fill.color = lit
|
||||
fill.color = _HP_BONUS if i >= base_max else lit
|
||||
|
||||
|
||||
const _SLOT_SEPARATION: int = 10
|
||||
|
||||
Reference in New Issue
Block a user