27 Commits

Author SHA1 Message Date
richard a4d18e4928 Move bull ability FX into the authorable systems/fx/ library
CI / Decide whether to run (push) Has been cancelled
CI / Lint & headless tests (push) Has been cancelled
The bull's kick/dash/slam/roll and movement particles were ~112 lines of
hand-built CPUParticles3D in player.gd, bypassing the Fx system entirely
(Fx.spawn had zero callers) — so Siim couldn't author them in the inspector.

- Add six inspector-editable effect scenes (bull_slam/kick/dash one-shots +
  bull_hoof_dust/charge_trail/roll_sparks continuous); wire player.gd to
  Fx.spawn() / new Fx.attach(); register them + the gallery + smoke test.
- Extract the coded, non-particle slam rings + kick flash into a new
  bull_impact_fx.gd component (bull_legs.gd pattern).
- Drop 12 now-obsolete DP dust look-params (the .tscn owns the look now).
- Trim god-file comment noise: rewrite an AI-placeholder barrel block to typed
  code, drop dead commented-out bounce/topple code, fix stale post-reorg file
  paths (Assets/… → art/…, PascalCase scene names → snake_case).
- Document the Siim workflow in systems/fx/README.md; update CLAUDE.md.

player.gd 1307 → 944 lines. Full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CCyaJPTHCH9EDthpf4A5Sj
2026-09-08 14:35:25 +03:00
richard c742c4ae0e Remove the dead Slay-the-Spire map system (superseded by the lobby)
The lobby hub replaced the between-fights map, leaving the whole map
subsystem unused. Remove it:

- Delete MapScreen.tscn + map_screen.gd (the dead screen) and map_node.gd
  (the MapNode class).
- Strip map generation + navigation from run_state.gd (~90 lines:
  _generate_map/_link_rows/reachable/select_node/node_at/current_node/
  run_complete/complete_current_level, plus map/player_row/col/pending_row/col).
  ensure_run() now guards on a _run_active flag instead of the map array, so
  the shell reloading for each fight still can't wipe the run in progress.
- Simplify active_level() to the lobby flow (override -> lobby -> gate -> arena).
- Drop the now-unread LevelDef.color / is_boss fields and the _make_level args.
- Remove the map test from logic_test.gd.

Live flow unchanged (lobby gates via choose_level, wins via return_to_lobby).
Full headless suite green; menu + shell boot into the lobby clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBbGXbzg3SD6ZBSadPPF5H
2026-09-07 01:23:12 +03:00
richard 5caccd318c Drop version suffixes from Blender source filenames
CI / Decide whether to run (push) Has been cancelled
CI / Lint & headless tests (push) Has been cancelled
Rename the Blender working files to plain role names, dropping the version
noise: bull_v11 -> bull, matador_v03 -> matador, publik_mees_v01 ->
publik_mees, and collapse anim_bull2 into anim_bull (identical rig, newer
save kept; older duplicate dropped). Repoint create_matador_anims.py (blend
input + fbx export name), render_anim_preview.py and CLAUDE.md at the new
names.

The anim_* and sword files link the model blends as Blender libraries
(//bull_v11.blend, //matador_v03.blend, //publik_mees_v01.blend), so those
links need repointing once in Blender. The game is unaffected — it ships the
exported fbx/glb, not the .blend sources (art/blender/ is Godot-ignored).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBbGXbzg3SD6ZBSadPPF5H
2026-09-07 01:05:09 +03:00
richard 78efca445a clean up yt link
CI / Decide whether to run (push) Has been cancelled
CI / Lint & headless tests (push) Has been cancelled
2026-09-07 00:56:05 +03:00
richard 5078502228 Move Blender sources under art/blender and snake_case them
Relocate the Blender working files into art/blender/ (Godot-ignored via
the .gdignore that travels with them) and lowercase the ANIM_* files to
snake_case: anim_bull, anim_bull2, anim_matador, anim_publik_mees. Drop
the stray matador2.gd.uid (no matador2.gd exists).

Repoint .gitignore (preview + blend-backup patterns), render_anim_preview.py
and CLAUDE.md at art/blender/, and update the anim-doc references from the
deleted matador_v02 to matador_v03.

No game impact: art/blender/ sits outside Godot's import graph (confirmed:
no .blend imported, preview stays gitignored, menu boots clean).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBbGXbzg3SD6ZBSadPPF5H
2026-09-07 00:54:34 +03:00
richard 7bc241bbe6 Consolidate raw art under art/ and prune unused assets
Group all raw art by role under art/ (characters, levels, weapons, fx,
anim, textures, ui, icons, fonts, hdri, materials), matching the earlier
code reorg. Files are snake_case; asset uids are preserved so uid:// refs
still resolve, and every res:// path reference is rewritten across scenes,
scripts and project.godot.

