Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ea4171863 | |||
| cd54242de0 |
@@ -835,9 +835,8 @@ func take_sword_hit(cause: String = "", hit_pos: Vector3 = Vector3.ZERO) -> void
|
|||||||
health_changed.emit(_hp, _max_hp)
|
health_changed.emit(_hp, _max_hp)
|
||||||
hit_taken.emit(cause)
|
hit_taken.emit(cause)
|
||||||
camera_pivot.call(&"trigger_hit", 0.7)
|
camera_pivot.call(&"trigger_hit", 0.7)
|
||||||
# Vibrate controller 0 with half weak strength, full strong strength, for 0.5 seconds
|
# Vibrate controller 0 with half weak strength, full strong strength, for 0.2 seconds
|
||||||
if DP.b("gamepad_vibration"):
|
Controls.vibrate(0, 0.5, 1, 0.2)
|
||||||
Input.start_joy_vibration(0, 0.5, 1, 0.2)
|
|
||||||
|
|
||||||
# A spear gores a spot — blood spurts from the entry wound (where it struck the
|
# A spear gores a spot — blood spurts from the entry wound (where it struck the
|
||||||
# bull). Melee gores pass no position and skip the spurt; gore can be turned off.
|
# bull). Melee gores pass no position and skip the spurt; gore can be turned off.
|
||||||
|
|||||||
@@ -11,5 +11,4 @@ func _process(_delta: float) -> void:
|
|||||||
if Run.bear_gate_open() or DP.b("gate_unlocked_bear"):
|
if Run.bear_gate_open() or DP.b("gate_unlocked_bear"):
|
||||||
bear_gate_open = true
|
bear_gate_open = true
|
||||||
$AnimationPlayer.play("Gate_opening")
|
$AnimationPlayer.play("Gate_opening")
|
||||||
if DP.b("gamepad_vibration"):
|
Controls.vibrate(0, 0.5, .5, 7)
|
||||||
Input.start_joy_vibration(0, 0.5, .5, 7)
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const _COMMANDS := {
|
|||||||
"list": "print params (optional section)",
|
"list": "print params (optional section)",
|
||||||
"level": "select level and reload (level <id> — arena / arena_boss / bear)",
|
"level": "select level and reload (level <id> — arena / arena_boss / bear)",
|
||||||
"restart": "reload the current level scene",
|
"restart": "reload the current level scene",
|
||||||
|
"video": "preview an outcome screen (video win / loss / finish)",
|
||||||
"clear": "clear the log",
|
"clear": "clear the log",
|
||||||
"help": "show this help",
|
"help": "show this help",
|
||||||
}
|
}
|
||||||
@@ -206,12 +207,6 @@ func _input(event: InputEvent) -> void:
|
|||||||
# while the console is open — cheap, and it never lets the command line go dark.
|
# while the console is open — cheap, and it never lets the command line go dark.
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
if _is_open and not _input_field.has_focus():
|
if _is_open and not _input_field.has_focus():
|
||||||
# Diagnostic: name whatever currently holds focus before we snatch it back, so
|
|
||||||
# a persistent thief (a HUD button, a menu control) shows up in the log instead
|
|
||||||
# of us guessing. Remove once the culprit is identified.
|
|
||||||
var owner: Control = get_viewport().gui_get_focus_owner()
|
|
||||||
var who: String = String(owner.name) if owner != null else "<nothing>"
|
|
||||||
_log_warn("focus stolen by %s — reclaiming" % who)
|
|
||||||
_input_field.grab_focus()
|
_input_field.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
@@ -476,6 +471,8 @@ func _run_command(parts: PackedStringArray) -> bool:
|
|||||||
_cmd_list(parts)
|
_cmd_list(parts)
|
||||||
"level":
|
"level":
|
||||||
_cmd_level(parts)
|
_cmd_level(parts)
|
||||||
|
"video":
|
||||||
|
_cmd_video(parts)
|
||||||
"restart":
|
"restart":
|
||||||
_log_ok("Restarting…")
|
_log_ok("Restarting…")
|
||||||
get_tree().paused = false
|
get_tree().paused = false
|
||||||
@@ -608,6 +605,27 @@ func _set_param(key: String, meta: Dictionary, raw: String) -> void:
|
|||||||
_log_warn("Cannot set param of type %d" % meta["type"])
|
_log_warn("Cannot set param of type %d" % meta["type"])
|
||||||
|
|
||||||
|
|
||||||
|
# Jumps straight to an outcome screen (win / loss / the bear's thankyou.ogv finish clip)
|
||||||
|
# via hud.gd's debug-only entry point — a quick way to preview a clip's framing/timing
|
||||||
|
# without actually clearing (or dying to) a fight.
|
||||||
|
const _VIDEO_KINDS := ["win", "loss", "finish"]
|
||||||
|
|
||||||
|
|
||||||
|
func _cmd_video(parts: PackedStringArray) -> void:
|
||||||
|
if parts.size() < 2 or not _VIDEO_KINDS.has(parts[1].to_lower()):
|
||||||
|
_log_warn("Usage: video <%s>" % "|".join(_VIDEO_KINDS))
|
||||||
|
return
|
||||||
|
var hud: Node = get_tree().get_first_node_in_group(&"hud")
|
||||||
|
if hud == null:
|
||||||
|
_log_warn("No HUD in the current scene.")
|
||||||
|
return
|
||||||
|
var kind := parts[1].to_lower()
|
||||||
|
get_tree().paused = false
|
||||||
|
_toggle() # close the console so it doesn't sit on top of the outcome screen
|
||||||
|
hud.debug_force_outcome(kind)
|
||||||
|
_log_ok("Playing '%s' outcome…" % kind)
|
||||||
|
|
||||||
|
|
||||||
func _cmd_level(parts: PackedStringArray) -> void:
|
func _cmd_level(parts: PackedStringArray) -> void:
|
||||||
if parts.size() == 1:
|
if parts.size() == 1:
|
||||||
var cur := Run.active_level()
|
var cur := Run.active_level()
|
||||||
|
|||||||
@@ -222,6 +222,35 @@ func rigid_skin_enabled() -> bool:
|
|||||||
return DP.b("use_rigid_skin")
|
return DP.b("use_rigid_skin")
|
||||||
|
|
||||||
|
|
||||||
|
## Rumble a connected gamepad, gated on the DP "gamepad_vibration" flag. Off web this is a thin
|
||||||
|
## wrapper over Input.start_joy_vibration(); on web that call is a documented engine no-op
|
||||||
|
## (godotengine/godot#96985 — never wired to the browser side), so there we drive the Gamepad
|
||||||
|
## Haptic Actuator API ourselves. Safe no-op wherever it's unsupported (Safari, most Firefox
|
||||||
|
## versions, or a controller with no rumble motors) — desktop Chromium/Edge is the target.
|
||||||
|
func vibrate(device: int, weak_magnitude: float, strong_magnitude: float, duration: float) -> void:
|
||||||
|
if not DP.b("gamepad_vibration"):
|
||||||
|
return
|
||||||
|
if not OS.has_feature("web"):
|
||||||
|
Input.start_joy_vibration(device, weak_magnitude, strong_magnitude, duration)
|
||||||
|
return
|
||||||
|
JavaScriptBridge.eval("""
|
||||||
|
(function(){
|
||||||
|
try {
|
||||||
|
var pads = navigator.getGamepads ? navigator.getGamepads() : [];
|
||||||
|
var pad = pads[%d];
|
||||||
|
var act = pad && pad.vibrationActuator;
|
||||||
|
if (act && act.playEffect) {
|
||||||
|
act.playEffect('dual-rumble', {
|
||||||
|
duration: %d,
|
||||||
|
weakMagnitude: %f,
|
||||||
|
strongMagnitude: %f
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
})();
|
||||||
|
""" % [device, int(duration * 1000.0), weak_magnitude, strong_magnitude], true)
|
||||||
|
|
||||||
|
|
||||||
## Returns the first keyboard event bound to an action, or null.
|
## Returns the first keyboard event bound to an action, or null.
|
||||||
func get_key_event(action: StringName) -> InputEventKey:
|
func get_key_event(action: StringName) -> InputEventKey:
|
||||||
for event: InputEvent in InputMap.action_get_events(action):
|
for event: InputEvent in InputMap.action_get_events(action):
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ extends CanvasLayer
|
|||||||
const _GOLD := Color(1.00, 0.78, 0.22)
|
const _GOLD := Color(1.00, 0.78, 0.22)
|
||||||
const _CREAM := Color(0.93, 0.88, 0.72)
|
const _CREAM := Color(0.93, 0.88, 0.72)
|
||||||
const _MUTED := Color(0.66, 0.60, 0.46)
|
const _MUTED := Color(0.66, 0.60, 0.46)
|
||||||
|
# Sampled from thankyou.ogv's baked "THANK YOU FOR PLAYING!" title — a flat dark ink on the
|
||||||
|
# clip's sunburst poster art, no outline/glow. The run-time label mimics it so it reads as
|
||||||
|
# part of that same poster instead of a HUD readout stamped on top.
|
||||||
|
const _INK := Color(0.12, 0.08, 0.02)
|
||||||
|
|
||||||
# Per-ability trim colour, tying each tile (and its key cap) to that ability's FX.
|
# Per-ability trim colour, tying each tile (and its key cap) to that ability's FX.
|
||||||
# Order matches _ABILITY_ICONS: kick, dash, slam, roll.
|
# Order matches _ABILITY_ICONS: kick, dash, slam, roll.
|
||||||
@@ -79,11 +83,15 @@ const _WIN_DELAY := 3.0
|
|||||||
|
|
||||||
var _game_over: bool = false
|
var _game_over: bool = false
|
||||||
var _result_win: bool = false
|
var _result_win: bool = false
|
||||||
|
# Set per-outcome in _show_game_over: the bear is the run's last fight, so its win screen
|
||||||
|
# offers a fresh run instead of a lobby gate to continue through.
|
||||||
|
var _is_final_boss: bool = false
|
||||||
var _over_root: Control
|
var _over_root: Control
|
||||||
var _over_video: VideoStreamPlayer
|
var _over_video: VideoStreamPlayer
|
||||||
var _over_label: Label
|
var _over_label: Label
|
||||||
var _over_continue: Button
|
var _over_continue: Button
|
||||||
var _over_again: Button
|
var _over_again: Button
|
||||||
|
var _over_label_tween: Tween # the finish screen's breathing "Your Time" pulse
|
||||||
|
|
||||||
# Run clock readout — top-left, desktop only (touch's top-left corner is already the
|
# Run clock readout — top-left, desktop only (touch's top-left corner is already the
|
||||||
# mobile HP readout from mobile_hud.gd). Tracks Run.run_elapsed(); hidden until the timer
|
# mobile HP readout from mobile_hud.gd). Tracks Run.run_elapsed(); hidden until the timer
|
||||||
@@ -95,6 +103,7 @@ var _run_timer_label: Label
|
|||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
layer = 11
|
layer = 11
|
||||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
add_to_group(&"hud") # lets the debug console's `video` command find this instance
|
||||||
_build_controls_panel()
|
_build_controls_panel()
|
||||||
_build_tab_hint()
|
_build_tab_hint()
|
||||||
_build_bottom_hud()
|
_build_bottom_hud()
|
||||||
@@ -387,7 +396,7 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||||||
if event is InputEventKey and event.pressed and not event.echo:
|
if event is InputEventKey and event.pressed and not event.echo:
|
||||||
if _game_over and event.physical_keycode in [KEY_ENTER, KEY_KP_ENTER]:
|
if _game_over and event.physical_keycode in [KEY_ENTER, KEY_KP_ENTER]:
|
||||||
if _result_win:
|
if _result_win:
|
||||||
_continue_to_lobby()
|
_on_win_button_pressed()
|
||||||
else:
|
else:
|
||||||
_restart()
|
_restart()
|
||||||
elif event.physical_keycode == KEY_TAB and not _game_over:
|
elif event.physical_keycode == KEY_TAB and not _game_over:
|
||||||
@@ -417,6 +426,24 @@ func _continue_to_lobby() -> void:
|
|||||||
get_tree().change_scene_to_file(Run.active_level().scene_path)
|
get_tree().change_scene_to_file(Run.active_level().scene_path)
|
||||||
|
|
||||||
|
|
||||||
|
# The bear is the run's last fight — there's no further lobby gate to continue through, so
|
||||||
|
# the win screen offers a fresh run instead. Mirrors main_menu.gd's Play button.
|
||||||
|
func _play_again() -> void:
|
||||||
|
get_tree().paused = false
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||||
|
Run.start_new_run()
|
||||||
|
get_tree().change_scene_to_file("res://levels/game_shell.tscn")
|
||||||
|
|
||||||
|
|
||||||
|
# Shared by the win-screen button and its Enter-key shortcut, routing to whichever the
|
||||||
|
# current outcome needs (set in _show_game_over).
|
||||||
|
func _on_win_button_pressed() -> void:
|
||||||
|
if _is_final_boss:
|
||||||
|
_play_again()
|
||||||
|
else:
|
||||||
|
_continue_to_lobby()
|
||||||
|
|
||||||
|
|
||||||
func _toggle_controls() -> void:
|
func _toggle_controls() -> void:
|
||||||
_controls_visible = not _controls_visible
|
_controls_visible = not _controls_visible
|
||||||
_toggle_btn.text = "Hide controls" if _controls_visible else "Show controls"
|
_toggle_btn.text = "Hide controls" if _controls_visible else "Show controls"
|
||||||
@@ -498,16 +525,73 @@ func _show_game_over(win: bool, _cause: String = "") -> void:
|
|||||||
|
|
||||||
await get_tree().create_timer(_WIN_DELAY if win else _LOSS_DELAY, false, false, true).timeout
|
await get_tree().create_timer(_WIN_DELAY if win else _LOSS_DELAY, false, false, true).timeout
|
||||||
|
|
||||||
var is_final_boss := _result_win and Run.active_level().id == &"bear"
|
_is_final_boss = _result_win and Run.active_level().id == &"bear"
|
||||||
# The thank-you clip already bakes in its own title, so this label carries the
|
_present_outcome()
|
||||||
# winning time instead — the one thing that clip can't show.
|
|
||||||
_over_label.text = ("Your Time: %s" % _format_time(Run.run_elapsed())) if is_final_boss \
|
|
||||||
|
## Debug-only entry point for the console's `video` command — skips the fight and the
|
||||||
|
## outcome delay, jumping straight to an outcome screen so a clip can be previewed
|
||||||
|
## (`video finish` for thankyou.ogv) without clearing the bear for real. kind: one of
|
||||||
|
## "win" / "loss" / "finish".
|
||||||
|
func debug_force_outcome(kind: String) -> void:
|
||||||
|
if _controls_panel:
|
||||||
|
_controls_panel.visible = false
|
||||||
|
if _tab_hint:
|
||||||
|
_tab_hint.visible = false
|
||||||
|
_game_over = true
|
||||||
|
match kind:
|
||||||
|
"finish":
|
||||||
|
_result_win = true
|
||||||
|
_is_final_boss = true
|
||||||
|
"win":
|
||||||
|
_result_win = true
|
||||||
|
_is_final_boss = false
|
||||||
|
_:
|
||||||
|
_result_win = false
|
||||||
|
_is_final_boss = false
|
||||||
|
_present_outcome()
|
||||||
|
|
||||||
|
|
||||||
|
# Populates and reveals the outcome screen from _result_win / _is_final_boss, set by
|
||||||
|
# either _show_game_over (a real fight ending) or debug_force_outcome (console preview).
|
||||||
|
func _present_outcome() -> void:
|
||||||
|
# The thank-you clip already bakes in its own title, so this label carries the winning
|
||||||
|
# time instead — the one thing that clip can't show. Anchored as a fraction of the
|
||||||
|
# screen (not fixed pixels) at ~83% down — just under the clip's own baked title (which
|
||||||
|
# sits at ~72-81% in the source video) and clear of the bull it frames, instead of the
|
||||||
|
# shared top-of-screen spot "Bull Won!"/"Matadors Won!" use. Styled to match the baked
|
||||||
|
# title's flat dark-ink poster look rather than the gold HUD outline style.
|
||||||
|
_over_label.text = ("Your Time: %s" % _format_time(Run.run_elapsed())) if _is_final_boss \
|
||||||
else ("Bull Won!" if _result_win else "Matadors Won!")
|
else ("Bull Won!" if _result_win else "Matadors Won!")
|
||||||
# A win advances the run (on to the map); a loss ends it (retry from the top).
|
if _is_final_boss:
|
||||||
|
_over_label.anchor_top = 0.83
|
||||||
|
_over_label.anchor_bottom = 0.83
|
||||||
|
_over_label.grow_vertical = Control.GROW_DIRECTION_END
|
||||||
|
_over_label.offset_top = 0.0
|
||||||
|
_over_label.offset_bottom = 0.0
|
||||||
|
_over_label.add_theme_font_size_override("font_size", 40)
|
||||||
|
_over_label.add_theme_color_override("font_color", _INK)
|
||||||
|
_over_label.add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 0.0))
|
||||||
|
_over_label.add_theme_constant_override("outline_size", 0)
|
||||||
|
_start_over_label_pulse()
|
||||||
|
else:
|
||||||
|
_over_label.anchor_top = 0.0
|
||||||
|
_over_label.anchor_bottom = 0.0
|
||||||
|
_over_label.grow_vertical = Control.GROW_DIRECTION_END
|
||||||
|
_over_label.offset_top = 80.0
|
||||||
|
_over_label.offset_bottom = 0.0
|
||||||
|
_over_label.add_theme_font_size_override("font_size", 64)
|
||||||
|
_over_label.add_theme_color_override("font_color", _GOLD)
|
||||||
|
_over_label.add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 0.9))
|
||||||
|
_over_label.add_theme_constant_override("outline_size", 6)
|
||||||
|
_stop_over_label_pulse()
|
||||||
|
# A win advances the run (on to the map, or — for the bear, the run's last fight — into
|
||||||
|
# a fresh run); a loss ends it (retry from the top).
|
||||||
|
_over_continue.text = "Play Again (Enter)" if _is_final_boss else "Continue (Enter)"
|
||||||
_over_continue.visible = _result_win
|
_over_continue.visible = _result_win
|
||||||
_over_again.visible = not _result_win
|
_over_again.visible = not _result_win
|
||||||
|
|
||||||
var path := _THANKYOU_VIDEO if is_final_boss else (_WIN_VIDEO if _result_win else _LOSS_VIDEO)
|
var path := _THANKYOU_VIDEO if _is_final_boss else (_WIN_VIDEO if _result_win else _LOSS_VIDEO)
|
||||||
if ResourceLoader.exists(path):
|
if ResourceLoader.exists(path):
|
||||||
_over_video.stream = load(path)
|
_over_video.stream = load(path)
|
||||||
_over_video.play()
|
_over_video.play()
|
||||||
@@ -520,6 +604,29 @@ func _show_game_over(win: bool, _cause: String = "") -> void:
|
|||||||
create_tween().tween_property(_over_root, "modulate:a", 1.0, 0.5)
|
create_tween().tween_property(_over_root, "modulate:a", 1.0, 0.5)
|
||||||
|
|
||||||
|
|
||||||
|
# thankyou.ogv's baked title isn't static — it breathes, scaling gently in and out on a loop
|
||||||
|
# (measured off the source clip: roughly a 1.4 s in/out cycle, matching the clip's own loop
|
||||||
|
# length). The "Your Time" label mimics that same pulse so it reads as part of the clip
|
||||||
|
# rather than a HUD readout stamped over it. Scales around the label's own centre (its size
|
||||||
|
# only settles a frame after the anchor/text changes above, hence the deferred pivot read).
|
||||||
|
func _start_over_label_pulse() -> void:
|
||||||
|
_stop_over_label_pulse()
|
||||||
|
_over_label.scale = Vector2.ONE
|
||||||
|
await get_tree().process_frame
|
||||||
|
_over_label.pivot_offset = _over_label.size * 0.5
|
||||||
|
_over_label_tween = create_tween().set_loops()
|
||||||
|
_over_label_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
|
||||||
|
_over_label_tween.tween_property(_over_label, "scale", Vector2(1.08, 1.08), 0.7)
|
||||||
|
_over_label_tween.tween_property(_over_label, "scale", Vector2(0.92, 0.92), 0.7)
|
||||||
|
|
||||||
|
|
||||||
|
func _stop_over_label_pulse() -> void:
|
||||||
|
if _over_label_tween:
|
||||||
|
_over_label_tween.kill()
|
||||||
|
_over_label_tween = null
|
||||||
|
_over_label.scale = Vector2.ONE
|
||||||
|
|
||||||
|
|
||||||
func _build_game_over() -> void:
|
func _build_game_over() -> void:
|
||||||
_over_root = Control.new()
|
_over_root = Control.new()
|
||||||
_over_root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
_over_root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
@@ -576,10 +683,11 @@ func _build_game_over() -> void:
|
|||||||
row.offset_bottom = -48.0
|
row.offset_bottom = -48.0
|
||||||
_over_root.add_child(row)
|
_over_root.add_child(row)
|
||||||
|
|
||||||
# Win → continue to the run map; loss → restart from the top. Shown per-result in
|
# Win → continue to the run map, or — beating the bear, the run's last fight — start a
|
||||||
# _show_game_over; both share the Enter shortcut and label.
|
# fresh run; loss → restart from the top. Text is set per-outcome in _show_game_over;
|
||||||
|
# both routes share the Enter shortcut.
|
||||||
_over_continue = _make_button("Continue (Enter)")
|
_over_continue = _make_button("Continue (Enter)")
|
||||||
_over_continue.pressed.connect(_continue_to_lobby)
|
_over_continue.pressed.connect(_on_win_button_pressed)
|
||||||
row.add_child(_over_continue)
|
row.add_child(_over_continue)
|
||||||
|
|
||||||
_over_again = _make_button("Continue (Enter)")
|
_over_again = _make_button("Continue (Enter)")
|
||||||
|
|||||||
Reference in New Issue
Block a user