- Move the used assets out of Assets/ Animations/ fonts/ HUD/ HDRI/
  Materials/ into art/; remove those dirs.
- Materials snake_cased: barrel_metal, barrel_wood, bear_claw_fx, fire.
- Delete unreferenced assets: alternate models, empty stub scripts,
  superseded BEAR_*.tres, marketing/spare PNGs, a spare font weight and
  the orphan M_Barrel_wood.gdshader.
- Blender: drop .blend auto-backups and matador_v01/v02 (+ stale v02 fbx);
  keep matador_v03 and repoint create_matador_anims.py to it. .gitignore
  now blocks all Blender/*.blend[0-9] backups.
- GLB Extract-Textures siblings (art/levels/*.png) kept and tracked.
- Document the art/ layout in CLAUDE.md.

Headless suite green; menu and gameplay shell boot clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FBbGXbzg3SD6ZBSadPPF5H
2026-09-07 00:46:34 +03:00
richard 534b7f45ca Untrack the stray Assets/bear_level.tscn swept in during the reorg
CI / Decide whether to run (push) Has been cancelled
CI / Lint & headless tests (push) Has been cancelled
An unreferenced duplicate of levels/bear_level.tscn (the canonical bear level) that
git add -A pulled in by accident. Return it to untracked — it's kept on disk so
Richard can decide whether it's an accidental editor save-out to delete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KoYBYQgRyY1zDHXMsXY3V
2026-09-06 01:32:04 +03:00
richard 524a222b6c Point tests/README.md at the Gitea CI path
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KoYBYQgRyY1zDHXMsXY3V
2026-09-06 01:31:40 +03:00
richard 9ca6f282f5 Drop the stale GitHub-workflow reference from the Gitea CI header
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KoYBYQgRyY1zDHXMsXY3V
2026-09-06 01:31:29 +03:00
richard 64a8a9863d Remove the GitHub Actions workflow; Gitea is the CI host
The team hosts on Gitea, so .github/workflows/ci.yml is dead weight. The Gitea
workflow (.gitea/workflows/ci.yml) is the single source of CI: lint + headless
tests on push/PR, plus the daily safety-net run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KoYBYQgRyY1zDHXMsXY3V
2026-09-06 01:31:09 +03:00
richard c5ef68e82a Add a daily safety-net run to Gitea CI that skips already-tested commits
Add a schedule trigger (04:00 UTC daily) + workflow_dispatch to the Gitea CI, plus a
guard job: on push/PR it passes straight through, but on the daily run it asks the
Gitea API whether main's tip already carries a successful 'ci/headless' commit status
and, if so, skips the test job. The test job stamps that status when green, so a
commit already verified at push time is never re-tested — the nightly only spends a
runner on a tip whose push-time run was missed, skipped, or failed.

Also fix a cold-cache bug the nightly would have hit anyway: CI always starts without
a .godot cache, and a single 'godot --import' pass leaves .fbx/.glb-derived scenes
unresolved (the first pass imports base assets, the second resolves dependents), so
the headless tests couldn't load them. Import now runs twice.

Needs Gitea >= 1.21 (schedule) / >= 1.23 (the manual-run button); the auto-injected
GITEA_TOKEN needs statuses:write for the marker, granted via workflow permissions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KoYBYQgRyY1zDHXMsXY3V
2026-09-06 01:22:10 +03:00
richard 4569d3f5d4 Update CLAUDE.md paths + folder-layout overview for the reorg
Repoint the project-structure table and lint command at the new role-based tree,
and add a folder-layout summary up top.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KoYBYQgRyY1zDHXMsXY3V
2026-09-06 00:06:33 +03:00
richard 7b6a141767 Reorganize the codebase into role-based folders (snake_case)
Move the 40-odd root-level scripts and scenes into a tree that groups by role,
per the approved roadmap (WS2/WS3): actors/{player,matador,bear}, systems/{combat,
fx,input,debug,render,crowd,config}, ui/, props/ (was tscn_s/). Rename the shell
scene.tscn -> levels/game_shell.tscn and every PascalCase scene to snake_case
(Player.tscn -> player.tscn, MainMenu.tscn -> main_menu.tscn, etc.) so the
case-insensitive web/macOS filesystems can't alias two files together.

Done as a scripted move: each file travels with its .uid; every quoted res:// path
across scenes, scripts, .tres, and project.godot's 9 autoloads is rewritten; the
.godot uid/class cache is rebuilt from scratch. Scenes still resolve by uid, so the
rewrite is belt-and-suspenders. Lint globs (run_tests.sh + both CI workflows) now
walk the new dirs; an orphan animation_tree.gd.uid and 7 pre-existing trailing-
whitespace lints (barrel/torch, never covered by the old root-only glob) are cleaned.

Full suite green on both sides. Asset consolidation (Assets/Animations/fonts ->
art/) is deferred — moving imported .glb/.png/.ttf is safer done in the editor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KoYBYQgRyY1zDHXMsXY3V
2026-09-06 00:04:41 +03:00
richard b839252fe0 Track the UID sidecars for the new DamageEvent and test-harness scripts
These .gd files were committed earlier without their generated .uid siblings; the
repo tracks .uid everywhere else, so commit them too and stop each machine
regenerating (and churning) the resource UIDs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq1VX2o1ZfiNRdCjD294wf
2026-09-05 10:52:23 +03:00
richard ac0da2a6fc Add Gitea Actions CI mirroring the GitHub workflow
Gitea Actions is GitHub-Actions-compatible and reads .gitea/workflows/, so the
lint + headless-test gate runs on the team's own Gitea host. Unlike the GitHub
workflow it installs Godot (curl) and gdtoolkit (venv) with plain shell steps, so
it doesn't depend on the chickensoft setup-godot action being reachable from a
self-hosted runner — only actions/checkout and upload-artifact remain external,
both covered by Gitea's default action proxy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq1VX2o1ZfiNRdCjD294wf
2026-09-05 10:52:23 +03:00
richard c3f92d8758 Move the FX placeholder seeder into tools/
_seed_placeholders.gd is a one-off generator, not a shipped effect — it belongs
with the other headless tooling (bake_crowd_sheet, preview_*), not in the fx/
runtime dir Siim edits. Rename it to tools/seed_fx_placeholders.gd (matching the
dir's verb_noun convention) and repoint the two doc-comment references.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq1VX2o1ZfiNRdCjD294wf
2026-09-05 10:52:23 +03:00
richard 9c4563a973 Complete the DamageEvent migration: one deal() entry point for every attacker
The bull's Hitbox already routed through DamageEvent, but the spear, the bear's
maul, and the matador's reach-gore still did raw Object.call(&"take_sword_hit", ...)
— the exact scattered string paths DamageEvent was meant to retire. Add a single
static DamageEvent.deal(target, cause, at, source, knock_up) that builds the typed
event and hands it to receive_damage(), falling back to the legacy string call for
any not-yet-migrated victim, and route all four attackers (Hitbox included) through
it. The damage_pipeline characterization test proves the bull's outcomes are
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq1VX2o1ZfiNRdCjD294wf
2026-09-05 10:52:10 +03:00
richard ffe3052468 Fix stale logic-test reference to renamed param
mat_spawn_count was renamed mat_wave_size; the test still queried the old key
(returning 0.0). Point it at the current param + its registered range.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmG3mQGVXZUPDVapmdbWZ1
2026-09-05 00:19:19 +03:00
richard f02185e859 Fix pre-existing lint errors blocking the new CI gate
A too-long line in console.gd and trailing whitespace in matador_spawn.gd
predate this branch but fail gdlint, which the new CI runs on every push.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmG3mQGVXZUPDVapmdbWZ1
2026-09-05 00:18:21 +03:00
richard 70274f1773 Route the bull's hits through the typed DamageEvent pipeline
Hitbox now builds a DamageEvent (cause + hit position + shaped knock-up) and
calls the bull's new receive_damage(), instead of two stringly Object.call()s.
receive_damage adapts over the existing pip/knock-up logic so behaviour is
identical -- the characterization test drives both the string and typed paths
and asserts the same outcomes. Wire fx/damage/harness tests into run_tests.sh
+ CLAUDE.md. All hitbox guard tests green (stab, bear hitbox, ram, charge).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmG3mQGVXZUPDVapmdbWZ1
2026-09-05 00:17:04 +03:00
richard 973db7d8b8 Add scene-authored FX effect library and Fx autoload
Replace code-buried CPUParticles with inspector-editable effect scenes fired
through one Fx autoload: code names the effect (Fx.spawn(&"blood_burst", xform)),
the .tscn owns the look. Ships four placeholder effects (blood_burst, slam_dust,
spark, hit_flash) seeded from the old code params, a self-freeing one_shot_effect
root helper, a live preview gallery, and an FX style guide for Siim. CPUParticles
only (mobile GLES3 constraint). Existing call sites untouched -- migrated later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-05 00:13:43 +03:00
richard 1ea7f7a744 Add characterization test locking the bull's damage reception
Pins take_sword_hit / apply_knock_up behaviour (pip loss, i-frames, single
tagged death, upward-only knock-up) before WS2 reroutes them through
DamageEvent. 12/12 headless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmG3mQGVXZUPDVapmdbWZ1
2026-09-05 00:13:23 +03:00
richard 64fabb688f Add typed DamageEvent contract for the unified hit pipeline
The single payload every attack will route through, replacing the stringly
Object.call(&"take_sword_hit", "gored") paths. Standalone for now; the
per-actor slices wire hitbox.gd and the bull/matador/bear receivers onto it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmG3mQGVXZUPDVapmdbWZ1
2026-09-05 00:09:35 +03:00
richard 9122a601d0 Wire harness self-test into run_tests.sh
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmG3mQGVXZUPDVapmdbWZ1
2026-09-05 00:07:46 +03:00
richard a6f938a0da Add CI workflow and reusable test-harness foundation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-05 00:07:03 +03:00
richard 994ce3be73 Add headless UI-layout robustness test
Asserts computed Control geometry instead of pixels: HUD/menu stay on-screen,
text fits its box, tap targets >=44px, and the options scroll can reach its
overflow -- across five resolutions plus a +40% pseudo-locale pass. Wired into
run_tests.sh and the CLAUDE.md test list. 16/16 pass headless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmG3mQGVXZUPDVapmdbWZ1
2026-09-05 00:01:06 +03:00
richard 39aacc3c87 Remove editor autosave cruft and orphan scenes
Delete 17 tracked *.tmp editor autosaves and three verified-unreferenced
orphans (Assets/bear_level.tscn, Assets/matador_2.tscn, matador_2.gd), and
gitignore *.tmp so they never return. The real Assets/bear_level_arena.glb is
kept — it still backs levels/bear_level.tscn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmG3mQGVXZUPDVapmdbWZ1
2026-09-05 00:01:06 +03:00
16 changed files with 7 additions and 142 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -23,7 +23,7 @@ script = ExtResource("2_camera_iso")
[node name="bull" parent="." unique_id=1386495166 instance=ExtResource("4_e80uo")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.655, 0.23)
[node name="Skeleton3D" parent="bull/Armature" parent_id_path=PackedInt32Array(1386495166, 1057197468) index="0" unique_id=840295370]
[node name="Skeleton3D" parent="bull/Armature" parent_id_path=PackedInt32Array(1386495166, 1942410469) index="0" unique_id=550138996]
bones/0/position = Vector3(0, 0, 0.009963839)
bones/0/rotation = Quaternion(0.7071068, 0, 0, 0.7071067)
bones/0/scale = Vector3(1, 1, 1)
-14
View File
@@ -1,14 +0,0 @@
extends Node3D
var player: Node3D = null
func _process(delta: float) -> void:
# If we don't have the player yet, try to find them in the group
if not is_instance_valid(player):
var players = get_tree().get_nodes_in_group("player")
if players.size() > 0:
player = players[0] as Node3D
# If the player exists, follow their position
if is_instance_valid(player):
global_position = player.global_position
-1
View File
@@ -1 +0,0 @@
uid://cmapsaj010q40
-14
View File
@@ -1,14 +0,0 @@
[gd_scene format=3 uid="uid://4v25hb0qu5l0"]
[ext_resource type="Script" uid="uid://cmapsaj010q40" path="res://actors/player/player_follow_light.gd" id="1_w7wrt"]
[node name="PlayerFollowLight" type="Node3D" unique_id=1889971494]
script = ExtResource("1_w7wrt")
[node name="SpotLight3D" type="SpotLight3D" parent="." unique_id=1523840927]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0, 2.6136062, 1.0431258e-07)
light_color = Color(0.9999998, 0.8702623, 0.60548127, 1)
light_energy = 8.0
shadow_enabled = true
distance_fade_length = 30.45
spot_angle = 72.62192
+3 -7
View File
@@ -2,7 +2,6 @@
[ext_resource type="PackedScene" uid="uid://bbfvwolhomilp" path="res://props/torch.tscn" id="1_w7uyf"]
[ext_resource type="PackedScene" uid="uid://bd56iuttyiypv" path="res://props/barrel_simple.tscn" id="2_3o0md"]
[ext_resource type="PackedScene" uid="uid://4v25hb0qu5l0" path="res://actors/player/player_follow_light.tscn" id="2_8bh2m"]
[ext_resource type="PackedScene" uid="uid://dctmlh4hnuux1" path="res://art/levels/lobby.glb" id="2_w7uyf"]
[ext_resource type="Script" uid="uid://cwbf7jfkr8j10" path="res://levels/lobby_underground.gd" id="4_73452"]
[ext_resource type="Script" uid="uid://dn77mjyxllgl6" path="res://levels/to_the_bear_level.gd" id="5_6j2sa"]
@@ -46,8 +45,8 @@ size = Vector3(4.1274414, 8.299622, 6.9958496)
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Lights" unique_id=955995990]
transform = Transform3D(-0.502238, -0.82287306, 0.26577574, 0.36594617, 0.07621761, 0.9275096, -0.78347975, 0.5630903, 0.26284787, 0, 0, 0)
light_color = Color(0.5266291, 0.5549962, 0.9999985, 1)
light_energy = 0.466
light_color = Color(0.78431374, 0.8117647, 1, 1)
light_energy = 0.228
shadow_enabled = true
[node name="SpotLight3D" type="SpotLight3D" parent="Lights" unique_id=1783040386]
@@ -100,9 +99,6 @@ transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, -0.3
[node name="Torch8" parent="Lights/Torches" unique_id=987701581 instance=ExtResource("1_w7uyf")]
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, -0.349617, 1.9073486e-06, -0.31505227)
[node name="PlayerFollowLight" parent="Lights" unique_id=1889971494 instance=ExtResource("2_8bh2m")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.1712027, 0)
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=939963056]
environment = SubResource("Environment_lobby")
@@ -244,7 +240,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.1928787, 0.11255789, 2.8063
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 0, 0, 0)
script = ExtResource("4_73452")
[node name="Plane" parent="Lobby_underground" index="1" unique_id=2038841618]
[node name="Plane" parent="Lobby_underground" index="1" unique_id=1962555538]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.2352347, 0)
[node name="AnimationPlayer" type="AnimationPlayer" parent="Lobby_underground" unique_id=2070502042]
-47
View File
@@ -33,14 +33,6 @@ var cleared: Dictionary = {}
## loads — the player reads it on spawn — and is capped so it can't stack past BONUS_HP_CAP.
var bonus_hp: int = 0
const BONUS_HP_CAP: int = 2
## Wall-clock run timer: starts the moment the player walks through the arena gate (the
## run's first fight) and freezes the instant the bear falls, so the completion screen can
## show a final time. Time.get_ticks_msec()-based — real time, not delta-summed — so it
## keeps ticking correctly across the scene reload between the arena and the bear's den.
var _timer_start_msec: int = -1
var _timer_end_msec: int = -1
var _timer_running: bool = false
## True once start_new_run() has built a run. Lets ensure_run() avoid wiping an active run
## when the shell reloads for each fight.
var _run_active: bool = false
@@ -113,9 +105,6 @@ func start_new_run() -> void:
lobby_choice = null
cleared = {}
bonus_hp = 0
_timer_start_msec = -1
_timer_end_msec = -1
_timer_running = false
_run_active = true
run_started.emit()
@@ -165,39 +154,3 @@ func add_bonus_hp(amount: int) -> int:
var before := bonus_hp
bonus_hp = clampi(bonus_hp + amount, 0, BONUS_HP_CAP)
return bonus_hp - before
## Begin the run clock. No-op while the clock is already running, so walking back through
## the arena gate mid-run can't restart it — but once it's been stopped (the bear is dead
## and the player has looped back to the arena to go again), the next call restarts it from
## zero rather than staying stuck showing the previous run's final time.
func start_run_timer() -> void:
if _timer_running:
return
_timer_start_msec = Time.get_ticks_msec()
_timer_end_msec = -1
_timer_running = true
## Freeze the run clock — called the instant the bear falls, so the recorded time is the
## kill moment itself, not whatever the win-screen's celebratory delay adds on top.
func stop_run_timer() -> void:
if not _timer_running:
return
_timer_end_msec = Time.get_ticks_msec()
_timer_running = false
## Seconds elapsed since the timer started (live while running, held steady once
## stop_run_timer() has been called). 0 before the timer has ever started.
func run_elapsed() -> float:
if _timer_start_msec < 0:
return 0.0
var end_msec := Time.get_ticks_msec() if _timer_running else _timer_end_msec
return float(end_msec - _timer_start_msec) / 1000.0
## Whether the run clock has been started (the player has entered the arena at least once
## this run). Lets the HUD hide the timer readout until there's a time worth showing.
func run_timer_started() -> bool:
return _timer_start_msec >= 0
-1
View File
@@ -8,6 +8,5 @@ func _ready() -> void:
func _on_body_entered(body: Node3D) -> void:
if not body.is_in_group("player"):
return
Run.start_run_timer()
Run.choose_level(&"arena")
get_tree().change_scene_to_file(Run.active_level().scene_path)
+1 -1
View File
@@ -11,7 +11,7 @@ config_version=5
[application]
config/name="Bullosseum"
config/version="0.5.0"
config/version="0.4.0"
run/main_scene="uid://bmnmenu0intr1"
config/features=PackedStringArray("4.7", "GL Compatibility")
boot_splash/bg_color=Color(0, 0, 0, 1)
-1
View File
@@ -167,7 +167,6 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -9.011785, -2.371254, 16.2322
[node name="OmniLight3D" type="OmniLight3D" parent="MeshInstance3D/TorchLight" unique_id=1094582672]
transform = Transform3D(0.9999986, -0.001596809, 0.00045388562, 0.0014380338, 0.96984345, 0.24372463, -0.00082937966, -0.24372365, 0.9698444, 9.096397, 6.2542057, -15.77264)
light_color = Color(0.92773837, 0.6304423, 0, 1)
light_energy = 2.0
shadow_enabled = true
omni_range = 13.657384
+2 -54
View File
@@ -66,10 +66,6 @@ const _BULL_FACE_PAD: float = 1.15 # breathing room around the cluster within
# records which way it went (read by the gameplay test).
const _WIN_VIDEO := "res://videos/bull_win.ogv" # the bull triumphs
const _LOSS_VIDEO := "res://videos/matador_win.ogv" # the matador triumphs
# Clearing the bear — the run's final boss — replaces the usual bull-win clip with the
# closing "Thank You For Playing" screen. That clip already bakes its own title text in,
# so _over_label is left blank for this outcome (see _show_game_over).
const _THANKYOU_VIDEO := "res://videos/thankyou.ogv"
# Beat between the run ending and the outcome screen. A loss is a short freeze on the
# dead bull; a win gives the player a longer stretch to strut a victory lap around the
@@ -85,12 +81,6 @@ var _over_label: Label
var _over_continue: Button
var _over_again: Button
# 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
# has actually started (the player hasn't entered the arena yet) and while the game-over
# screen is up.
var _run_timer_label: Label
func _ready() -> void:
layer = 11
@@ -102,7 +92,6 @@ func _ready() -> void:
_build_ability_bar()
_build_health()
_build_version_label()
_build_run_timer()
_build_damage_vignette()
_build_game_over()
_build_touch_controls()
@@ -217,41 +206,8 @@ func _build_version_label() -> void:
add_child(lbl)
# Run clock, top-left. Desktop only — touch already owns that corner for the mobile HP
# readout, and a run timer is a nice-to-have there, not worth the crowding.
func _build_run_timer() -> void:
if Controls.use_touch_ui():
return
_run_timer_label = Label.new()
_run_timer_label.text = _format_time(0.0)
_run_timer_label.visible = false
_run_timer_label.add_theme_font_override("font", UiFonts.body())
_run_timer_label.add_theme_color_override("font_color", _CREAM)
_run_timer_label.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85))
_run_timer_label.add_theme_constant_override("outline_size", 4)
_run_timer_label.add_theme_font_size_override("font_size", 20)
_run_timer_label.set_anchors_preset(Control.PRESET_TOP_LEFT)
_run_timer_label.offset_left = 20.0
_run_timer_label.offset_top = 16.0
_run_timer_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_run_timer_label)
# mm:ss.hh — matches how a speedrun-style clock reads at a glance.
static func _format_time(seconds: float) -> String:
var total_cs := int(round(seconds * 100.0))
var minutes := total_cs / 6000
var secs := (total_cs / 100) % 60
var centis := total_cs % 100
return "%02d:%02d.%02d" % [minutes, secs, centis]
func _process(_delta: float) -> void:
_update_control_hints()
if _run_timer_label != null:
_run_timer_label.visible = Run.run_timer_started() and not _game_over
if _run_timer_label.visible:
_run_timer_label.text = _format_time(Run.run_elapsed())
if _player == null:
var players := get_tree().get_nodes_in_group(&"player")
if not players.is_empty():
@@ -478,10 +434,6 @@ func _on_player_died(cause: String = "") -> void:
func _on_all_defeated() -> void:
# Freeze the run clock the instant the bear falls — before _show_game_over's
# celebratory delay — so the recorded time is the kill moment, not the victory lap.
if Run.active_level().id == &"bear":
Run.stop_run_timer()
_show_game_over(true)
@@ -498,16 +450,12 @@ 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
var 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
# 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 \
else ("Bull Won!" if _result_win else "Matadors Won!")
_over_label.text = "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).
_over_continue.visible = _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 := _WIN_VIDEO if _result_win else _LOSS_VIDEO
if ResourceLoader.exists(path):
_over_video.stream = load(path)
_over_video.play()
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
uid://b5rupuoe1lk5j