diff --git a/Assets/bear_level_arena.glb b/Assets/bear_level_arena.glb new file mode 100644 index 0000000..dfc75e0 Binary files /dev/null and b/Assets/bear_level_arena.glb differ diff --git a/Assets/bear_level_arena.glb.import b/Assets/bear_level_arena.glb.import new file mode 100644 index 0000000..f45aa16 --- /dev/null +++ b/Assets/bear_level_arena.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://bm6rmt3xprhjs" +path="res://.godot/imported/bear_level_arena.glb-b6a9df3b338c8b0345fe2642d8b715eb.scn" + +[deps] + +source_file="res://Assets/bear_level_arena.glb" +dest_files=["res://.godot/imported/bear_level_arena.glb-b6a9df3b338c8b0345fe2642d8b715eb.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/Bear.tscn b/Bear.tscn new file mode 100644 index 0000000..284e520 --- /dev/null +++ b/Bear.tscn @@ -0,0 +1,29 @@ +[gd_scene load_steps=5 format=3 uid="uid://bear_scene_v1"] + +[ext_resource type="Script" path="res://bear.gd" id="1_beargd"] +[ext_resource type="PackedScene" uid="uid://cofv5dbavesis" path="res://Assets/Bear.glb" id="2_bearglb"] + +[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"] +radius = 1.0 +height = 2.4 + +[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_hit"] +radius = 1.3 +height = 2.7 + +[node name="Bear" type="CharacterBody3D"] +script = ExtResource("1_beargd") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.2, 0) +shape = SubResource("CapsuleShape3D_body") + +[node name="HitArea" type="Area3D" parent="."] +monitoring = true +monitorable = false + +[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.35, 0) +shape = SubResource("CapsuleShape3D_hit") + +[node name="bear_model" parent="." instance=ExtResource("2_bearglb")] diff --git a/Blender/bear_level_arena.blend b/Blender/bear_level_arena.blend new file mode 100644 index 0000000..3778ef1 Binary files /dev/null and b/Blender/bear_level_arena.blend differ diff --git a/CLAUDE.md b/CLAUDE.md index 9036adb..7296d42 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,17 +12,23 @@ I am a **Godot 4.6+ expert**. I follow current best practices for GDScript, scen | Path | Purpose | |---|---| -| `scene.tscn` | Main scene (arena + player) | +| `scene.tscn` | The reusable **shell** loaded for every level: player, camera, HUD, PS1 filter, and an empty `LevelRoot`. Its `game_shell.gd` instances the active level's module into `LevelRoot` at runtime | +| `levels/` | The level system: per-level module scenes plus the code that defines, loads and sequences them (see rows below) | +| `levels/game_shell.gd` | Shell controller — on load, frees any current level module and instances `Run.active_level().level_scene` into `LevelRoot`. Swapping the whole subtree (never hiding it) is what keeps one level's meshes/colliders/spawners from leaking into the next | +| `levels/arena_level.tscn` | The arena **level module**: floor + wall colliders, arena meshes, `MatadorSpawner` (with child `Node3D` spawn points placed by the designer), lighting, audience (`PublikNode`). Instanced into the shell's `LevelRoot` | +| `levels/bear_level.tscn` | The Bear's Den **level module**: bear-arena mesh, its own floor collider, lighting, and a `BearSpawner` (`bear_spawn.gd`) that places the bear at center. Instanced into the shell's `LevelRoot` | | `Player.tscn` | Player scene root (`CharacterBody3D`) | | `player.gd` | Movement, charge, turning logic | | `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) | | `camera_follow.gd` | Smooth camera follow (`Camera3D`) | | `matador.gd` | Matador AI (wander / ragdoll state machine) | -| `matador_spawn.gd` | Spawns N matadors at random arena positions | -| `run_state.gd` | Run progression autoload (`Run`) — Slay-the-Spire map + player position across fights | -| `level_def.gd` | `LevelDef` resource — a level's name / scene / map colour (arena only for now) | -| `map_node.gd` | `MapNode` — one runtime node on the run map (position, level, links) | -| `MapScreen.tscn` / `map_screen.gd` | Between-fights map screen; win → pick a node → next level | +| `levels/matador_spawn.gd` | Arena spawner — one matador per designer-placed child `Node3D` spawn point (each enters with a jump-roll toward center); falls back to a programmatic spread if no spawn points are present. Also handles the split-on-death hydra logic | +| `levels/bear_spawn.gd` | Bear-level spawner — instantiates `enemy_scene` at the arena center; emits the same `all_defeated` / `matador_killed` signals as `matador_spawn.gd` | +| `Bear.tscn` / `bear.gd` | Dark-Souls-style bear boss (`CharacterBody3D`). Routine state machine (wind-up → active → recovery) picking telegraphed moves by range band: Double Swipe / Overhead Smash (close), Leap Slam (mid-far), plus Stalk (2-leg walk-in) / Prowl (4-leg lope) positioning. Melee steps in during the swing (`bear_lunge_speed`) + tracks the bull; the leap is frame-synced to JUMP_SMASH so the paws hit ground on the impact frame (`bear_leap_impact_frame`); slams kick up dust + shake. Combos chain, hyper-armour mid-swing + poise (`bear_stagger_cd`), phase 2 enrage under `bear_enrage_frac` HP. Never damages the bull by colliding — only swings/slams. Joins `&"matador"` so bull abilities already hit it. Tuning under the `Bear` DP section | +| `levels/run_state.gd` | Run progression autoload (`Run`) — Slay-the-Spire map + player position across fights | +| `levels/level_def.gd` | `LevelDef` resource — a level's name / map colour / `scene_path` (always the shell) / `level_scene` (the level module instanced into the shell) / optional `enemy_scene` (arena wave vs. a boss like the bear) | +| `levels/map_node.gd` | `MapNode` — one runtime node on the run map (position, level, links) | +| `levels/MapScreen.tscn` / `levels/map_screen.gd` | Between-fights map screen; win → pick a node → next level | | `debug_params.gd` | Runtime-tunable parameter registry (autoload `DP`) | | `Assets/` | Raw 3D assets (`.glb`, `.fbx`) | | `Blender/` | Blender source files, animation scripts, FBX exports | @@ -92,10 +98,12 @@ After FBX import, animations appear as `Armature|walk` and `Armature|idle` in An bash run_tests.sh # Individual tests: -gdlint *.gd tests/*.gd # GDScript lint (gdtoolkit, installed via uv) +gdlint *.gd levels/*.gd tests/*.gd # GDScript lint (gdtoolkit, installed via uv) godot --headless --script tests/logic_test.gd # pure logic, ~2 s godot --headless --script tests/performance_test.gd # frame-budget regression guard, ~4 s godot --headless --script tests/gameplay_test.gd # full scene, bone sanity, ~4 s +godot --headless --script tests/level_switch_test.gd # no arena geometry leaks into the bear level, ~2 s +godot --headless --script tests/bear_boss_test.gd # bear boss moveset: routines, leap landing, hyper-armour, ~10 s # Gameplay test with real rendering (inspect frame strip visually): godot --script tests/gameplay_test.gd # opens a window, saves real screenshots diff --git a/bear.gd b/bear.gd new file mode 100644 index 0000000..4f00bba --- /dev/null +++ b/bear.gd @@ -0,0 +1,882 @@ +extends CharacterBody3D +## Dark-Souls-style boss bear with a learnable moveset. From NEUTRAL it weighs its +## options by the range band to the bull and commits to one telegraphed routine — +## wind-up (tell) → active (the hit) → recovery (the player's punish window). A routine +## always plays to the end (hyper-armour: it takes damage mid-swing but isn't knocked out +## of it), so every attack is readable and punishable. Variety comes from which routine +## NEUTRAL picks (weighted per range band); predictability from each routine being fixed +## once entered. Below bear_enrage_frac HP it enters phase 2: snappier tells/recovery and +## longer combo chains. +## +## The bear only wounds the bull with its swings and slams — walking or leaping into the +## bull does nothing. It plugs into the bull's combat interface: it joins the &"matador" +## group so every bull ability already targets it (apply_ability_hit), and mauls the bull +## via take_sword_hit. + +# Positioning / neutral states, then the three attack routines, then reactions. +enum State { NEUTRAL, STALK, PROWL, SWIPE, SMASH, LEAP, STAGGER, DEAD } +# Every attack routine runs through these three beats. +enum Phase { WINDUP, ACTIVE, RECOVER } + +signal killed +signal health_changed(current: int, max_hp: int) + +var _state: State = State.NEUTRAL +var _phase: Phase = Phase.WINDUP +var _bull: CharacterBody3D = null +var _skeleton: Skeleton3D = null +var _anim_player: AnimationPlayer = null +var _blood_burst: CPUParticles3D = null + +var _hp: int = 10 +var _enraged: bool = false +var _phase_timer: float = 0.0 +var _active_len: float = 0.8 +var _think_timer: float = 0.0 +var _state_timer: float = 0.0 +var _stagger_timer: float = 0.0 +var _stagger_cd: float = 0.0 # poise window after a stagger during which hits don't stun +var _hits_done: int = 0 +var _leap_airborne: bool = false +# Solved per-leap so the arc lands on the clip's impact frame: the launch vertical speed +# and the custom gravity used only while airborne (so peak height and airtime are tuned +# independently of world gravity). +var _leap_up: float = 0.0 +var _leap_grav: float = 0.0 +# Attack routines queued to fire back-to-back as a combo, popped when the current one +# finishes its recovery. Element type is State. +var _combo_queue: Array = [] + +# Resolved at runtime from the rig's clip list — the Bear.glb ships Rigify-named clips +# (RUN_ON4LEGS, IDLE_standing, SMASH_HIT, …) that may import with any prefix, so we match +# by substring rather than hard-coding the full track names. +var _anim_idle: StringName = &"" # IDLE_ON4LEGS — neutral / recovery (the punish pose) +var _anim_run: StringName = &"" # RUN_ON4LEGS — prowl approach / neutral coast +var _anim_walk2: StringName = &"" # WALK_ON2LEGS — the 2-leg stalk-in +var _anim_stand: StringName = &"" # IDLE_standing — reared-up wind-up / roar +var _anim_swipe: StringName = &"" # ONE_TWO_HIT — double swipe +var _anim_smash: StringName = &"" # SMASH_HIT — overhead smash +var _anim_leap: StringName = &"" # JUMP_SMASH — leap slam + +const _ACCEL_FACTOR: float = 9.0 +# Hard deceleration for planted states (wind-up / recovery / neutral settle) so the bear +# never coasts across the ground with a stationary pose playing (no foot-sliding). +const _PLANT_DECEL: float = 22.0 +# The Bear.glb clips are authored at 30 fps (see Bear.glb.import); leap timing is keyed by +# frame, so convert frames→seconds with this. +const _ANIM_FPS: float = 30.0 +# How close the bear closes on the bull before it stops advancing (roughly the bear+bull +# body radii). Its swings still reach well past this; it just never tries to occupy the +# bull's spot, so it doesn't shove up onto it. +const _BULL_STANDOFF: float = 1.9 + +@onready var _mesh: Node3D = $bear_model +@onready var _hit_area: Area3D = $HitArea +var _dust: CPUParticles3D = null +# Claw-slash bursts bound to the paw bones, fired one per hit of the 1-2 punch. +var _claw_l: CPUParticles3D = null +var _claw_r: CPUParticles3D = null + + +func _ready() -> void: + add_to_group(&"matador") # so every existing bull ability already targets the bear + add_to_group(&"bear") + _hp = maxi(1, int(DP.f("bear_max_hp"))) + health_changed.emit(_hp, _hp) + _skeleton = _find_skeleton(_mesh) + _anim_player = _find_anim_player(_mesh) + if _anim_player: + _anim_idle = _resolve_anim(["IDLE_ON4LEGS", "IDLE"]) + _anim_run = _resolve_anim(["RUN_ON4LEGS", "RUN"]) + _anim_walk2 = _resolve_anim(["WALK_ON2LEGS", "WALK", "IDLE_standing"]) + _anim_stand = _resolve_anim(["IDLE_standing", "WALK_ON2LEGS"]) + _anim_swipe = _resolve_anim(["ONE_TWO_HIT", "SMASH_HIT"]) + _anim_smash = _resolve_anim(["SMASH_HIT", "ONE_TWO_HIT"]) + _anim_leap = _resolve_anim(["JUMP_SMASH", "SMASH_HIT"]) + # Loop the locomotion / idle clips; the attack clips (swipe/smash/leap) are one-shot, + # so their on-ground settle frames play once and then hold instead of looping. + for a in [_anim_idle, _anim_run, _anim_walk2, _anim_stand]: + _ensure_loop(a) + for a in [_anim_swipe, _anim_smash, _anim_leap]: + _set_no_loop(a) + _anim_player.playback_default_blend_time = 0.12 + _hit_area.body_entered.connect(_on_body_entered) + _blood_burst = preload("res://blood_burst.gd").new() + add_child(_blood_burst) + _dust = _make_slam_dust() + add_child(_dust) + _setup_claws() + _enter_neutral() + + +# Bind a claw-slash emitter to each paw bone (hand_l / hand_r) via a BoneAttachment3D, so +# a burst fired on a swipe hit erupts from that paw wherever the animation has it. +func _setup_claws() -> void: + if _skeleton == null: + return + _claw_r = _attach_claw("hand_r") + _claw_l = _attach_claw("hand_l") + + +func _attach_claw(bone: String) -> CPUParticles3D: + if _skeleton.find_bone(bone) == -1: + return null + var attach := BoneAttachment3D.new() + attach.bone_name = bone + _skeleton.add_child(attach) + var claw := _make_claw_slash() + attach.add_child(claw) + return claw + + +func get_hp() -> int: + return _hp + + +func _acquire_bull() -> void: + if _bull != null: + return + var players := get_tree().get_nodes_in_group(&"player") + if not players.is_empty(): + _bull = players[0] as CharacterBody3D + # The bear only ever hits via distance / area checks, never body collision, so let + # it pass through the bull. Otherwise the big capsule rides up and gets stuck on top + # of the bull when it lunges or lands a leap on it. + if _bull != null: + add_collision_exception_with(_bull) + + +func _physics_process(delta: float) -> void: + if not is_on_floor(): + # The leap uses its own gravity mid-flight so its arc height and airtime can be + # tuned to land on the clip's impact frame; everything else falls under world gravity. + if _state == State.LEAP and _phase == Phase.ACTIVE and _leap_grav > 0.0: + velocity.y -= _leap_grav * delta + else: + velocity += get_gravity() * delta + + if _state == State.DEAD: + velocity.x = 0.0 + velocity.z = 0.0 + move_and_slide() + return + + _acquire_bull() + _update_enrage() + _stagger_cd = maxf(0.0, _stagger_cd - delta) + + match _state: + State.NEUTRAL: _tick_neutral(delta) + State.STALK: _tick_stalk(delta) + State.PROWL: _tick_prowl(delta) + State.SWIPE: _tick_swipe(delta) + State.SMASH: _tick_smash(delta) + State.LEAP: _tick_leap(delta) + State.STAGGER: _tick_stagger(delta) + + move_and_slide() + + +# ── Phase 2 ────────────────────────────────────────────────────────────────────── + +func _update_enrage() -> void: + if _enraged: + return + if _hp <= int(ceil(DP.f("bear_max_hp") * DP.f("bear_enrage_frac"))): + _enraged = true + # Roar: a beat of stagger-free wind-up with a screen shake, so the phase flip reads. + _blood_burst.burst(global_position + Vector3(0.0, 1.6, 0.0), Vector3.UP) + _shake(0.9) + + +# Timers that shape readability (tells + recovery) shrink in phase 2; active windows and +# the clip-locked leap timings are left alone. +func _scaled(t: float) -> float: + return t * (DP.f("bear_enrage_scale") if _enraged else 1.0) + + +# ── NEUTRAL: think, then commit ──────────────────────────────────────────────────── + +func _enter_neutral() -> void: + _state = State.NEUTRAL + # A short, lightly-varied beat so the bear flows from one action into the next; phase 2 + # shrinks it further. Keep the jitter small so it reads as eager, not hesitant. + var think := DP.f("bear_think_time") + _think_timer = _scaled(think * randf_range(0.7, 1.3)) + + +func _tick_neutral(delta: float) -> void: + _decelerate(_PLANT_DECEL, delta) + if _bull != null: + _face_point(_bull.global_position, delta) + # Coast on the run clip while residual speed bleeds off, then settle to idle — never + # hold the idle pose while still sliding. + _play(_anim_run if _horizontal_speed() > 1.0 else _anim_idle) + _think_timer -= delta + if _think_timer <= 0.0: + _decide() + + +# Pop the next queued combo step, or pick a fresh routine weighted by the current range +# band. Weights lean into pressure (and chaining) in phase 2. +func _decide() -> void: + if not _combo_queue.is_empty(): + var next: State = _combo_queue.pop_front() + if _range_allows(next): + _start_routine(next) + return + _combo_queue.clear() + + if _bull == null: + _start_routine(State.PROWL) + return + + var dist := _flat_dist_to_bull() + var options: Array + if dist <= DP.f("bear_close_range"): + # In strike range: punch or overhead (both step in during the swing). + options = [[State.SWIPE, 3.0], [State.SMASH, 3.0 if _enraged else 2.5]] + elif dist <= DP.f("bear_mid_range"): + # Mid: mostly walk the bull down on two legs; occasionally leap the gap. + options = [[State.STALK, 3.0], [State.LEAP, 2.0 if _enraged else 1.5]] + else: + # Far: lope in on all fours, or spring across with a leap. + options = [[State.PROWL, 3.0], [State.LEAP, 1.5]] + _start_routine(_weighted_pick(options)) + + +# A queued combo step only fires if the bull is still roughly in its band, so a chain +# doesn't whiff into empty air after the player rolls away. +func _range_allows(routine: State) -> bool: + if _bull == null: + return routine == State.PROWL + var dist := _flat_dist_to_bull() + match routine: + State.SWIPE, State.SMASH: + return dist <= DP.f("bear_close_range") * 1.4 + State.LEAP: + return dist <= DP.f("bear_mid_range") * 1.5 + return true + + +func _start_routine(routine: State) -> void: + match routine: + State.SWIPE: _start_swipe() + State.SMASH: _start_smash() + State.LEAP: _start_leap() + State.STALK: _start_stalk() + State.PROWL: _start_prowl() + _: _enter_neutral() + + +# When a routine's recovery ends: fire the next combo step if one is queued, else think. +func _finish_routine() -> void: + if not _combo_queue.is_empty(): + _decide() + else: + _enter_neutral() + + +# ── STALK: a menacing 2-leg walk-in that closes the gap into strike range ──────────── + +func _start_stalk() -> void: + _state = State.STALK + _state_timer = DP.f("bear_stalk_time") + + +func _tick_stalk(delta: float) -> void: + _state_timer -= delta + if _bull == null or _state_timer <= 0.0 or _flat_dist_to_bull() <= DP.f("bear_close_range"): + _enter_neutral() + return + var to_bull := _flat_to_bull() + _accelerate(to_bull.normalized(), DP.f("bear_stalk_speed"), delta) + _face_point(_bull.global_position, delta) + _play(_anim_walk2) + + +# ── PROWL: lope in on all fours to close a big gap ───────────────────────────────── + +func _start_prowl() -> void: + _state = State.PROWL + _state_timer = 1.6 + + +func _tick_prowl(delta: float) -> void: + _state_timer -= delta + if _bull == null or _state_timer <= 0.0 or _flat_dist_to_bull() <= DP.f("bear_mid_range") * 0.9: + _enter_neutral() + return + var to_bull := _flat_to_bull() + _accelerate(to_bull.normalized(), DP.f("bear_run_speed"), delta) + _face_point(_bull.global_position, delta) + _play(_anim_run) + + +# ── SWIPE: Double Swipe (ONE_TWO_HIT), two quick hits ────────────────────────────── + +func _start_swipe() -> void: + _state = State.SWIPE + _phase = Phase.WINDUP + _phase_timer = _scaled(DP.f("bear_swipe_windup")) + _hits_done = 0 + _play(_anim_stand) + # Sometimes chain the big smash after the swipe (always in phase 2) — the signature + # close string the player learns to bait and punish. + if _combo_queue.is_empty() and (_enraged or randf() < DP.f("bear_combo_chance")): + _combo_queue.append(State.SMASH) + + +func _tick_swipe(delta: float) -> void: + match _phase: + Phase.WINDUP: + _decelerate(_PLANT_DECEL, delta) # rear up in place — no slide + _face_point_if_bull(delta) # track hard before committing + _play(_anim_stand) + _phase_timer -= delta + if _phase_timer <= 0.0: + _begin_attack_active(_anim_swipe, 0.8) + Phase.ACTIVE: + _lunge_and_track(DP.f("bear_lunge_speed"), delta) + _phase_timer -= delta + var frac := 1.0 - _phase_timer / maxf(_active_len, 0.001) + if _hits_done == 0 and frac >= 0.25: + _hits_done = 1 + _try_hit(DP.f("bear_swipe_reach")) + _claw_slash(true) # first paw (right) rakes across + elif _hits_done == 1 and frac >= 0.65: + _hits_done = 2 + _try_hit(DP.f("bear_swipe_reach")) + _claw_slash(false) # back-hand with the left paw + if _phase_timer <= 0.0: + _begin_recover(DP.f("bear_swipe_recover")) + Phase.RECOVER: + _tick_recover(delta) + + +# ── SMASH: Overhead Smash (SMASH_HIT), slower tell, big punish window ──────────────── + +func _start_smash() -> void: + _state = State.SMASH + _phase = Phase.WINDUP + _phase_timer = _scaled(DP.f("bear_smash_windup")) + _hits_done = 0 + _play(_anim_stand) + + +func _tick_smash(delta: float) -> void: + match _phase: + Phase.WINDUP: + _decelerate(_PLANT_DECEL, delta) + _face_point_if_bull(delta) + _play(_anim_stand) + _phase_timer -= delta + if _phase_timer <= 0.0: + _begin_attack_active(_anim_smash, 0.9) + Phase.ACTIVE: + # The overhead steps in less than the swipe — it's a commitment, not a rush. + _lunge_and_track(DP.f("bear_lunge_speed") * 0.5, delta) + _phase_timer -= delta + var frac := 1.0 - _phase_timer / maxf(_active_len, 0.001) + if _hits_done == 0 and frac >= 0.4: + _hits_done = 1 + _slam(DP.f("bear_smash_radius"), 0.8) + if _phase_timer <= 0.0: + _begin_recover(DP.f("bear_smash_recover")) + Phase.RECOVER: + _tick_recover(delta) + + +# Shared wind-up → active handoff for the melee swings: start the clip (sped up for snappy +# punches) and size the active window to the sped-up clip length. +func _begin_attack_active(clip: StringName, fallback_len: float) -> void: + _phase = Phase.ACTIVE + var spd := maxf(DP.f("bear_attack_anim_speed"), 0.1) + _active_len = _anim_length(clip, fallback_len) / spd + _phase_timer = _active_len + _play_attack(clip, spd) + + +# ── LEAP: Leap Slam (JUMP_SMASH) — clip-synced ballistic gap-closer + AoE ──────────── +# The one JUMP_SMASH clip runs start-to-finish across the whole routine: it plays the +# crouch on the ground (wind-up), springs at the takeoff frame, the arc is solved so the +# paws hit ground exactly on the impact frame, and the last frames settle on the ground. + +func _start_leap() -> void: + _state = State.LEAP + _phase = Phase.WINDUP + _hits_done = 0 + _leap_airborne = false + _leap_grav = 0.0 + _active_len = _anim_length(_anim_leap, 1.0) + _play_attack(_anim_leap, 1.0) # play from frame 0 at real speed, grounded crouch + _phase_timer = DP.f("bear_leap_takeoff_frame") / _ANIM_FPS # ground time before the spring + # In phase 2 a leap sometimes follows straight into a swipe once it lands close. + if _enraged and _combo_queue.is_empty() and randf() < 0.5: + _combo_queue.append(State.SWIPE) + + +func _tick_leap(delta: float) -> void: + match _phase: + Phase.WINDUP: + _decelerate(_PLANT_DECEL, delta) # crouch in place + _face_point_if_bull(delta) # aim the pounce, tracking the bull + _phase_timer -= delta + if _phase_timer <= 0.0: + _launch_leap() + Phase.ACTIVE: + _phase_timer -= delta + if not is_on_floor(): + _leap_airborne = true + elif _leap_airborne or _phase_timer <= 0.0: + velocity.x = 0.0 + velocity.z = 0.0 + _slam(DP.f("bear_slam_radius"), 0.7) + _phase = Phase.RECOVER + _phase_timer = _scaled(DP.f("bear_leap_recover")) + Phase.RECOVER: + _decelerate(_PLANT_DECEL, delta) + # Let the clip's on-ground settle frames finish first, then idle out the recovery. + if _anim_player != null and _anim_player.is_playing() \ + and _anim_player.current_animation == _anim_leap: + return + _play(_anim_idle) + _phase_timer -= delta + if _phase_timer <= 0.0: + _finish_routine() + + +func _launch_leap() -> void: + _phase = Phase.ACTIVE + _leap_airborne = false + var target := _bull.global_position if _bull != null else global_position + var flat := target - global_position + flat.y = 0.0 + var dist := flat.length() + var dir := flat.normalized() if dist > 0.1 else _facing() + _face_dir(dir, 1.0) + # Airtime = the clip span between the takeoff and impact frames, so the paws hit ground + # on the impact frame (25). A custom leap gravity lets the arc reach bear_leap_height in + # exactly that airtime regardless of world gravity: for a symmetric hop of airtime T to + # peak height H, v_up = 4H/T and g = 8H/T². + var air_t := maxf( + (DP.f("bear_leap_impact_frame") - DP.f("bear_leap_takeoff_frame")) / _ANIM_FPS, 0.05) + var h := maxf(DP.f("bear_leap_height"), 0.1) + _leap_up = 4.0 * h / air_t + _leap_grav = 8.0 * h / (air_t * air_t) + # Land just in front of the bull, not on top of it — the slam radius still catches it. + var land_dist := maxf(dist - _BULL_STANDOFF, 0.0) + var h_speed := clampf(land_dist / air_t, 0.0, DP.f("bear_leap_speed")) + velocity = dir * h_speed + Vector3.UP * _leap_up + _phase_timer = air_t + 1.0 # safety fallback if a landing is missed + + +func _begin_recover(secs: float) -> void: + _phase = Phase.RECOVER + _phase_timer = _scaled(secs) + + +func _tick_recover(delta: float) -> void: + _decelerate(_PLANT_DECEL, delta) + _play(_anim_idle) + _phase_timer -= delta + if _phase_timer <= 0.0: + _finish_routine() + + +func _tick_stagger(delta: float) -> void: + _decelerate(12.0, delta) + _stagger_timer -= delta + _play(_anim_idle) + if _stagger_timer <= 0.0: + _stagger_cd = DP.f("bear_stagger_cd") # poise window so it can act before the next stun + _enter_neutral() + + +# Step toward the bull during a swing (so a strike lands even when the bull is a touch out +# of reach) while tracking its facing — but plant once close so the bear doesn't shove +# through it. Motion here reads as the strike's lunge, under the attack clip. +func _lunge_and_track(speed: float, delta: float) -> void: + if _bull == null: + _decelerate(_PLANT_DECEL, delta) + return + _face_point(_bull.global_position, delta, DP.f("bear_track_active")) + var to_bull := _flat_to_bull() + var dist := to_bull.length() + if speed > 0.0 and dist > _BULL_STANDOFF: + _accelerate(to_bull / dist, speed, delta) + else: + _decelerate(_PLANT_DECEL, delta) + + +# ── Hitting the bull ─────────────────────────────────────────────────────────────── + +# Directional strike: the bull is mauled inside `reach` of the bear that's facing it +# (a fast bull tunnels a thin collider between frames, so distance + facing is the +# reliable hit). Returns whether it connected. +func _try_hit(reach: float) -> bool: + if _bull == null: + return false + var to_bull := _flat_to_bull() + var dist := to_bull.length() + if dist > reach or dist < 0.01: + return false + if _facing().dot(to_bull / dist) < cos(deg_to_rad(DP.f("bear_hit_arc"))): + return false + _bull.call(&"take_sword_hit", "mauled") + _shake(0.55) + return true + + +# Radial slam (smash / leap landing): hits any way the bull is, within `radius`, with a +# ground-pound shake and a burst of dust kicked up at the paws. No facing arc — you're +# caught if you're near where it comes down. +func _slam(radius: float, shake: float) -> void: + _slam_dust() + if _bull == null: + _shake(shake) + return + if _flat_dist_to_bull() <= radius: + _bull.call(&"take_sword_hit", "mauled") + _shake(shake) + + +# Kick up a one-shot ring of dust at the bear's feet where the slam lands. +func _slam_dust() -> void: + if _dust == null: + return + _dust.global_position = global_position + Vector3(0.0, 0.15, 0.0) + _dust.restart() + _dust.emitting = true + + +# Tan ground-impact dust: a fast, wide, low burst that arcs up and settles — built in code +# so the Bear scene needs no extra particle node. +func _make_slam_dust() -> CPUParticles3D: + var p := CPUParticles3D.new() + var mat := StandardMaterial3D.new() + mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + mat.vertex_color_use_as_albedo = true + mat.cull_mode = BaseMaterial3D.CULL_DISABLED + + var sphere := SphereMesh.new() + sphere.radius = 0.16 + sphere.height = 0.32 + sphere.radial_segments = 4 + sphere.rings = 2 + sphere.material = mat + + var ramp := Gradient.new() + ramp.set_color(0, Color(0.72, 0.60, 0.40, 0.9)) + ramp.set_color(1, Color(0.55, 0.45, 0.30, 0.0)) + + var scale_curve := Curve.new() + scale_curve.add_point(Vector2(0.0, 0.4)) + scale_curve.add_point(Vector2(0.35, 1.0)) + scale_curve.add_point(Vector2(1.0, 0.0)) + + p.mesh = sphere + p.color_ramp = ramp + p.scale_amount_curve = scale_curve + p.emitting = false + p.one_shot = true + p.explosiveness = 1.0 + p.amount = 36 + p.lifetime = 0.9 + p.randomness = 0.5 + p.local_coords = false + # A flat-ish disc kicked outward and up from the ground. + p.emission_shape = CPUParticles3D.EMISSION_SHAPE_SPHERE + p.emission_sphere_radius = 0.5 + p.direction = Vector3(0.0, 1.0, 0.0) + p.spread = 75.0 + p.gravity = Vector3(0.0, -6.0, 0.0) + p.initial_velocity_min = 2.5 + p.initial_velocity_max = 6.5 + p.scale_amount_min = 0.6 + p.scale_amount_max = 1.4 + return p + + +# Fire the claw-slash burst on one paw for a swipe hit (right paw first, then left). +func _claw_slash(right: bool) -> void: + var claw := _claw_r if right else _claw_l + if claw == null: + return + claw.restart() + claw.emitting = true + + +# A short fan of pale streaks raked from a paw — thin boxes aligned to their velocity so +# they read as claw marks slashing outward. Built in code and parented to a hand bone. +func _make_claw_slash() -> CPUParticles3D: + var p := CPUParticles3D.new() + var mat := StandardMaterial3D.new() + mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + mat.blend_mode = BaseMaterial3D.BLEND_MODE_ADD + mat.vertex_color_use_as_albedo = true + mat.cull_mode = BaseMaterial3D.CULL_DISABLED + + # A thin, long box becomes a streak once aligned to its velocity. + var streak := BoxMesh.new() + streak.size = Vector3(0.03, 0.55, 0.03) + streak.material = mat + + var ramp := Gradient.new() + ramp.set_color(0, Color(0.85, 0.95, 1.0, 0.95)) + ramp.set_color(1, Color(0.5, 0.75, 1.0, 0.0)) + + var scale_curve := Curve.new() + scale_curve.add_point(Vector2(0.0, 1.0)) + scale_curve.add_point(Vector2(0.7, 1.0)) + scale_curve.add_point(Vector2(1.0, 0.0)) + + p.mesh = streak + p.color_ramp = ramp + p.scale_amount_curve = scale_curve + p.emitting = false + p.one_shot = true + p.explosiveness = 1.0 # all at once = a single rake, not a stream + p.amount = 5 # a handful of parallel claw marks + p.lifetime = 0.22 + p.randomness = 0.2 + p.local_coords = false # streaks stay in the air where they were raked, then fade + p.set_particle_flag(CPUParticles3D.PARTICLE_FLAG_ALIGN_Y_TO_VELOCITY, true) + # Rake across the paw's swing plane: a tight fan of fast streaks. + p.emission_shape = CPUParticles3D.EMISSION_SHAPE_SPHERE + p.emission_sphere_radius = 0.12 + p.direction = Vector3(0.0, 0.0, 1.0) + p.spread = 22.0 + p.gravity = Vector3.ZERO + p.initial_velocity_min = 6.0 + p.initial_velocity_max = 9.0 + return p + + +func _shake(amount: float) -> void: + if _bull == null: + return + var cam := _bull.get_node_or_null("Camera3D") + if cam: + cam.call(&"trigger_hit", clampf(amount, 0.2, 1.0)) + + +# ── Taking damage ────────────────────────────────────────────────────────────────── + +# Called by every bull ability (kick / slam / roll) that iterates the matador group. +func apply_ability_hit(hit_dir: Vector3, strength: float, _up_boost: float = 0.0) -> void: + _acquire_bull() + _take_hit(hit_dir, strength) + + +# Ramming: only a real charge from the bull wounds the bear; a gentle nudge is shrugged +# off. The bear never damages the bull by colliding — only its swings/slams do. +func _on_body_entered(body: Node3D) -> void: + if _state == State.DEAD or not body.is_in_group(&"player"): + return + var player := body as CharacterBody3D + if player.velocity.length() < DP.f("bear_hit_threshold"): + return + var flat := Vector3(player.velocity.x, 0.0, player.velocity.z) + var dir := flat.normalized() if flat.length() > 0.5 else \ + (global_position - player.global_position).normalized() + _take_hit(dir, player.velocity.length()) + + +func _take_hit(hit_dir: Vector3, strength: float) -> void: + if _state == State.DEAD: + return + _hp -= 1 + health_changed.emit(_hp, int(DP.f("bear_max_hp"))) + hit_dir.y = 0.0 + hit_dir = hit_dir.normalized() + _blood_burst.burst(global_position + Vector3(0.0, 1.4, 0.0), hit_dir) + _shake(clampf(strength / 15.0, 0.4, 1.0)) + if _hp <= 0: + _die(hit_dir) + return + # Hyper-armour: a committed swing takes the wound but isn't knocked out of it — only + # the vulnerable states (neutral / positioning / a routine's recovery) stagger. This + # is what makes the recovery the real punish window. + if not _can_stagger(): + return + velocity.x = hit_dir.x * 4.0 + velocity.z = hit_dir.z * 4.0 + _combo_queue.clear() + _state = State.STAGGER + _stagger_timer = DP.f("bear_stagger_time") + + +func _can_stagger() -> bool: + if _stagger_cd > 0.0: + return false # poise: recently staggered, so it rides out hits instead of stun-locking + match _state: + State.NEUTRAL, State.STALK, State.PROWL, State.STAGGER: + return true + State.SWIPE, State.SMASH, State.LEAP: + return _phase == Phase.RECOVER # airborne / mid-swing can't be staggered + return false + + +func _die(hit_dir: Vector3) -> void: + _state = State.DEAD + killed.emit() + collision_layer = 0 + if _hit_area: + _hit_area.monitoring = false + velocity = Vector3.ZERO + _blood_burst.burst(global_position + Vector3(0.0, 1.4, 0.0), hit_dir) + _play(_anim_idle) + # No death clip on the rig — topple the beast onto its side (about the hit direction, + # so it falls the way it was struck) and sink it away, then free. + var fall_axis := Vector3(-hit_dir.z, 0.0, hit_dir.x) + if fall_axis.length() < 0.01: + fall_axis = Vector3.RIGHT + var tween := create_tween() + tween.tween_property(_mesh, "rotation", + _mesh.rotation + fall_axis.normalized() * deg_to_rad(88.0), 0.7)\ + .set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) + tween.tween_interval(2.5) + tween.tween_property(_mesh, "position:y", _mesh.position.y - 3.0, 1.0) + tween.tween_callback(queue_free) + + +# ── Interface shared with the matador (guarded elsewhere via has_method) ───────────── + +func is_active() -> bool: + return _state != State.DEAD + + +func ai_state_name() -> String: + var n := State.keys()[_state] as String + if _state in [State.SWIPE, State.SMASH, State.LEAP]: + n += ":" + (Phase.keys()[_phase] as String) + if _enraged: + n += "*" + return n + + +# ── Movement / geometry helpers ───────────────────────────────────────────────────── + +func _flat_to_bull() -> Vector3: + var to := _bull.global_position - global_position + to.y = 0.0 + return to + + +func _flat_dist_to_bull() -> float: + return _flat_to_bull().length() + + +func _horizontal_speed() -> float: + return Vector2(velocity.x, velocity.z).length() + + +func _facing() -> Vector3: + return Vector3(sin(_mesh.rotation.y), 0.0, cos(_mesh.rotation.y)) + + +func _accelerate(dir: Vector3, spd: float, delta: float) -> void: + velocity.x = move_toward(velocity.x, dir.x * spd, spd * _ACCEL_FACTOR * delta) + velocity.z = move_toward(velocity.z, dir.z * spd, spd * _ACCEL_FACTOR * delta) + + +func _decelerate(rate: float, delta: float) -> void: + velocity.x = move_toward(velocity.x, 0.0, rate * delta) + velocity.z = move_toward(velocity.z, 0.0, rate * delta) + + +func _face_point_if_bull(delta: float, rate: float = -1.0) -> void: + if _bull != null: + _face_point(_bull.global_position, delta, rate) + + +func _face_point(point: Vector3, delta: float, rate: float = -1.0) -> void: + _face_dir(point - global_position, delta, rate) + + +func _face_dir(dir: Vector3, delta: float, rate: float = -1.0) -> void: + if rate < 0.0: + rate = DP.f("bear_turn_rate") + dir.y = 0.0 + if dir.length_squared() > 0.01: + _mesh.rotation.y = lerp_angle(_mesh.rotation.y, atan2(dir.x, dir.z), delta * rate) + + +func _weighted_pick(options: Array) -> State: + var total := 0.0 + for o: Array in options: + total += o[1] + var r := randf() * total + for o: Array in options: + r -= o[1] + if r <= 0.0: + return o[0] + return options[0][0] + + +# ── Animation helpers ─────────────────────────────────────────────────────────────── + +func _resolve_anim(candidates: Array) -> StringName: + if _anim_player == null: + return &"" + var list := _anim_player.get_animation_list() + for want: String in candidates: + for have: String in list: + if have.to_upper().contains(want.to_upper()): + return StringName(have) + return &"" + + +func _ensure_loop(anim_name: StringName) -> void: + if anim_name != &"" and _anim_player.has_animation(anim_name): + _anim_player.get_animation(anim_name).loop_mode = Animation.LOOP_LINEAR + + +func _set_no_loop(anim_name: StringName) -> void: + if anim_name != &"" and _anim_player.has_animation(anim_name): + _anim_player.get_animation(anim_name).loop_mode = Animation.LOOP_NONE + + +func _anim_length(anim_name: StringName, fallback: float) -> float: + if _anim_player and anim_name != &"" and _anim_player.has_animation(anim_name): + return _anim_player.get_animation(anim_name).length + return fallback + + +func _play(anim_name: StringName) -> void: + if _anim_player == null or anim_name == &"": + return + if _anim_player.current_animation == anim_name: + return + _anim_player.play(anim_name) + + +# Restart a one-shot attack clip from its first frame at `speed` (even if already current). +func _play_attack(anim_name: StringName, speed: float) -> void: + if _anim_player == null or anim_name == &"": + return + _anim_player.play(anim_name, -1.0, speed) + _anim_player.seek(0.0, true) + + +func _find_skeleton(node: Node) -> Skeleton3D: + if node is Skeleton3D: + return node as Skeleton3D + for child: Node in node.get_children(): + var r := _find_skeleton(child) + if r: + return r + return null + + +func _find_anim_player(node: Node) -> AnimationPlayer: + if node is AnimationPlayer: + return node as AnimationPlayer + for child: Node in node.get_children(): + var r := _find_anim_player(child) + if r: + return r + return null diff --git a/bear.gd.uid b/bear.gd.uid new file mode 100644 index 0000000..a2169ad --- /dev/null +++ b/bear.gd.uid @@ -0,0 +1 @@ +uid://cn7fhjkro7637 diff --git a/console.gd b/console.gd index 58b8a36..fb470ca 100644 --- a/console.gd +++ b/console.gd @@ -18,6 +18,8 @@ const _COMMANDS := { "diff": "list params changed from default", "toggle": "flip a bool param", "list": "print params (optional section)", + "level": "select level and reload (level — arena / arena_boss / bear)", + "restart": "reload the current level scene", "clear": "clear the log", "help": "show this help", } @@ -472,6 +474,12 @@ func _run_command(parts: PackedStringArray) -> bool: _cmd_diff() "list": _cmd_list(parts) + "level": + _cmd_level(parts) + "restart": + _log_ok("Restarting…") + get_tree().paused = false + get_tree().reload_current_scene() "clear": _log.clear() "help": @@ -581,9 +589,9 @@ func _set_param(key: String, meta: Dictionary, raw: String) -> void: var lo := meta["min"] as float var hi := meta["max"] as float if not raw.is_valid_float() or not is_finite(raw.to_float()): - _log_warn("%s wants a number %s … %s" % [key, _fmt_num(lo), _fmt_num(hi)]) + _log_warn("%s wants a number" % key) return - var v := snappedf(clampf(raw.to_float(), lo, hi), meta["step"] as float) + var v := raw.to_float() DP.set_value(key, v) _log_ok("[b]%s[/b] ← [color=#ffe080]%s[/color]" % [key, _fmt_num(v)]) elif meta["type"] == TYPE_BOOL: @@ -600,6 +608,25 @@ func _set_param(key: String, meta: Dictionary, raw: String) -> void: _log_warn("Cannot set param of type %d" % meta["type"]) +func _cmd_level(parts: PackedStringArray) -> void: + if parts.size() == 1: + var cur := Run.active_level() + var cur_name := (cur.id as String) if cur != null else "none" + _log_ok("Current level: [b]%s[/b]" % cur_name) + var ids: Array[StringName] = Run.level_ids() + _log_raw("Available: " + " ".join(ids.map(func(id: StringName) -> String: return str(id)))) + return + var id := StringName(parts[1].to_lower()) + var level := Run.get_level(id) + if level == null: + _log_warn("Unknown level '%s'. Try: %s" % [parts[1], " ".join(Run.level_ids().map(func(i: StringName) -> String: return str(i)))]) + return + Run.debug_level_override = level + _log_ok("Level set to [b]%s[/b] — reloading…" % str(id)) + get_tree().paused = false + get_tree().change_scene_to_file(level.scene_path) + + func _cmd_list(parts: PackedStringArray) -> void: var filter := parts[1].to_lower() if parts.size() > 1 else "" var all := DP.get_all() diff --git a/debug_params.gd b/debug_params.gd index fb95e54..6f8e5d9 100644 --- a/debug_params.gd +++ b/debug_params.gd @@ -17,7 +17,6 @@ var _values: Dictionary = {} func _ready() -> void: _register_all() - load_saved() func _register_all() -> void: @@ -46,12 +45,12 @@ func _register_all() -> void: _reg_f("Dust", "dust_explosiveness", 0.35, 0.0, 1.0) _reg_f("Dust", "dust_spread", 45.0, 5.0, 90.0) _reg_f("Dust", "dust_gravity_y", -1.5,-15.0, 0.0) - _reg_f("Dust", "walk_scale_min", 0.35, 0.05, 3.0) - _reg_f("Dust", "walk_scale_max", 0.65, 0.05, 3.0) + _reg_f("Dust", "walk_scale_min", 0.20, 0.05, 3.0) + _reg_f("Dust", "walk_scale_max", 0.40, 0.05, 3.0) _reg_f("Dust", "walk_vel_min", 0.8, 0.1, 12.0) _reg_f("Dust", "walk_vel_max", 1.8, 0.1, 12.0) - _reg_f("Dust", "charge_scale_min", 1.0, 0.05, 6.0) - _reg_f("Dust", "charge_scale_max", 2.2, 0.05, 6.0) + _reg_f("Dust", "charge_scale_min", 0.35, 0.05, 6.0) + _reg_f("Dust", "charge_scale_max", 0.70, 0.05, 6.0) _reg_f("Dust", "charge_vel_min", 2.8, 0.1, 20.0) _reg_f("Dust", "charge_vel_max", 5.5, 0.1, 20.0) # ── Tail ────────────────────────────────────────────────────────────────── @@ -99,16 +98,11 @@ func _register_all() -> void: _reg_f("Camera", "cam_drag_speed", 3.5, 0.5, 20.0) _reg_f("Camera", "cam_follow_lerp", 10.0, 1.0, 40.0) # ── Matador ─────────────────────────────────────────────────────────────── - _reg_f("Matador", "mat_spawn_count", 1.0, 1.0, 30.0, 1.0) - # Hydra split: a slain matador is replaced on the spot by mat_split_factor fresh - # matadors, so the horde doubles each generation (1 → 2 → 4 → 8 …) at the default - # 2. Capped at mat_split_generations deep so the arena stays winnable (factor <2 or - # generations 0 = no splitting, the classic one-and-done arena). - _reg_f("Matador", "mat_split_factor", 2.0, 1.0, 6.0, 1.0) - _reg_f("Matador", "mat_split_generations", 3.0, 0.0, 6.0, 1.0) - # The bull wins outright once it has slain this many matadors, even if the hydra - # still has bodies on the sand. 0 = disabled (win only when the horde dies out) - _reg_f("Matador", "mat_win_kills", 21.0, 0.0, 200.0, 1.0) + # Wave system: matadors enter the arena in escalating waves (1 → 2 → … → N where N + # is the number of placed spawn points). mat_spawn_stagger is the delay between each + # matador leaping in within a wave; mat_wave_pause is the breather after a wave clears. + _reg_f("Matador", "mat_spawn_stagger", 0.4, 0.0, 3.0, 0.05) + _reg_f("Matador", "mat_wave_pause", 2.0, 0.0, 8.0, 0.25) _reg_f("Matador", "mat_walk_speed", 5.5, 0.5, 10.0) _reg_f("Matador", "mat_wander_radius", 22.0, 5.0, 50.0) # Radius of the inner fighting floor: bull + matadors spawn on this ring (so @@ -190,6 +184,76 @@ func _register_all() -> void: _reg_f("Matador", "mat_taunt_hold", 8.0, 1.0, 20.0, 0.5) # Small so it smooths dithering without locking the matador into idling. _reg_f("Matador", "mat_w_commit", 0.15, 0.0, 2.0, 0.05) + # ── Bear (boss) ───────────────────────────────────────────────────────────── + # A Dark-Souls-style boss with a learnable moveset. It soaks bear_max_hp clean hits + # (ram or ability), staggering on each, and only topples when worn down. From NEUTRAL + # it weighs its options by the range band to the bull and commits to one telegraphed + # routine (wind-up → active → recovery); each routine plays to the end (the recovery + # is the player's punish window). Below bear_enrage_frac HP it enters phase 2: faster + # tells and recovery (× bear_enrage_scale) and longer combo chains. + _reg_f("Bear", "bear_max_hp", 10.0, 1.0, 40.0, 1.0) + # HP fraction at which phase 2 ("Fury") triggers, and how much it multiplies every + # wind-up / recovery timer (lower = snappier, more dangerous). + _reg_f("Bear", "bear_enrage_frac", 0.4, 0.0, 1.0, 0.05) + _reg_f("Bear", "bear_enrage_scale", 0.7, 0.3, 1.0, 0.05) + # Range bands (m) that pick the moveset: ≤close it strikes (swipe / smash), ≤mid it + # walks in to close the gap (stalk) or leaps, beyond it lopes in (prowl) or leaps. + # close is deliberately generous — the bear commits to melee "from a bit further" and + # steps in during the swing (see bear_lunge_speed) rather than needing to be on top. + _reg_f("Bear", "bear_close_range", 5.0, 1.0, 12.0, 0.1) + _reg_f("Bear", "bear_mid_range", 9.0, 3.0, 20.0, 0.5) + # Beat spent in NEUTRAL sizing up the bull between routines — kept short so the bear + # flows straight from one action into the next instead of pausing (raise for a more + # hesitant, readable boss; near-0 for relentless pressure). Shrinks again in phase 2. + _reg_f("Bear", "bear_think_time", 0.06, 0.0, 1.0, 0.01) + # Locomotion: the all-fours prowl (close a big gap), the menacing 2-leg walk-in + # (stalk), the facing rate while positioning/winding up, and the softer tracking rate + # a swing keeps on the bull mid-attack so it's harder to just walk out of. + _reg_f("Bear", "bear_run_speed", 11.0, 1.0, 20.0, 0.1) + _reg_f("Bear", "bear_stalk_speed", 5.5, 0.5, 12.0, 0.1) + _reg_f("Bear", "bear_turn_rate", 7.0, 1.0, 20.0, 0.5) + _reg_f("Bear", "bear_track_active", 3.5, 0.0, 12.0, 0.1) + # Forward step-in during a swing so a strike lands even when the bull is a touch out of + # reach (also carries the tracking); 0 = a purely stationary swing. + _reg_f("Bear", "bear_lunge_speed", 7.0, 0.0, 16.0, 0.5) + # Playback multiplier for the swipe/smash clips — >1 makes the punches snappier. + _reg_f("Bear", "bear_attack_anim_speed", 1.35, 0.5, 3.0, 0.05) + # Half-arc (deg) the bear must be facing the bull within for a directional swing to + # connect; slams use their own radius instead of an arc. + _reg_f("Bear", "bear_hit_arc", 70.0, 5.0, 180.0, 5.0) + # Double Swipe (ONE_TWO_HIT) — bread-and-butter close combo, two quick hits. + _reg_f("Bear", "bear_swipe_windup", 0.3, 0.05, 2.0, 0.05) + _reg_f("Bear", "bear_swipe_recover", 0.35, 0.05, 3.0, 0.05) + _reg_f("Bear", "bear_swipe_reach", 4.2, 1.0, 8.0, 0.1) + # Overhead Smash (SMASH_HIT) — slower tell, biggest punish window, AoE on impact. + _reg_f("Bear", "bear_smash_windup", 0.6, 0.1, 3.0, 0.05) + _reg_f("Bear", "bear_smash_recover", 0.75, 0.1, 4.0, 0.05) + _reg_f("Bear", "bear_smash_radius", 4.2, 1.0, 8.0, 0.1) + # Leap Slam (JUMP_SMASH, 47 frames @30fps) — timed to the clip by FRAME: it plays the + # crouch grounded, springs at the takeoff frame, and the arc is solved so the paws hit + # ground exactly on the impact frame (25), leaving the tail frames to settle on the + # ground — no finishing the anim in mid-air. The arc uses its own gravity so peak height + # and airtime are independent: bear_leap_height is the peak reached in that fixed airtime. + _reg_f("Bear", "bear_leap_takeoff_frame", 6.0, 0.0, 46.0, 1.0) + _reg_f("Bear", "bear_leap_impact_frame", 25.0, 1.0, 47.0, 1.0) + _reg_f("Bear", "bear_leap_height", 2.6, 0.5, 8.0, 0.1) + _reg_f("Bear", "bear_leap_recover", 0.35, 0.0, 4.0, 0.05) + _reg_f("Bear", "bear_leap_speed", 14.0, 4.0, 30.0, 0.5) + _reg_f("Bear", "bear_slam_radius", 4.0, 1.0, 8.0, 0.1) + # Stalk (WALK_ON2LEGS) — how long it will keep walking the bull down before re-deciding. + _reg_f("Bear", "bear_stalk_time", 2.5, 0.0, 6.0, 0.1) + # Odds a close combo chains a follow-up smash after the swipe (phase 2 always chains). + _reg_f("Bear", "bear_combo_chance", 0.7, 0.0, 1.0, 0.05) + # Minimum bull speed (ram) that counts as a hit on the bear — a gentle nudge is + # shrugged off, only a real charge or an ability wounds it. (The bear itself never + # damages the bull by colliding — only its swings and slams hurt.) + _reg_f("Bear", "bear_hit_threshold", 6.0, 1.0, 30.0, 0.5) + # How long (s) the bear is rocked back and can't act after taking a hit. + _reg_f("Bear", "bear_stagger_time", 0.5, 0.0, 2.0, 0.05) + # Poise: after a stagger the bear shrugs off staggers for this long (hits still wound + # it), so it can't be perma-stunlocked — it gets a window to commit an attack that then + # rides its hyper-armour. 0 = no poise (stun-locks under sustained fire). + _reg_f("Bear", "bear_stagger_cd", 1.0, 0.0, 4.0, 0.05) # ── Sword ───────────────────────────────────────────────────────────────── # Local seating of the sword in the right hand (drawn / fighting). # Defaults are identity — the bone is oriented in Blender to seat the sword @@ -352,11 +416,40 @@ func get_all() -> Dictionary: func save() -> void: - var cfg := ConfigFile.new() + var path := "res://debug_params.gd" + var file := FileAccess.open(path, FileAccess.READ) + if file == null: + push_error("DP.save: cannot open " + path) + return + var src := file.get_as_text() + file.close() + for key: String in _params: var p: Dictionary = _params[key] - cfg.set_value(p["section"], key, p["value"]) - cfg.save(SAVE_PATH) + if p["value"] == p["default"]: + continue + var re := RegEx.new() + if p["type"] == TYPE_FLOAT: + re.compile('(_reg_f\\("[^"]+",\\s*"%s",\\s*)([-\\d.e]+)' % key) + src = re.sub(src, "$1" + _fmt_float(p["value"] as float)) + else: + re.compile('(_reg_b\\("[^"]+",\\s*"%s",\\s*)(true|false)' % key) + src = re.sub(src, "$1" + ("true" if bool(p["value"]) else "false")) + p["default"] = p["value"] + + var wfile := FileAccess.open(path, FileAccess.WRITE) + if wfile == null: + push_error("DP.save: cannot write " + path) + return + wfile.store_string(src) + wfile.close() + + +func _fmt_float(v: float) -> String: + var s := "%.6g" % v + if not ("." in s or "e" in s): + s += ".0" + return s func load_saved() -> void: diff --git a/hud.gd b/hud.gd index 2c5d5a9..e280fa9 100644 --- a/hud.gd +++ b/hud.gd @@ -243,7 +243,7 @@ func _continue_to_map() -> void: get_tree().paused = false Input.mouse_mode = Input.MOUSE_MODE_VISIBLE Run.complete_current_level() - get_tree().change_scene_to_file("res://MapScreen.tscn") + get_tree().change_scene_to_file("res://levels/MapScreen.tscn") func _toggle_controls() -> void: diff --git a/level_def.gd b/level_def.gd deleted file mode 100644 index 5531416..0000000 --- a/level_def.gd +++ /dev/null @@ -1,16 +0,0 @@ -class_name LevelDef -extends Resource -## Definition of one playable level on the run map — the arena today, later a cave with -## a bear, a china shop to smash, and so on. Every map node references a LevelDef, and -## the map/flow code never hard-codes a scene: adding a level is a new LevelDef (plus its -## scene) handed out by the map generator in run_state.gd. - -@export var id: StringName = &"" -@export var display_name: String = "Level" -## Scene loaded when the player enters a node with this level. Every node points at the -## arena for now. -@export_file("*.tscn") var scene_path: String = "" -## Placeholder pip tint on the map until per-level icon art exists. -@export var color: Color = Color(0.80, 0.62, 0.24) -## A boss level caps a run — its node is drawn larger and clearing it ends the map. -@export var is_boss: bool = false diff --git a/MapScreen.tscn b/levels/MapScreen.tscn similarity index 72% rename from MapScreen.tscn rename to levels/MapScreen.tscn index 8c0c388..6c29183 100644 --- a/MapScreen.tscn +++ b/levels/MapScreen.tscn @@ -1,6 +1,6 @@ [gd_scene format=3 uid="uid://map_screen_v1"] -[ext_resource type="Script" path="res://map_screen.gd" id="1_map"] +[ext_resource type="Script" path="res://levels/map_screen.gd" id="1_map"] [node name="MapScreen" type="Control"] layout_mode = 3 diff --git a/scene.tscn307250851.tmp b/levels/arena_level.tscn similarity index 57% rename from scene.tscn307250851.tmp rename to levels/arena_level.tscn index 89480aa..79ba155 100644 --- a/scene.tscn307250851.tmp +++ b/levels/arena_level.tscn @@ -1,369 +1,360 @@ -[gd_scene format=3 uid="uid://b7afbx36ltb3f"] +[gd_scene format=3 uid="uid://co4tgvdpdte4v"] -[ext_resource type="PackedScene" uid="uid://crtm0xtiql3r8" path="res://Player.tscn" id="1_ulcgi"] [ext_resource type="PackedScene" uid="uid://dio67r8ambe7h" path="res://Assets/arena_placeholder_v01.glb" id="2_nxogm"] -[ext_resource type="Script" uid="uid://8b1qil754yd3" path="res://matador_spawn.gd" id="3_spawn"] -[ext_resource type="Shader" uid="uid://d3xyyy5bh0ma" path="res://ps1_filter.gdshader" id="4_ps1shader"] +[ext_resource type="PackedScene" uid="uid://cl54dt38yj2x" path="res://Assets/Arena.glb" id="3_fy5k1"] +[ext_resource type="Script" uid="uid://dou5ar6tkmng1" path="res://levels/matador_spawn.gd" id="3_spawn"] [ext_resource type="PackedScene" uid="uid://cmxqocsdbgufr" path="res://Animations/mees_plaksutab.glb" id="5_m7jmp"] [ext_resource type="PackedScene" uid="uid://1y07gn65ktsi" path="res://Animations/ANIM_publik_mees.glb" id="7_xlvrw"] -[ext_resource type="Script" uid="uid://dr6phggarm73e" path="res://Animations/anim_publik_mees.gd" id="8_73fnb"] -[ext_resource type="Script" uid="uid://di0jafakftp3n" path="res://publikum_controller.gd" id="9_pubctrl"] -[ext_resource type="Script" uid="uid://xu6osmd62rj2" path="res://hud.gd" id="10_hud"] - -[sub_resource type="CameraAttributesPractical" id="CameraAttributesPractical_73fnb"] -dof_blur_far_enabled = true -dof_blur_far_distance = 8192.0 -dof_blur_far_transition = 8192.0 -dof_blur_near_enabled = true -dof_blur_near_distance = 8192.0 -dof_blur_near_transition = 8192.0 -dof_blur_amount = 1.0 +[ext_resource type="Script" uid="uid://wj6tgh8nb0ka" path="res://Animations/anim_publik_mees.gd" id="8_73fnb"] +[ext_resource type="Script" uid="uid://bs682ey3p1ivj" path="res://publikum_controller.gd" id="9_pubctrl"] +[ext_resource type="PackedScene" uid="uid://dxe72ig8gfbcg" path="res://Assets/arena model2026.glb" id="10_5juve"] +[ext_resource type="Script" uid="uid://cj0ytlkuavmf4" path="res://crowd_billboards.gd" id="11_crowd"] [sub_resource type="BoxShape3D" id="BoxShape3D_ulcgi"] -[sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_b1ung"] -data = PackedVector3Array(10.55265, 0.9051099, -2.4514918, 10.55265, -0.9414401, 0.016268253, 10.55265, -0.9414401, -2.4514918, 10.55265, 0.9051099, -2.4514918, 10.55265, 0.9051099, 0.016268253, 10.55265, -0.9414401, 0.016268253, 10.55265, -0.9414401, -2.4514918, 10.23201, -2.75993, 0.016268253, 10.23201, -2.75993, -2.4514918, 10.55265, -0.9414401, -2.4514918, 10.55265, -0.9414401, 0.016268253, 10.23201, -2.75993, 0.016268253, 10.23201, -2.75993, -2.4514918, 9.60045, -4.49512, 0.016268253, 9.60045, -4.49512, -2.4514918, 10.23201, -2.75993, -2.4514918, 10.23201, -2.75993, 0.016268253, 9.60045, -4.49512, 0.016268253, 9.60045, -4.49512, -2.4514918, 8.67718, -6.09428, 0.016268253, 8.67718, -6.09428, -2.4514918, 9.60045, -4.49512, -2.4514918, 9.60045, -4.49512, 0.016268253, 8.67718, -6.09428, 0.016268253, 8.67718, -6.09428, -2.4514918, 7.49024, -7.50881, 0.016268253, 7.49024, -7.50881, -2.4514918, 8.67718, -6.09428, -2.4514918, 8.67718, -6.09428, 0.016268253, 7.49024, -7.50881, 0.016268253, 7.49024, -7.50881, -2.4514918, 6.0757, -8.69575, 0.016268253, 6.0757, -8.69575, -2.4514918, 7.49024, -7.50881, -2.4514918, 7.49024, -7.50881, 0.016268253, 6.0757, -8.69575, 0.016268253, 6.0757, -8.69575, -2.4514918, 4.47655, -9.61902, 0.016268253, 4.47655, -9.61902, -2.4514918, 6.0757, -8.69575, -2.4514918, 6.0757, -8.69575, 0.016268253, 4.47655, -9.61902, 0.016268253, 4.47655, -9.61902, -2.4514918, 2.74136, -10.25058, 0.016268253, 2.74136, -10.25058, -2.4514918, 4.47655, -9.61902, -2.4514918, 4.47655, -9.61902, 0.016268253, 2.74136, -10.25058, 0.016268253, 2.74136, -10.25058, -2.4514918, 0.92287, -10.57123, 0.016268253, 0.92287, -10.57123, -2.4514918, 2.74136, -10.25058, -2.4514918, 2.74136, -10.25058, 0.016268253, 0.92287, -10.57123, 0.016268253, 0.92287, -10.57123, -2.4514918, -0.92368, -10.57123, 0.016268253, -0.92368, -10.57123, -2.4514918, 0.92287, -10.57123, -2.4514918, 0.92287, -10.57123, 0.016268253, -0.92368, -10.57123, 0.016268253, -0.92368, -10.57123, -2.4514918, -2.74218, -10.25058, 0.016268253, -2.74218, -10.25058, -2.4514918, -0.92368, -10.57123, -2.4514918, -0.92368, -10.57123, 0.016268253, -2.74218, -10.25058, 0.016268253, -2.74218, -10.25058, -2.4514918, -4.47736, -9.61902, 0.016268253, -4.47736, -9.61902, -2.4514918, -2.74218, -10.25058, -2.4514918, -2.74218, -10.25058, 0.016268253, -4.47736, -9.61902, 0.016268253, -4.47736, -9.61902, -2.4514918, -6.07652, -8.69575, 0.016268253, -6.07652, -8.69575, -2.4514918, -4.47736, -9.61902, -2.4514918, -4.47736, -9.61902, 0.016268253, -6.07652, -8.69575, 0.016268253, -6.07652, -8.69575, -2.4514918, -7.49106, -7.50881, 0.016268253, -7.49106, -7.50881, -2.4514918, -6.07652, -8.69575, -2.4514918, -6.07652, -8.69575, 0.016268253, -7.49106, -7.50881, 0.016268253, -7.49106, -7.50881, -2.4514918, -8.67799, -6.09427, 0.016268253, -8.67799, -6.09427, -2.4514918, -7.49106, -7.50881, -2.4514918, -7.49106, -7.50881, 0.016268253, -8.67799, -6.09427, 0.016268253, -8.67799, -6.09427, -2.4514918, -9.60127, -4.49512, 0.016268253, -9.60127, -4.49512, -2.4514918, -8.67799, -6.09427, -2.4514918, -8.67799, -6.09427, 0.016268253, -9.60127, -4.49512, 0.016268253, -9.60127, -4.49512, -2.4514918, -10.23282, -2.75993, 0.016268253, -10.23282, -2.75993, -2.4514918, -9.60127, -4.49512, -2.4514918, -9.60127, -4.49512, 0.016268253, -10.23282, -2.75993, 0.016268253, -10.23282, -2.75993, -2.4514918, -10.55347, -0.9414401, 0.016268253, -10.55347, -0.9414401, -2.4514918, -10.23282, -2.75993, -2.4514918, -10.23282, -2.75993, 0.016268253, -10.55347, -0.9414401, 0.016268253, -10.55347, -0.9414401, -2.4514918, -10.55347, 0.9051099, 0.016268253, -10.55347, 0.9051099, -2.4514918, -10.55347, -0.9414401, -2.4514918, -10.55347, -0.9414401, 0.016268253, -10.55347, 0.9051099, 0.016268253, -10.55347, 0.9051099, -2.4514918, -10.23282, 2.7236, 0.016268253, -10.23282, 2.7236, -2.4514918, -10.55347, 0.9051099, -2.4514918, -10.55347, 0.9051099, 0.016268253, -10.23282, 2.7236, 0.016268253, -10.23282, 2.7236, -2.4514918, -9.60127, 4.45879, 0.016268253, -9.60127, 4.45879, -2.4514918, -10.23282, 2.7236, -2.4514918, -10.23282, 2.7236, 0.016268253, -9.60127, 4.45879, 0.016268253, -9.60127, 4.45879, -2.4514918, -8.67799, 6.05794, 0.016268253, -8.67799, 6.05794, -2.4514918, -9.60127, 4.45879, -2.4514918, -9.60127, 4.45879, 0.016268253, -8.67799, 6.05794, 0.016268253, -8.67799, 6.05794, -2.4514918, -7.49106, 7.47248, 0.016268253, -7.49106, 7.47248, -2.4514918, -8.67799, 6.05794, -2.4514918, -8.67799, 6.05794, 0.016268253, -7.49106, 7.47248, 0.016268253, -7.49106, 7.47248, -2.4514918, -6.07652, 8.65942, 0.016268253, -6.07652, 8.65942, -2.4514918, -7.49106, 7.47248, -2.4514918, -7.49106, 7.47248, 0.016268253, -6.07652, 8.65942, 0.016268253, -6.07652, 8.65942, -2.4514918, -4.47736, 9.58269, 0.016268253, -4.47736, 9.58269, -2.4514918, -6.07652, 8.65942, -2.4514918, -6.07652, 8.65942, 0.016268253, -4.47736, 9.58269, 0.016268253, -4.47736, 9.58269, -2.4514918, -2.74217, 10.21425, 0.016268253, -2.74217, 10.21425, -2.4514918, -4.47736, 9.58269, -2.4514918, -4.47736, 9.58269, 0.016268253, -2.74217, 10.21425, 0.016268253, -2.74217, 10.21425, -2.4514918, -0.92368, 10.5349, 0.016268253, -0.92368, 10.5349, -2.4514918, -2.74217, 10.21425, -2.4514918, -2.74217, 10.21425, 0.016268253, -0.92368, 10.5349, 0.016268253, -0.92368, 10.5349, -2.4514918, 0.92287, 10.5349, 0.016268253, 0.92287, 10.5349, -2.4514918, -0.92368, 10.5349, -2.4514918, -0.92368, 10.5349, 0.016268253, 0.92287, 10.5349, 0.016268253, 0.92287, 10.5349, -2.4514918, 2.74136, 10.21425, 0.016268253, 2.74136, 10.21425, -2.4514918, 0.92287, 10.5349, -2.4514918, 0.92287, 10.5349, 0.016268253, 2.74136, 10.21425, 0.016268253, 2.74136, 10.21425, -2.4514918, 4.47655, 9.58269, 0.016268253, 4.47655, 9.58269, -2.4514918, 2.74136, 10.21425, -2.4514918, 2.74136, 10.21425, 0.016268253, 4.47655, 9.58269, 0.016268253, 4.47655, 9.58269, -2.4514918, 6.0757, 8.65942, 0.016268253, 6.0757, 8.65942, -2.4514918, 4.47655, 9.58269, -2.4514918, 4.47655, 9.58269, 0.016268253, 6.0757, 8.65942, 0.016268253, 6.0757, 8.65942, -2.4514918, 7.49024, 7.47248, 0.016268253, 7.49024, 7.47248, -2.4514918, 6.0757, 8.65942, -2.4514918, 6.0757, 8.65942, 0.016268253, 7.49024, 7.47248, 0.016268253, 7.49024, 7.47248, -2.4514918, 8.67718, 6.05794, 0.016268253, 8.67718, 6.05794, -2.4514918, 7.49024, 7.47248, -2.4514918, 7.49024, 7.47248, 0.016268253, 8.67718, 6.05794, 0.016268253, 8.67718, 6.05794, -2.4514918, 9.60045, 4.45879, 0.016268253, 9.60045, 4.45879, -2.4514918, 8.67718, 6.05794, -2.4514918, 8.67718, 6.05794, 0.016268253, 9.60045, 4.45879, 0.016268253, 9.60045, 4.45879, -2.4514918, 10.23201, 2.7236, 0.016268253, 10.23201, 2.7236, -2.4514918, 9.60045, 4.45879, -2.4514918, 9.60045, 4.45879, 0.016268253, 10.23201, 2.7236, 0.016268253, 10.23201, 2.7236, -2.4514918, 10.55265, 0.9051099, 0.016268253, 10.55265, 0.9051099, -2.4514918, 10.23201, 2.7236, -2.4514918, 10.23201, 2.7236, 0.016268253, 10.55265, 0.9051099, 0.016268253) - -[sub_resource type="ShaderMaterial" id="ShaderMaterial_ps1"] -shader = ExtResource("4_ps1shader") -shader_parameter/pixel_scale = 4.0 -shader_parameter/color_depth = 5.0 -shader_parameter/dither_strength = 0.8 -shader_parameter/scanline_strength = 0.2 -shader_parameter/roll_speed = 3.0 -shader_parameter/roll_strength = 1.0 - [sub_resource type="Environment" id="Environment_73fnb"] -[node name="scene" type="Node3D" unique_id=22320312] +[node name="arena_level" type="Node3D" unique_id=701327120] -[node name="Player" parent="." unique_id=165510044 instance=ExtResource("1_ulcgi")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5.1711025, 0) +[node name="Node3D" type="Node3D" parent="." unique_id=1772505910] -[node name="Camera3D" parent="Player" index="3" unique_id=1120161087] -attributes = SubResource("CameraAttributesPractical_73fnb") -fov = 50.0 -near = 2.636 -far = 0.01 +[node name="StaticBody3D" type="StaticBody3D" parent="Node3D" unique_id=2083665760] -[node name="Node3D" type="Node3D" parent="." unique_id=1081100876] - -[node name="StaticBody3D" type="StaticBody3D" parent="Node3D" unique_id=2072095624] - -[node name="CollisionShape3D" type="CollisionShape3D" parent="Node3D/StaticBody3D" unique_id=1444198204] +[node name="CollisionShape3D" type="CollisionShape3D" parent="Node3D/StaticBody3D" unique_id=1028681009] transform = Transform3D(30, 0, 0, 0, 1, 0, 0, 0, 30, 0, -0.5, 0) shape = SubResource("BoxShape3D_ulcgi") -[node name="arena_placeholder_v01" parent="Node3D" unique_id=1341659233 instance=ExtResource("2_nxogm")] +[node name="arena_placeholder_v01" parent="Node3D" unique_id=173047470 instance=ExtResource("2_nxogm")] -[node name="CollisionShape3D2" type="CollisionShape3D" parent="Node3D/arena_placeholder_v01/Wall/StaticBody3D" parent_id_path=PackedInt32Array(1341659233, 1793167121) index="0" unique_id=1881819927] -transform = Transform3D(0.9999999, 0, 0, 0, 0.9999999, 0, 0, 0, 0.9999999, -1.3237867, -1.2018226, -7.163421e-08) -shape = SubResource("ConcavePolygonShape3D_b1ung") - -[node name="MatadorSpawner" type="Node3D" parent="." unique_id=-1293967295] -script = ExtResource("3_spawn") - -[node name="HUD" type="CanvasLayer" parent="." unique_id=1100000001] -script = ExtResource("10_hud") - -[node name="CanvasLayer" type="CanvasLayer" parent="." unique_id=1032421313] -layer = 10 +[node name="Arena_draft" parent="Node3D/arena_placeholder_v01" index="0" unique_id=1422517759] visible = false -[node name="PS1Filter" type="ColorRect" parent="CanvasLayer" unique_id=1449116873] -material = SubResource("ShaderMaterial_ps1") -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 +[node name="Wall" parent="Node3D/arena_placeholder_v01" index="1" unique_id=1497936357] +visible = false -[node name="Lights" type="Node3D" parent="." unique_id=737767662] +[node name="CollisionShape3D" parent="Node3D/arena_placeholder_v01/Wall/StaticBody3D" parent_id_path=PackedInt32Array(173047470, 773519804) index="0" unique_id=580729261] +transform = Transform3D(0.98309773, 0, 0, 0, 0.9558868, 0, 0, 0, 4.061706, 0.13596812, -0.091219574, -5.4371103e-09) -[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Lights" unique_id=818712731] -transform = Transform3D(0.0153599195, 0.5682554, -0.82270896, 0.16280845, 0.810404, 0.5627957, 0.9865381, -0.1425885, -0.08006904, -4.08134, 0.3070748, 0.97087634) +[node name="Arena2" parent="Node3D/arena_placeholder_v01" unique_id=1270130903 instance=ExtResource("3_fy5k1")] + +[node name="arena model2026" parent="Node3D/arena_placeholder_v01" unique_id=1744180217 instance=ExtResource("10_5juve")] +transform = Transform3D(0.99999994, 0, 0, 0, 0.99999994, 0, 0, 0, 0.99999994, -0.901, -0.6, 0) +visible = false + +[node name="MatadorSpawner" type="Node3D" parent="." unique_id=1524912967] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3, 14) +script = ExtResource("3_spawn") + +[node name="MatadorSpawner2" type="Node3D" parent="." unique_id=2081003535] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 3, 0) +script = ExtResource("3_spawn") + +[node name="MatadorSpawner4" type="Node3D" parent="." unique_id=497566616] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 14, 3, 0) +script = ExtResource("3_spawn") + +[node name="MatadorSpawner3" type="Node3D" parent="." unique_id=618446747] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3, -15) +script = ExtResource("3_spawn") + +[node name="Lights" type="Node3D" parent="." unique_id=614769858] + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Lights" unique_id=960453715] +transform = Transform3D(0.01535992, 0.56825536, -0.82270896, 0.16280846, 0.8104039, 0.56279576, 0.9865382, -0.14258848, -0.08006904, -4.08134, 9.197985, 0.97087634) +light_energy = 0.4 light_indirect_energy = 0.767 shadow_blur = 0.47 -[node name="SpotLight3D2" type="SpotLight3D" parent="Lights" unique_id=918477294] -transform = Transform3D(0.904133, 0.30962405, -0.2944087, 0.3084627, 0.0037425563, 0.9512291, 0.2956252, -0.95085174, -0.0921237, -5.954, 14.477, -4.756) -visible = false -light_energy = 13.695 -shadow_enabled = true +[node name="SpotLight3D2" type="SpotLight3D" parent="Lights" unique_id=1654524086] +transform = Transform3D(0.904133, 0.30962402, -0.2944087, 0.3084627, 0.0037425472, 0.9512291, 0.2956252, -0.95085174, -0.0921237, -3.629381, 10.86347, -2.0124264) +light_energy = 16.0 +light_indirect_energy = 26.095 distance_fade_enabled = true spot_range = 32.90586 +spot_angle = 73.93 -[node name="SpotLight3D" type="SpotLight3D" parent="Lights" unique_id=422808558] +[node name="SpotLight3D" type="SpotLight3D" parent="Lights" unique_id=1408554387] transform = Transform3D(0.9763736, -9.445564e-09, 0.21608931, -0.20963383, 0.24260214, 0.9472053, -0.052423734, -0.9701259, 0.23687032, 4.0400305, 13.463301, 3.9410677) visible = false light_energy = 13.695 shadow_enabled = true spot_range = 32.90586 -[node name="PublikNode" type="Node3D" parent="." unique_id=1020007445] +[node name="SUN_DirectionalLight3D2" type="DirectionalLight3D" parent="Lights" unique_id=1840798568] +transform = Transform3D(0.45811793, 0.34843865, -0.81775206, 0.5316611, 0.62986195, 0.56622505, 0.7123655, -0.69416475, 0.10329977, 0, 8.980043, 0) +light_energy = 0.106 +shadow_enabled = true +shadow_blur = 10.0 + +[node name="PublikNode" type="Node3D" parent="." unique_id=537560774] visible = false -[node name="publikum2" type="Node3D" parent="PublikNode" unique_id=813507078] +[node name="publikum2" type="Node3D" parent="PublikNode" unique_id=632248377] transform = Transform3D(-0.91522944, 0, -0.4029331, 0, 1, 0, 0.4029331, 0, -0.91522944, 4.195425, 3.6553268, 13.885913) +visible = false script = ExtResource("9_pubctrl") -[node name="mees_plaksutab" parent="PublikNode/publikum2" unique_id=1052467743 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab" parent="PublikNode/publikum2" unique_id=1523494533 instance=ExtResource("5_m7jmp")] -[node name="mees_plaksutab2" parent="PublikNode/publikum2" unique_id=1015461330 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab2" parent="PublikNode/publikum2" unique_id=1136452990 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) -[node name="mees_plaksutab3" parent="PublikNode/publikum2" unique_id=1089134459 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab3" parent="PublikNode/publikum2" unique_id=1034056333 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) -[node name="mees_plaksutab4" parent="PublikNode/publikum2" unique_id=322028795 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab4" parent="PublikNode/publikum2" unique_id=1301549876 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) -[node name="mees_plaksutab5" parent="PublikNode/publikum2" unique_id=482889095 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab5" parent="PublikNode/publikum2" unique_id=426787640 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) -[node name="mees_plaksutab6" parent="PublikNode/publikum2" unique_id=944445851 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab6" parent="PublikNode/publikum2" unique_id=264608279 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) -[node name="publikum4" type="Node3D" parent="PublikNode" unique_id=68365083] +[node name="publikum4" type="Node3D" parent="PublikNode" unique_id=627563732] transform = Transform3D(-0.629974, 0, -0.77661633, 0, 1, 0, 0.77661633, 0, -0.629974, 9.842437, 3.6553268, 10.401334) +visible = false script = ExtResource("9_pubctrl") -[node name="mees_plaksutab" parent="PublikNode/publikum4" unique_id=222379180 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab" parent="PublikNode/publikum4" unique_id=1220293959 instance=ExtResource("5_m7jmp")] -[node name="mees_plaksutab2" parent="PublikNode/publikum4" unique_id=1871765797 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab2" parent="PublikNode/publikum4" unique_id=1450591086 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) -[node name="mees_plaksutab3" parent="PublikNode/publikum4" unique_id=10437017 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab3" parent="PublikNode/publikum4" unique_id=1560510378 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) -[node name="mees_plaksutab4" parent="PublikNode/publikum4" unique_id=901414927 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab4" parent="PublikNode/publikum4" unique_id=37533204 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) -[node name="mees_plaksutab5" parent="PublikNode/publikum4" unique_id=2077358822 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab5" parent="PublikNode/publikum4" unique_id=1261306009 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) -[node name="mees_plaksutab6" parent="PublikNode/publikum4" unique_id=1131238522 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab6" parent="PublikNode/publikum4" unique_id=1900385491 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) -[node name="publikum5" type="Node3D" parent="PublikNode" unique_id=1254884314] +[node name="publikum5" type="Node3D" parent="PublikNode" unique_id=1812640124] transform = Transform3D(-0.39064395, 0, 0.920542, 0, 1, 0, -0.920542, 0, -0.39064395, -14.538336, 3.6553268, 3.9378712) script = ExtResource("9_pubctrl") -[node name="mees_plaksutab" parent="PublikNode/publikum5" unique_id=1914156014 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab" parent="PublikNode/publikum5" unique_id=83635667 instance=ExtResource("5_m7jmp")] -[node name="mees_plaksutab2" parent="PublikNode/publikum5" unique_id=602140062 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab2" parent="PublikNode/publikum5" unique_id=1143183809 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) -[node name="mees_plaksutab3" parent="PublikNode/publikum5" unique_id=224404749 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab3" parent="PublikNode/publikum5" unique_id=1283965822 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) -[node name="mees_plaksutab4" parent="PublikNode/publikum5" unique_id=958300982 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab4" parent="PublikNode/publikum5" unique_id=1691614127 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) -[node name="mees_plaksutab5" parent="PublikNode/publikum5" unique_id=860025476 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab5" parent="PublikNode/publikum5" unique_id=1236404634 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) -[node name="mees_plaksutab6" parent="PublikNode/publikum5" unique_id=1052988416 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab6" parent="PublikNode/publikum5" unique_id=606457826 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) -[node name="publikum6" type="Node3D" parent="PublikNode" unique_id=709827517] +[node name="publikum6" type="Node3D" parent="PublikNode" unique_id=629778401] transform = Transform3D(-0.7681126, 0, 0.64031476, 0, 1, 0, -0.64031476, 0, -0.7681126, -11.103292, 3.6553288, 9.335619) script = ExtResource("9_pubctrl") -[node name="mees_plaksutab" parent="PublikNode/publikum6" unique_id=1007939702 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab" parent="PublikNode/publikum6" unique_id=1894579943 instance=ExtResource("5_m7jmp")] -[node name="mees_plaksutab2" parent="PublikNode/publikum6" unique_id=860884329 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab2" parent="PublikNode/publikum6" unique_id=1361331032 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) -[node name="mees_plaksutab3" parent="PublikNode/publikum6" unique_id=1928974579 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab3" parent="PublikNode/publikum6" unique_id=1108954377 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) -[node name="mees_plaksutab4" parent="PublikNode/publikum6" unique_id=547889733 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab4" parent="PublikNode/publikum6" unique_id=300775025 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) -[node name="mees_plaksutab5" parent="PublikNode/publikum6" unique_id=1501877017 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab5" parent="PublikNode/publikum6" unique_id=1733379910 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) -[node name="mees_plaksutab6" parent="PublikNode/publikum6" unique_id=1951764566 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab6" parent="PublikNode/publikum6" unique_id=903641089 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) -[node name="publikum7" type="Node3D" parent="PublikNode" unique_id=1483978907] +[node name="publikum7" type="Node3D" parent="PublikNode" unique_id=1108195583] transform = Transform3D(0.55061686, 0, -0.8347584, 0, 1, 0, 0.8347584, 0, 0.55061686, 13.163043, 3.4379854, -5.268345) script = ExtResource("9_pubctrl") -[node name="mees_plaksutab" parent="PublikNode/publikum7" unique_id=788603784 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab" parent="PublikNode/publikum7" unique_id=365348429 instance=ExtResource("5_m7jmp")] -[node name="mees_plaksutab2" parent="PublikNode/publikum7" unique_id=1537286096 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab2" parent="PublikNode/publikum7" unique_id=1699361442 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) -[node name="mees_plaksutab3" parent="PublikNode/publikum7" unique_id=1957279061 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab3" parent="PublikNode/publikum7" unique_id=934639622 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) -[node name="mees_plaksutab4" parent="PublikNode/publikum7" unique_id=1379894897 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab4" parent="PublikNode/publikum7" unique_id=1585045587 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) -[node name="mees_plaksutab5" parent="PublikNode/publikum7" unique_id=1920281402 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab5" parent="PublikNode/publikum7" unique_id=501748248 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) -[node name="mees_plaksutab6" parent="PublikNode/publikum7" unique_id=811906038 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab6" parent="PublikNode/publikum7" unique_id=92474890 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) -[node name="publikum8" type="Node3D" parent="PublikNode" unique_id=1073953916] +[node name="publikum8" type="Node3D" parent="PublikNode" unique_id=1138908471] transform = Transform3D(0.85247177, 0, -0.5227735, 0, 1, 0, 0.5227735, 0, 0.85247177, 11.217102, 4.568224, -10.151715) script = ExtResource("9_pubctrl") -[node name="mees_plaksutab" parent="PublikNode/publikum8" unique_id=641607406 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab" parent="PublikNode/publikum8" unique_id=2110447734 instance=ExtResource("5_m7jmp")] -[node name="mees_plaksutab2" parent="PublikNode/publikum8" unique_id=740439664 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab2" parent="PublikNode/publikum8" unique_id=1304761336 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) -[node name="mees_plaksutab3" parent="PublikNode/publikum8" unique_id=1896496635 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab3" parent="PublikNode/publikum8" unique_id=1725684741 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) -[node name="mees_plaksutab4" parent="PublikNode/publikum8" unique_id=1787601370 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab4" parent="PublikNode/publikum8" unique_id=2002512340 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) -[node name="mees_plaksutab5" parent="PublikNode/publikum8" unique_id=1153730967 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab5" parent="PublikNode/publikum8" unique_id=1527416664 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) -[node name="mees_plaksutab6" parent="PublikNode/publikum8" unique_id=1023874478 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab6" parent="PublikNode/publikum8" unique_id=1248985994 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) -[node name="publikum3" type="Node3D" parent="PublikNode" unique_id=71842083] +[node name="publikum3" type="Node3D" parent="PublikNode" unique_id=1978237178] transform = Transform3D(0.8698311, 0, 0.49334967, 0, 1, 0, -0.49334967, 0, 0.8698311, -4.8910522, 4.627474, -14.224113) script = ExtResource("9_pubctrl") -[node name="mees_plaksutab" parent="PublikNode/publikum3" unique_id=1585155222 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab" parent="PublikNode/publikum3" unique_id=878298260 instance=ExtResource("5_m7jmp")] -[node name="mees_plaksutab2" parent="PublikNode/publikum3" unique_id=1301960990 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab2" parent="PublikNode/publikum3" unique_id=1331430063 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) -[node name="mees_plaksutab3" parent="PublikNode/publikum3" unique_id=1481094020 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab3" parent="PublikNode/publikum3" unique_id=263054039 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) -[node name="mees_plaksutab4" parent="PublikNode/publikum3" unique_id=1958614289 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab4" parent="PublikNode/publikum3" unique_id=374864807 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) -[node name="mees_plaksutab5" parent="PublikNode/publikum3" unique_id=318927325 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab5" parent="PublikNode/publikum3" unique_id=674678014 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) -[node name="mees_plaksutab6" parent="PublikNode/publikum3" unique_id=492002897 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab6" parent="PublikNode/publikum3" unique_id=2055934328 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) -[node name="publikum9" type="Node3D" parent="PublikNode" unique_id=1844757155] +[node name="publikum9" type="Node3D" parent="PublikNode" unique_id=1005200820] transform = Transform3D(0.9760592, 0, 0.21750516, 0, 1, 0, -0.21750516, 0, 0.9760592, -2.1318102, 3.6553268, -13.557504) script = ExtResource("9_pubctrl") -[node name="mees_plaksutab" parent="PublikNode/publikum9" unique_id=896469763 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab" parent="PublikNode/publikum9" unique_id=1436817061 instance=ExtResource("5_m7jmp")] -[node name="mees_plaksutab2" parent="PublikNode/publikum9" unique_id=1412552760 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab2" parent="PublikNode/publikum9" unique_id=375947457 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) -[node name="mees_plaksutab3" parent="PublikNode/publikum9" unique_id=59611810 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab3" parent="PublikNode/publikum9" unique_id=71021140 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) -[node name="mees_plaksutab4" parent="PublikNode/publikum9" unique_id=1161683450 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab4" parent="PublikNode/publikum9" unique_id=2106738663 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) -[node name="mees_plaksutab5" parent="PublikNode/publikum9" unique_id=1706050589 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab5" parent="PublikNode/publikum9" unique_id=283876866 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) -[node name="mees_plaksutab6" parent="PublikNode/publikum9" unique_id=1929239115 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab6" parent="PublikNode/publikum9" unique_id=1033622841 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) -[node name="publikum11" type="Node3D" parent="PublikNode" unique_id=727404993] +[node name="publikum11" type="Node3D" parent="PublikNode" unique_id=407707263] transform = Transform3D(-0.95107377, 0, -0.30896416, 0, 1, 0, 0.30896416, 0, -0.95107377, 2.4321594, 3.6553268, 14.119543) +visible = false script = ExtResource("9_pubctrl") -[node name="mees_plaksutab" parent="PublikNode/publikum11" unique_id=36716174 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab" parent="PublikNode/publikum11" unique_id=1140359395 instance=ExtResource("5_m7jmp")] -[node name="mees_plaksutab2" parent="PublikNode/publikum11" unique_id=2117684869 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab2" parent="PublikNode/publikum11" unique_id=5897282 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) -[node name="mees_plaksutab3" parent="PublikNode/publikum11" unique_id=684167492 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab3" parent="PublikNode/publikum11" unique_id=528539077 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) -[node name="mees_plaksutab4" parent="PublikNode/publikum11" unique_id=678520535 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab4" parent="PublikNode/publikum11" unique_id=693156815 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) -[node name="mees_plaksutab5" parent="PublikNode/publikum11" unique_id=1746700688 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab5" parent="PublikNode/publikum11" unique_id=1138785564 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) -[node name="mees_plaksutab6" parent="PublikNode/publikum11" unique_id=462492236 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab6" parent="PublikNode/publikum11" unique_id=576184958 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) -[node name="publikum13" type="Node3D" parent="PublikNode" unique_id=1816403209] +[node name="publikum13" type="Node3D" parent="PublikNode" unique_id=332757108] transform = Transform3D(-0.2962035, 0, 0.95512486, 0, 1, 0, -0.95512486, 0, -0.2962035, -14.037817, 3.6553268, 2.0647745) script = ExtResource("9_pubctrl") -[node name="mees_plaksutab" parent="PublikNode/publikum13" unique_id=1252057421 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab" parent="PublikNode/publikum13" unique_id=214289805 instance=ExtResource("5_m7jmp")] -[node name="mees_plaksutab2" parent="PublikNode/publikum13" unique_id=1096867731 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab2" parent="PublikNode/publikum13" unique_id=1147139958 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3.1134007, 0, 0.6326933) -[node name="mees_plaksutab3" parent="PublikNode/publikum13" unique_id=1130631283 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab3" parent="PublikNode/publikum13" unique_id=662075088 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) -[node name="mees_plaksutab4" parent="PublikNode/publikum13" unique_id=2008334523 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab4" parent="PublikNode/publikum13" unique_id=664162740 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) -[node name="mees_plaksutab5" parent="PublikNode/publikum13" unique_id=1185367656 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab5" parent="PublikNode/publikum13" unique_id=1967822989 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) -[node name="mees_plaksutab6" parent="PublikNode/publikum13" unique_id=649140591 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab6" parent="PublikNode/publikum13" unique_id=1108301136 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) -[node name="publikum15" type="Node3D" parent="PublikNode" unique_id=926602558] +[node name="publikum15" type="Node3D" parent="PublikNode" unique_id=485853358] transform = Transform3D(0.43092924, 0, -0.90238583, 0, 1, 0, 0.90238583, 0, 0.43092924, 14.766085, 4.568224, -2.378694) script = ExtResource("9_pubctrl") -[node name="mees_plaksutab" parent="PublikNode/publikum15" unique_id=2043094085 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab" parent="PublikNode/publikum15" unique_id=525464827 instance=ExtResource("5_m7jmp")] -[node name="mees_plaksutab2" parent="PublikNode/publikum15" unique_id=1927429095 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab2" parent="PublikNode/publikum15" unique_id=852905531 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) -[node name="mees_plaksutab3" parent="PublikNode/publikum15" unique_id=1660696957 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab3" parent="PublikNode/publikum15" unique_id=1785117697 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) -[node name="mees_plaksutab4" parent="PublikNode/publikum15" unique_id=1684596154 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab4" parent="PublikNode/publikum15" unique_id=1380611321 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) -[node name="mees_plaksutab5" parent="PublikNode/publikum15" unique_id=1401913487 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab5" parent="PublikNode/publikum15" unique_id=322138980 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) -[node name="mees_plaksutab6" parent="PublikNode/publikum15" unique_id=1893327164 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab6" parent="PublikNode/publikum15" unique_id=1360012012 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) -[node name="ANIM_publik_mees" parent="PublikNode" unique_id=413106028 instance=ExtResource("7_xlvrw")] +[node name="ANIM_publik_mees" parent="PublikNode" unique_id=941250087 instance=ExtResource("7_xlvrw")] transform = Transform3D(-0.9976194, 0, 0.0689602, 0, 1, 0, -0.0689602, 0, -0.9976194, -2.9310942, 4.1751385, 14.861324) script = ExtResource("8_73fnb") -[node name="publikum" type="Node3D" parent="PublikNode" unique_id=1311444475] +[node name="publikum" type="Node3D" parent="PublikNode" unique_id=1122462483] transform = Transform3D(0.70069104, 0, 0.7134648, 0, 1, 0, -0.7134648, 0, 0.70069104, -9.04294, 3.6553268, -10.339719) script = ExtResource("9_pubctrl") -[node name="mees_plaksutab" parent="PublikNode/publikum" unique_id=1787142704 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab" parent="PublikNode/publikum" unique_id=1481053606 instance=ExtResource("5_m7jmp")] -[node name="mees_plaksutab2" parent="PublikNode/publikum" unique_id=330098781 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab2" parent="PublikNode/publikum" unique_id=984780880 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) -[node name="mees_plaksutab3" parent="PublikNode/publikum" unique_id=1406686735 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab3" parent="PublikNode/publikum" unique_id=1137984224 instance=ExtResource("5_m7jmp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) -[node name="mees_plaksutab4" parent="PublikNode/publikum" unique_id=132494522 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab4" parent="PublikNode/publikum" unique_id=1462909335 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) -[node name="mees_plaksutab5" parent="PublikNode/publikum" unique_id=2101925215 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab5" parent="PublikNode/publikum" unique_id=227073959 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) -[node name="mees_plaksutab6" parent="PublikNode/publikum" unique_id=403179328 instance=ExtResource("5_m7jmp")] +[node name="mees_plaksutab6" parent="PublikNode/publikum" unique_id=1680469468 instance=ExtResource("5_m7jmp")] transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) -[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=755853425] +[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1750284504] environment = SubResource("Environment_73fnb") -[editable path="Player"] -[editable path="Player/bull_v10"] +[node name="CrowdBillboards" type="MultiMeshInstance3D" parent="." unique_id=106730614] +visible = false +script = ExtResource("11_crowd") + [editable path="Node3D/arena_placeholder_v01"] [editable path="PublikNode/publikum2/mees_plaksutab"] [editable path="PublikNode/publikum2/mees_plaksutab2"] diff --git a/levels/bear_level.tscn b/levels/bear_level.tscn new file mode 100644 index 0000000..c28c310 --- /dev/null +++ b/levels/bear_level.tscn @@ -0,0 +1,49 @@ +[gd_scene format=3 uid="uid://dfxigg3cr6nl5"] + +[ext_resource type="PackedScene" uid="uid://bm6rmt3xprhjs" path="res://Assets/bear_level_arena.glb" id="1_beararena"] +[ext_resource type="Script" path="res://levels/bear_spawn.gd" id="2_spawn"] +[ext_resource type="PackedScene" path="res://Bear.tscn" id="3_bear"] +[ext_resource type="Script" path="res://levels/bear_hud.gd" id="4_bearhud"] + +[sub_resource type="BoxShape3D" id="BoxShape3D_floor"] + +[node name="bear_level" type="Node3D" unique_id=261730746] + +[node name="Node3D" type="Node3D" parent="." unique_id=333447532] + +[node name="StaticBody3D" type="StaticBody3D" parent="Node3D" unique_id=1466540248] + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Node3D/StaticBody3D" unique_id=575454169] +transform = Transform3D(30, 0, 0, 0, 1, 0, 0, 0, 30, 0, -0.5, 0) +shape = SubResource("BoxShape3D_floor") + +[node name="bear_level_arena" parent="." unique_id=1529242945 instance=ExtResource("1_beararena")] +transform = Transform3D(2.875, 0, 0, 0, 2.875, 0, 0, 0, 2.875, 0, 0, 0) + +[node name="floor" parent="bear_level_arena/floor" parent_id_path=PackedInt32Array(1529242945, 41795043) index="0" unique_id=1653720920] +visible = false + +[node name="wall" parent="bear_level_arena/floor" parent_id_path=PackedInt32Array(1529242945, 41795043) index="1" unique_id=2041884474] +visible = false + +[node name="BearSpawner" type="Node3D" parent="." unique_id=47931969] +script = ExtResource("2_spawn") +enemy_scene = ExtResource("3_bear") + +[node name="BearHud" type="CanvasLayer" parent="." unique_id=102938471] +script = ExtResource("4_bearhud") + +[node name="Lights" type="Node3D" parent="." unique_id=937122836] + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Lights" unique_id=1786336124] +transform = Transform3D(1, 0, 0, 0, 0.7071067, 0.70710677, 0, -0.70710677, 0.7071067, 0, 36.945946, 70) +light_color = Color(1, 0.75782466, 0.7332154, 1) +light_energy = 1.796 +shadow_enabled = true + +[node name="DirectionalLight3D2" type="DirectionalLight3D" parent="Lights" unique_id=1308313006] +transform = Transform3D(1, 0, 0, 0, -0.8660254, 0.49999994, 0, -0.49999994, -0.8660254, 0, 112.56815, 93.37274) +light_color = Color(0, 0.33367383, 0.57971, 1) +light_energy = 0.616 + +[editable path="bear_level_arena"] diff --git a/levels/game_shell.gd b/levels/game_shell.gd new file mode 100644 index 0000000..893695e --- /dev/null +++ b/levels/game_shell.gd @@ -0,0 +1,26 @@ +extends Node3D +## The reusable game shell: player, camera, HUD and the PS1 filter — everything that +## outlives a single level. On load it instances the active level's module (geometry, +## spawners, lighting, colliders) into LevelRoot. Switching levels frees the whole +## previous module first, so no old mesh or collider can survive into the next fight. + +@onready var _level_root: Node3D = $LevelRoot + + +func _ready() -> void: + Run.ensure_run() + load_active_level() + + +## Tear down the current level module and build the run's active one. Freeing the whole +## subtree (not hiding it) is the point: a level's colliders and spawners cease to exist +## before the next level's are created. +func load_active_level() -> void: + for child: Node in _level_root.get_children(): + _level_root.remove_child(child) + child.queue_free() + var level := Run.active_level() + if level == null or level.level_scene == null: + push_warning("game_shell: active level has no level_scene") + return + _level_root.add_child(level.level_scene.instantiate()) diff --git a/levels/game_shell.gd.uid b/levels/game_shell.gd.uid new file mode 100644 index 0000000..2c08a46 --- /dev/null +++ b/levels/game_shell.gd.uid @@ -0,0 +1 @@ +uid://ptsfs3wq5bje diff --git a/levels/level_def.gd b/levels/level_def.gd new file mode 100644 index 0000000..da7262f --- /dev/null +++ b/levels/level_def.gd @@ -0,0 +1,26 @@ +class_name LevelDef +extends Resource +## Definition of one playable level on the run map — the arena today, later a cave with +## a bear, a china shop to smash, and so on. Every map node references a LevelDef, and +## the map/flow code never hard-codes a scene: adding a level is a new LevelDef (plus its +## scene) handed out by the map generator in run_state.gd. + +@export var id: StringName = &"" +@export var display_name: String = "Level" +## The shell scene loaded when the player enters a node with this level — the reusable +## frame (player, camera, HUD, PS1 filter). Shared by every level; the level's own +## geometry rides in `level_scene`, which the shell instances at runtime. +@export_file("*.tscn") var scene_path: String = "" +## The level module instanced into the shell's LevelRoot: spawners, lighting, meshes, +## colliders and any level-specific subscripts. Swapped wholesale between levels so no +## previous level's geometry (or its colliders) can leak into the next. +@export var level_scene: PackedScene = null +## Placeholder pip tint on the map until per-level icon art exists. +@export var color: Color = Color(0.80, 0.62, 0.24) +## A boss level caps a run — its node is drawn larger and clearing it ends the map. +@export var is_boss: bool = false +## The enemy this level pits the bull against. Null → the usual matador wave (the +## spawner's default). Set → a single boss enemy of this scene is spawned instead (e.g. +## the bear), and the fight is won when it falls. The arena scene is shared either way; +## only the opponent changes. +@export var enemy_scene: PackedScene = null diff --git a/level_def.gd.uid b/levels/level_def.gd.uid similarity index 100% rename from level_def.gd.uid rename to levels/level_def.gd.uid diff --git a/map_node.gd b/levels/map_node.gd similarity index 100% rename from map_node.gd rename to levels/map_node.gd diff --git a/map_node.gd.uid b/levels/map_node.gd.uid similarity index 100% rename from map_node.gd.uid rename to levels/map_node.gd.uid diff --git a/map_screen.gd b/levels/map_screen.gd similarity index 100% rename from map_screen.gd rename to levels/map_screen.gd diff --git a/map_screen.gd.uid b/levels/map_screen.gd.uid similarity index 100% rename from map_screen.gd.uid rename to levels/map_screen.gd.uid diff --git a/levels/matador_spawn.gd b/levels/matador_spawn.gd new file mode 100644 index 0000000..bd6d7b6 --- /dev/null +++ b/levels/matador_spawn.gd @@ -0,0 +1,76 @@ +extends Node3D + +signal matador_killed +signal all_defeated + +const MATADOR := preload("res://Matador.tscn") + +var _alive: int = 0 +var _wave: int = 0 +var _resolved: bool = false +var _spawn_points: Array[Node3D] = [] + + +func _ready() -> void: + add_to_group(&"matador_spawn") + _spawn.call_deferred() + + +# Each spawner node is a spawn-point marker. The first in tree order (the one the HUD +# connects its all_defeated to) runs the wave logic and uses every marker — itself and +# its siblings — as an entry point; the rest stay passive so waves aren't run four times. +func _spawn() -> void: + var markers := get_tree().get_nodes_in_group(&"matador_spawn") + if markers.is_empty() or markers[0] != self: + return + for node: Node in markers: + if node is Node3D: + _spawn_points.append(node as Node3D) + _place_bull(randf() * TAU, DP.f("arena_spawn_radius")) + _start_next_wave() + + +# Wave 1 sends 1 matador, wave 2 sends 2, … up to the number of placed spawn points. +# Matadors are assigned to a random subset of spawn points each wave and enter staggered. +func _start_next_wave() -> void: + _wave += 1 + var count := mini(_wave, _spawn_points.size()) + var points: Array = _spawn_points.duplicate() + points.shuffle() + for i: int in count: + var sp := points[i] as Node3D + var m := _spawn_at(sp.global_position) + m.call_deferred(&"enter_spawn_roll", Vector3.ZERO) + if i < count - 1: + await get_tree().create_timer(DP.f("mat_spawn_stagger")).timeout + + +func _place_bull(angle: float, arena_r: float) -> void: + var bull := get_tree().get_first_node_in_group(&"player") as Node3D + if not bull: + return + bull.global_position = Vector3( + cos(angle) * arena_r, bull.global_position.y, sin(angle) * arena_r + ) + + +func _spawn_at(pos: Vector3) -> Node3D: + var m: Node3D = MATADOR.instantiate() + m.position = pos + get_parent().add_child(m) + m.killed.connect(_on_matador_killed) + _alive += 1 + return m + + +func _on_matador_killed() -> void: + matador_killed.emit() + _alive -= 1 + if _alive > 0 or _resolved: + return + if _wave >= _spawn_points.size(): + _resolved = true + all_defeated.emit() + else: + await get_tree().create_timer(DP.f("mat_wave_pause")).timeout + _start_next_wave() diff --git a/matador_spawn.gd.uid b/levels/matador_spawn.gd.uid similarity index 100% rename from matador_spawn.gd.uid rename to levels/matador_spawn.gd.uid diff --git a/run_state.gd b/levels/run_state.gd similarity index 71% rename from run_state.gd rename to levels/run_state.gd index 1e9b373..d8d21c6 100644 --- a/run_state.gd +++ b/levels/run_state.gd @@ -10,7 +10,11 @@ extends Node signal run_started -const _ARENA_SCENE := "res://scene.tscn" +# Every level loads the same reusable shell (player, camera, HUD, PS1 filter); the shell +# then instances the level's own `level_scene` module into its LevelRoot. +const _SHELL_SCENE := "res://scene.tscn" +const _ARENA_LEVEL := preload("res://levels/arena_level.tscn") +const _BEAR_LEVEL := preload("res://levels/bear_level.tscn") # Map shape. Small + placeholder for now; the final row is a single boss node. const _ROWS := 5 @@ -26,32 +30,68 @@ var player_col: int = -1 ## Node picked on the map whose level is being played now; committed to on the next win. var pending_row: int = -1 var pending_col: int = -1 +## Debug override — when set, active_level() returns this regardless of map state. +## Set via the console `level` command; cleared on start_new_run(). +var debug_level_override: LevelDef = null + +# Level catalog. Every level shares the shell scene; what changes is the level module +# (its geometry + spawners) and the opponent. The arena (matador wave, normal + boss +# flavours) and the bear (a lone boss enemy that can turn up as a normal node) live here. +# Future levels — china shop — slot in the same way: a new module + a LevelDef entry. +const _BEAR_SCENE := preload("res://Bear.tscn") -# Level catalog. Only the arena exists today (a normal and a boss flavour, both loading -# the same scene). Future levels — cave/bear, china shop — slot in here. var _arena: LevelDef var _arena_boss: LevelDef +var _bear: LevelDef func _ready() -> void: - _arena = _make_level(&"arena", "The Arena", _ARENA_SCENE, Color(0.80, 0.62, 0.24), false) + _arena = _make_level(&"arena", "The Arena", _ARENA_LEVEL, Color(0.80, 0.62, 0.24), false) _arena_boss = _make_level( - &"arena_boss", "Grand Arena", _ARENA_SCENE, Color(0.88, 0.30, 0.20), true + &"arena_boss", "Grand Arena", _ARENA_LEVEL, Color(0.88, 0.30, 0.20), true ) + _bear = _make_level(&"bear", "Bear's Den", _BEAR_LEVEL, Color(0.55, 0.38, 0.22), false) + _bear.enemy_scene = _BEAR_SCENE func _make_level( - id: StringName, level_name: String, scene: String, color: Color, boss: bool + id: StringName, level_name: String, level_scene: PackedScene, color: Color, boss: bool ) -> LevelDef: var l := LevelDef.new() l.id = id l.display_name = level_name - l.scene_path = scene + l.scene_path = _SHELL_SCENE + l.level_scene = level_scene l.color = color l.is_boss = boss return l +## The level being fought right now: debug override wins, then the pending/current map +## node, falling back to the plain arena for the intro fight or a direct scene launch. +func active_level() -> LevelDef: + if debug_level_override != null: + return debug_level_override + var n := node_at(pending_row, pending_col) + if n == null: + n = current_node() + return n.level if n != null and n.level != null else _arena + + +## Return a level by id, or null if unknown. Used by the debug console `level` command. +func get_level(id: StringName) -> LevelDef: + match id: + &"arena": return _arena + &"arena_boss": return _arena_boss + &"bear": return _bear + return null + + +## All known level ids — shown by `level` in the console. +func level_ids() -> Array[StringName]: + return [&"arena", &"arena_boss", &"bear"] + + ## Begin a fresh run: build a new map and place the player at the start gate. Called from ## the main-menu Play button, not from the arena scene (which reloads for every fight). func start_new_run() -> void: @@ -60,6 +100,7 @@ func start_new_run() -> void: player_col = -1 pending_row = -1 pending_col = -1 + debug_level_override = null run_started.emit() @@ -149,7 +190,12 @@ func _generate_map() -> void: node.row = r node.col = c node.pos = Vector2(_column_x(c, count), y) - node.level = _arena_boss if r == _ROWS - 1 else _arena + # Boss row is always the Grand Arena; normal nodes are a mix of matador + # arenas and the occasional Bear's Den for variety. + if r == _ROWS - 1: + node.level = _arena_boss + else: + node.level = _bear if randf() < 0.35 else _arena row_nodes.append(node) map.append(row_nodes) _link_rows() diff --git a/run_state.gd.uid b/levels/run_state.gd.uid similarity index 100% rename from run_state.gd.uid rename to levels/run_state.gd.uid diff --git a/main_menu.gd b/main_menu.gd index f9a7b22..3837be4 100644 --- a/main_menu.gd +++ b/main_menu.gd @@ -190,13 +190,74 @@ func _process(_delta: float) -> void: play_button.anchor_bottom = cape.y -# Player-facing option toggles, seated above the rebind list in the Options panel: -# the master screen-shake switch and the gore toggle (both persisted via Settings). +# Player-facing option toggles and audio sliders, seated above the rebind list. func _build_options_toggles() -> void: - var scroll_idx := options_scroll.get_index() - # Insert in reverse so both land directly above the rebind scroll, in listed order. - _add_toggle_row("Blood & Gore", Settings.gore, Settings.set_gore, scroll_idx) - _add_toggle_row("Screen Shake", Settings.screen_shake, Settings.set_screen_shake, scroll_idx) + var idx := options_scroll.get_index() + _add_section_label("── Audio ──", idx); idx += 1 + _add_slider_row("Master Volume", Settings.volume_master, Settings.set_volume_master, idx); idx += 1 + _add_slider_row("Ambient Volume", Settings.volume_ambient, Settings.set_volume_ambient, idx); idx += 1 + _add_slider_row("Effects Volume", Settings.volume_effects, Settings.set_volume_effects, idx); idx += 1 + _add_toggle_row("Mute All", Settings.mute_all, Settings.set_mute_all, idx); idx += 1 + _add_section_label("── Gameplay ──", idx); idx += 1 + _add_toggle_row("Screen Shake", Settings.screen_shake, Settings.set_screen_shake, idx); idx += 1 + _add_toggle_row("Blood & Gore", Settings.gore, Settings.set_gore, idx); idx += 1 + _add_section_label("── Controls ──", idx) + + +func _add_section_label(text: String, at_index: int) -> void: + var lbl := Label.new() + lbl.text = text + lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL + lbl.add_theme_font_override("font", UiFonts.title()) + lbl.add_theme_font_size_override("font_size", 18) + lbl.add_theme_color_override("font_color", MENU_GOLD) + lbl.add_theme_color_override("font_outline_color", Color.BLACK) + lbl.add_theme_constant_override("outline_size", 4) + options_vbox.add_child(lbl) + options_vbox.move_child(lbl, at_index) + + +func _add_slider_row(label_text: String, value: float, setter: Callable, at_index: int) -> void: + var row := HBoxContainer.new() + row.size_flags_horizontal = Control.SIZE_EXPAND_FILL + + var name_label := Label.new() + name_label.text = label_text + name_label.custom_minimum_size = Vector2(160, 0) + name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + name_label.add_theme_font_override("font", UiFonts.body()) + name_label.add_theme_font_size_override("font_size", 20) + name_label.add_theme_color_override("font_color", MENU_CREAM) + name_label.add_theme_color_override("font_outline_color", Color.BLACK) + name_label.add_theme_constant_override("outline_size", 5) + row.add_child(name_label) + + var slider := HSlider.new() + slider.min_value = 0.0 + slider.max_value = 1.0 + slider.step = 0.01 + slider.value = value + slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL + slider.custom_minimum_size = Vector2(0, 28) + row.add_child(slider) + + var pct_label := Label.new() + pct_label.text = "%d%%" % roundi(value * 100.0) + pct_label.custom_minimum_size = Vector2(46, 0) + pct_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + pct_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + pct_label.add_theme_font_override("font", UiFonts.body()) + pct_label.add_theme_font_size_override("font_size", 18) + pct_label.add_theme_color_override("font_color", MENU_GOLD) + row.add_child(pct_label) + + slider.value_changed.connect(func(v: float) -> void: + setter.call(v) + pct_label.text = "%d%%" % roundi(v * 100.0)) + + options_vbox.add_child(row) + options_vbox.move_child(row, at_index) func _add_toggle_row(label_text: String, value: bool, setter: Callable, at_index: int) -> void: diff --git a/matador.gd b/matador.gd index 6245a79..c8d5a69 100644 --- a/matador.gd +++ b/matador.gd @@ -35,6 +35,7 @@ var _swing_timer: float = 0.0 var _lunge_timer: float = 0.0 var _roll_timer: float = 0.0 var _roll_dir: Vector3 = Vector3.ZERO +var _is_spawning: bool = false var _throw_timer: float = 0.0 var _throw_aim_cd: float = 0.0 var _throw_dir: Vector3 = Vector3.ZERO @@ -118,11 +119,13 @@ func _ready() -> void: _ole_player = AudioStreamPlayer.new() _ole_player.stream = load("res://sounds/ole.ogg") _ole_player.volume_db = 0.0 + _ole_player.bus = &"Effects" add_child(_ole_player) if ResourceLoader.exists("res://sounds/matador_death.ogg"): _death_player = AudioStreamPlayer.new() _death_player.stream = load("res://sounds/matador_death.ogg") _death_player.volume_db = 2.0 + _death_player.bus = &"Effects" add_child(_death_player) @@ -143,6 +146,10 @@ func _acquire_bull() -> void: func _physics_process(delta: float) -> void: _acquire_bull() + if _is_spawning and is_on_floor(): + _is_spawning = false + collision_layer = 1 + if DP.b("show_raycasts") and _steer_dir != Vector3.ZERO: var o := global_position + Vector3.UP * 0.6 DebugDraw.ray(o, o + _steer_dir * _STEER_LOOKAHEAD, Color(0.3, 1.0, 0.9)) @@ -564,6 +571,23 @@ func _enter_roll(hit_dir: Vector3) -> void: _anim_player.seek(0.0, true) +# Called by the spawner (deferred) to make the matador leap-roll toward a target +# point on entry — the cinematic arena entrance. After the roll it transitions to +# normal AI via the standard ROLL → FLEE path. +func enter_spawn_roll(toward: Vector3) -> void: + var dir := toward - global_position + dir.y = 0.0 + _roll_dir = dir.normalized() if dir.length() > 0.1 else -_mesh.global_transform.basis.z + _state = State.ROLL + _roll_timer = _anim_length(_ANIM_ROLL, DP.f("mat_roll_duration")) + _is_spawning = true + collision_layer = 0 + velocity.y = 4.0 + if _anim_player and _anim_player.has_animation(_ANIM_ROLL): + _anim_player.play(_ANIM_ROLL) + _anim_player.seek(0.0, true) + + func _tick_roll(delta: float) -> void: _roll_timer -= delta if _roll_timer <= 0.0: diff --git a/matador_spawn.gd b/matador_spawn.gd deleted file mode 100644 index 3361c61..0000000 --- a/matador_spawn.gd +++ /dev/null @@ -1,142 +0,0 @@ -extends Node3D - -signal matador_killed -signal all_defeated - -const MATADOR := preload("res://Matador.tscn") - -var _spawn_count: int = 1 -var _alive: int = 0 -var _kills: int = 0 -var _resolved: bool = false - -# Flat (x,z) positions of the most recent batch spawned. A fresh wave keeps at least -# _SPAWN_MIN_SEP away from these so it never lands on top of where the last one did. -const _SPAWN_MIN_SEP: float = 3.5 -var _last_spawn_positions: Array[Vector3] = [] - - -func _ready() -> void: - add_to_group(&"matador_spawn") - _spawn_count = maxi(1, int(DP.f("mat_spawn_count"))) - # Deferred so it's safe from _ready and matches the previous spawn timing. - _spawn.call_deferred() - - -func _spawn() -> void: - # Bull and matadors start at opposite ends of a random diameter, so they always - # begin as far apart as the arena allows (edge-to-edge across the centre). - var arena_r := DP.f("arena_spawn_radius") - var bull_angle := randf() * TAU - _place_bull(bull_angle, arena_r) - var placed: Array[Vector3] = _spread_positions(_spawn_count, bull_angle + PI, arena_r) - for pos: Vector3 in placed: - _spawn_at(pos) - _last_spawn_positions = placed - - -func _place_bull(angle: float, arena_r: float) -> void: - var bull := get_tree().get_first_node_in_group(&"player") as Node3D - if not bull: - return - bull.global_position = Vector3( - cos(angle) * arena_r, bull.global_position.y, sin(angle) * arena_r - ) - - -# Positions for the matadors, spread across the far side (centred on `center_angle`) -# so every matador stays near the point diametrically opposite the bull. Each slot gets -# its own angular band and a staggered radius, then rejection-samples inside that band so -# no two matadors land closer than _SPAWN_MIN_SEP — otherwise they pile up on top of -# each other on spawn. The arc widens with the crowd so a big wave doesn't get cramped. -func _spread_positions(count: int, center_angle: float, arena_r: float) -> Array[Vector3]: - var positions: Array[Vector3] = [] - if count <= 1: - positions.append(Vector3(cos(center_angle) * arena_r, 1.0, sin(center_angle) * arena_r)) - return positions - var arc := clampf(TAU * 0.33 + float(count) * 0.25, TAU * 0.33, TAU * 0.92) - var slot := arc / float(count) - var inner := maxf(2.5, arena_r * 0.5) - for i: int in count: - # Give each matador its own angular slot for an even fan, but sample the radius - # freely across the whole far-side band so the rejection test can push crowded - # neighbours onto a different ring instead of piling them at one distance. - var base_angle := center_angle - arc * 0.5 + slot * (float(i) + 0.5) - var pos := Vector3(cos(base_angle) * arena_r, 1.0, sin(base_angle) * arena_r) - for attempt: int in 16: - var angle := base_angle + randf_range(-0.5, 0.5) * slot - var radius := randf_range(inner, arena_r) - pos = Vector3(cos(angle) * radius, 1.0, sin(angle) * radius) - if _clear_of(pos, positions, _SPAWN_MIN_SEP): - break - positions.append(pos) - return positions - - -func _spawn_at(pos: Vector3, generation: int = 0) -> void: - var m: Node3D = MATADOR.instantiate() - m.position = pos - m.set_meta(&"generation", generation) - get_parent().add_child(m) - m.killed.connect(_on_matador_killed.bind(m)) - _alive += 1 - - -# A slain matador is replaced by mat_split_factor fresh ones on the spot (a hydra), -# so the horde doubles each generation, up to mat_split_generations deep. The bull -# wins only when a whole generation dies out without splitting — i.e. once the spawns -# stop and the last body drops. -func _on_matador_killed(dead: Node3D) -> void: - matador_killed.emit() - _alive -= 1 - _kills += 1 - _split(dead) - var win_kills := int(DP.f("mat_win_kills")) - var reached_quota := win_kills > 0 and _kills >= win_kills - if (reached_quota or _alive <= 0) and not _resolved: - _resolved = true - all_defeated.emit() - - -func _split(dead: Node3D) -> void: - var count := int(DP.f("mat_split_factor")) - var generation := int(dead.get_meta(&"generation", 0)) - if count < 2 or generation >= int(DP.f("mat_split_generations")): - return - var arena_r := DP.f("arena_spawn_radius") - var origin := dead.global_position - origin.y = 0.0 - var wave: Array[Vector3] = [] - for i: int in count: - var pos := _pick_spawn(origin, arena_r, wave) - wave.append(pos) - _spawn_at(pos, generation + 1) - _last_spawn_positions = wave - - -# Rejection-sample a spawn point ringed around `origin`, pushing the ring outward on -# each failed attempt until it clears both the previous wave (_last_spawn_positions) -# and the spots already taken this wave — so a fresh wave never lands where the last -# one did. Falls back to the last candidate if the arena is too cramped to satisfy it. -func _pick_spawn(origin: Vector3, arena_r: float, taken: Array[Vector3]) -> Vector3: - var pos := origin - for attempt: int in 12: - var angle := randf() * TAU - var r := randf_range(2.0, 3.5) + float(attempt) * 0.4 - pos = origin + Vector3(cos(angle) * r, 0.0, sin(angle) * r) - var flat := Vector2(pos.x, pos.z) - if flat.length() > arena_r: - flat = flat.normalized() * arena_r - pos = Vector3(flat.x, 0.0, flat.y) - if _clear_of(pos, _last_spawn_positions, _SPAWN_MIN_SEP) \ - and _clear_of(pos, taken, _SPAWN_MIN_SEP): - break - pos.y = 1.0 - return pos - - -func _clear_of(pos: Vector3, others: Array[Vector3], min_sep: float) -> bool: - for o: Vector3 in others: - if Vector2(pos.x - o.x, pos.z - o.z).length() < min_sep: - return false - return true diff --git a/player.gd b/player.gd index c535671..d0c0bb4 100644 --- a/player.gd +++ b/player.gd @@ -102,16 +102,16 @@ func _setup_legs() -> void: func _setup_hoof_dust() -> void: - var sphere := _particle_sphere(0.10, 4, 2, _unshaded_material()) + var sphere := _particle_sphere(0.07, 4, 2, _unshaded_material()) _walk_dust_ramp = Gradient.new() - _walk_dust_ramp.set_color(0, Color(0.80, 0.69, 0.46, 1.0)) - _walk_dust_ramp.set_color(1, Color(0.80, 0.69, 0.46, 0.0)) + _walk_dust_ramp.set_color(0, Color(0.94, 0.90, 0.83, 1.0)) + _walk_dust_ramp.set_color(1, Color(0.94, 0.90, 0.83, 0.0)) - # Light blue dust while charging + # White-beige dust while running _charge_dust_ramp = Gradient.new() - _charge_dust_ramp.set_color(0, Color(0.20, 0.40, 0.85, 1.0)) - _charge_dust_ramp.set_color(1, Color(0.20, 0.40, 0.85, 0.0)) + _charge_dust_ramp.set_color(0, Color(0.94, 0.90, 0.83, 1.0)) + _charge_dust_ramp.set_color(1, Color(0.94, 0.90, 0.83, 0.0)) var scale_curve := Curve.new() scale_curve.add_point(Vector2(0.0, 0.2)) @@ -215,6 +215,7 @@ func _setup_audio() -> void: _huff_player = AudioStreamPlayer.new() _huff_player.stream = load("res://sounds/bull_huff.ogg") _huff_player.volume_db = 0.0 + _huff_player.bus = &"Effects" add_child(_huff_player) if ResourceLoader.exists("res://sounds/crowd_ambient.ogg"): _crowd_player = AudioStreamPlayer.new() @@ -224,6 +225,7 @@ func _setup_audio() -> void: _crowd_player.stream = s _crowd_player.volume_db = -14.0 _crowd_player.autoplay = true + _crowd_player.bus = &"Ambient" add_child(_crowd_player) @@ -253,8 +255,8 @@ func _set_dust_state(new_state: DustState) -> void: p.initial_velocity_min = DP.f("charge_vel_min") p.initial_velocity_max = DP.f("charge_vel_max") p.emitting = true - if _charge_trail_emitter: - _charge_trail_emitter.emitting = _dust_state == DustState.CHARGE + # Blue energy trail belongs to the roll/dash ability only, not to fast WASD + # running — driven explicitly in the roll loop / _end_roll, not by dust state. func _on_dp_changed(_key: String, _val: Variant) -> void: @@ -494,6 +496,7 @@ func _tick_roll(delta: float, _steer: Vector3) -> void: _set_dust_state(DustState.CHARGE) if _charge_trail_emitter: + _charge_trail_emitter.emitting = true _charge_trail_emitter.direction = (-_roll_dir + Vector3(0.0, 0.25, 0.0)).normalized() move_and_slide() @@ -575,6 +578,8 @@ func _end_roll() -> void: _legs.set(&"external_rotation", false) if _roll_spark_emitter: _roll_spark_emitter.emitting = false + if _charge_trail_emitter: + _charge_trail_emitter.emitting = false velocity.x *= 0.15 velocity.z *= 0.15 _play_idle() diff --git a/project.godot b/project.godot index 4b194fb..c4dfcb2 100644 --- a/project.godot +++ b/project.godot @@ -17,7 +17,6 @@ config/features=PackedStringArray("4.7", "GL Compatibility") boot_splash/bg_color=Color(0, 0, 0, 1) boot_splash/image="res://Assets/loading_screen.png" config/icon="uid://dct1d88g015el" -boot_splash/minimum_display_time=4000 config/run/main_scene="res://MainMenu.tscn" [autoload] @@ -27,7 +26,7 @@ Console="*res://console.gd" Controls="*res://controls_manager.gd" DebugDraw="*res://debug_draw.gd" Settings="*res://settings.gd" -Run="*res://run_state.gd" +Run="*res://levels/run_state.gd" [display] @@ -94,13 +93,13 @@ ability_kick={ } ability_slam={ "deadzone": 0.2, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":71,"key_label":0,"unicode":103,"location":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":72,"key_label":0,"unicode":104,"location":0,"echo":false,"script":null) , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":true,"script":null) ] } ability_dash={ "deadzone": 0.2, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":72,"key_label":0,"unicode":104,"location":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":71,"key_label":0,"unicode":103,"location":0,"echo":false,"script":null) , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":true,"script":null) ] } diff --git a/run_tests.sh b/run_tests.sh index 98d1e4b..65a073c 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -8,7 +8,7 @@ SKIP_GAMEPLAY=0 for arg in "$@"; do [[ "$arg" == "--no-gameplay" ]] && SKIP_GAMEPLAY=1; done echo "=== gdlint ===" -gdlint *.gd tests/*.gd +gdlint *.gd levels/*.gd tests/*.gd echo "" echo "=== Logic tests ===" @@ -18,6 +18,10 @@ echo "" echo "=== Console focus tests ===" "$GODOT" --headless --script tests/console_focus_test.gd +echo "" +echo "=== Level switch tests (no cross-level geometry/collider leaks) ===" +"$GODOT" --headless --script tests/level_switch_test.gd + echo "" echo "=== Performance tests (frame-budget regression guard) ===" "$GODOT" --headless --script tests/performance_test.gd diff --git a/scene.tscn b/scene.tscn index 2f49b62..f985c92 100644 --- a/scene.tscn +++ b/scene.tscn @@ -1,18 +1,10 @@ -[gd_scene format=3 uid="uid://b7afbx36ltb3f"] +[gd_scene load_steps=8 format=3 uid="uid://b7afbx36ltb3f"] [ext_resource type="PackedScene" uid="uid://crtm0xtiql3r8" path="res://Player.tscn" id="1_ulcgi"] -[ext_resource type="PackedScene" uid="uid://dio67r8ambe7h" path="res://Assets/arena_placeholder_v01.glb" id="2_nxogm"] -[ext_resource type="PackedScene" uid="uid://cl54dt38yj2x" path="res://Assets/Arena.glb" id="3_fy5k1"] -[ext_resource type="Script" uid="uid://dou5ar6tkmng1" path="res://matador_spawn.gd" id="3_spawn"] [ext_resource type="Shader" uid="uid://d3xyyy5bh0ma" path="res://ps1_filter.gdshader" id="4_ps1shader"] -[ext_resource type="PackedScene" uid="uid://cmxqocsdbgufr" path="res://Animations/mees_plaksutab.glb" id="5_m7jmp"] -[ext_resource type="PackedScene" uid="uid://1y07gn65ktsi" path="res://Animations/ANIM_publik_mees.glb" id="7_xlvrw"] -[ext_resource type="Script" uid="uid://dr6phggarm73e" path="res://Animations/anim_publik_mees.gd" id="8_73fnb"] -[ext_resource type="Script" uid="uid://di0jafakftp3n" path="res://publikum_controller.gd" id="9_pubctrl"] -[ext_resource type="PackedScene" uid="uid://dxe72ig8gfbcg" path="res://Assets/arena model2026.glb" id="10_5juve"] [ext_resource type="Script" uid="uid://butd6b3tdwxrv" path="res://hud.gd" id="10_hud"] -[ext_resource type="Script" uid="uid://cj0ytlkuavmf4" path="res://crowd_billboards.gd" id="11_crowd"] [ext_resource type="Script" uid="uid://ce5n4qu88mhyx" path="res://ps1_filter.gd" id="12_ps1ctl"] +[ext_resource type="Script" path="res://levels/game_shell.gd" id="13_shell"] [sub_resource type="CameraAttributesPractical" id="CameraAttributesPractical_73fnb"] dof_blur_far_enabled = true @@ -23,8 +15,6 @@ dof_blur_near_distance = 8192.0 dof_blur_near_transition = 8192.0 dof_blur_amount = 1.0 -[sub_resource type="BoxShape3D" id="BoxShape3D_ulcgi"] - [sub_resource type="ShaderMaterial" id="ShaderMaterial_ps1"] shader = ExtResource("4_ps1shader") shader_parameter/pixel_scale = 4.0 @@ -34,20 +24,19 @@ shader_parameter/scanline_strength = 0.2 shader_parameter/roll_speed = 3.0 shader_parameter/roll_strength = 1.0 -[sub_resource type="Environment" id="Environment_73fnb"] +[node name="scene" type="Node3D"] +script = ExtResource("13_shell") -[node name="scene" type="Node3D" unique_id=22320312] - -[node name="Player" parent="." unique_id=165510044 instance=ExtResource("1_ulcgi")] +[node name="Player" parent="." instance=ExtResource("1_ulcgi")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.2416196, 5.1711025, -4.841382) -[node name="Camera3D" parent="Player" index="3" unique_id=1120161087] +[node name="Camera3D" parent="Player" index="3"] attributes = SubResource("CameraAttributesPractical_73fnb") fov = 50.0 near = 2.636 far = 0.01 -[node name="Skeleton3D" parent="Player/bull/Armature" parent_id_path=PackedInt32Array(165510044, 1386495166, 547304299) index="0" unique_id=2096877109] +[node name="Skeleton3D" parent="Player/bull/Armature" index="0"] bones/0/position = Vector3(0, 0, 0.0074015963) bones/0/rotation = Quaternion(-0.36922705, 0.6030517, -0.6030517, -0.36922705) bones/0/scale = Vector3(1, 1, 1) @@ -97,41 +86,13 @@ bones/24/position = Vector3(7.092306e-11, 0.003274492, 4.6413304e-10) bones/24/rotation = Quaternion(0.52910197, -0.15997829, -0.12024502, 0.82462066) bones/24/scale = Vector3(1, 1.0000001, 1.0000001) -[node name="Node3D" type="Node3D" parent="." unique_id=1081100876] - -[node name="StaticBody3D" type="StaticBody3D" parent="Node3D" unique_id=2072095624] - -[node name="CollisionShape3D" type="CollisionShape3D" parent="Node3D/StaticBody3D" unique_id=1444198204] -transform = Transform3D(30, 0, 0, 0, 1, 0, 0, 0, 30, 0, -0.5, 0) -shape = SubResource("BoxShape3D_ulcgi") - -[node name="arena_placeholder_v01" parent="Node3D" unique_id=1341659233 instance=ExtResource("2_nxogm")] - -[node name="Arena_draft" parent="Node3D/arena_placeholder_v01" index="0" unique_id=603097973] -visible = false - -[node name="Wall" parent="Node3D/arena_placeholder_v01" index="1" unique_id=171701243] -visible = false - -[node name="CollisionShape3D" parent="Node3D/arena_placeholder_v01/Wall/StaticBody3D" parent_id_path=PackedInt32Array(1341659233, 1793167121) index="0" unique_id=404958985] -transform = Transform3D(0.98309773, 0, 0, 0, 0.9558868, 0, 0, 0, 4.061706, 0.13596812, -0.091219574, -5.4371103e-09) - -[node name="Arena2" parent="Node3D/arena_placeholder_v01" unique_id=524718167 instance=ExtResource("3_fy5k1")] - -[node name="arena model2026" parent="Node3D/arena_placeholder_v01" unique_id=179544183 instance=ExtResource("10_5juve")] -transform = Transform3D(0.99999994, 0, 0, 0, 0.99999994, 0, 0, 0, 0.99999994, -0.901, -0.6, 0) -visible = false - -[node name="MatadorSpawner" type="Node3D" parent="." unique_id=-1293967295] -script = ExtResource("3_spawn") - -[node name="HUD" type="CanvasLayer" parent="." unique_id=1100000001] +[node name="HUD" type="CanvasLayer" parent="."] script = ExtResource("10_hud") -[node name="CanvasLayer" type="CanvasLayer" parent="." unique_id=1032421313] +[node name="CanvasLayer" type="CanvasLayer" parent="."] layer = 10 -[node name="PS1Filter" type="ColorRect" parent="CanvasLayer" unique_id=1449116873] +[node name="PS1Filter" type="ColorRect" parent="CanvasLayer"] material = SubResource("ShaderMaterial_ps1") anchors_preset = 15 anchor_right = 1.0 @@ -140,377 +101,7 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("12_ps1ctl") -[node name="Lights" type="Node3D" parent="." unique_id=737767662] - -[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Lights" unique_id=818712731] -transform = Transform3D(0.01535992, 0.56825536, -0.82270896, 0.16280846, 0.8104039, 0.56279576, 0.9865382, -0.14258848, -0.08006904, -4.08134, 9.197985, 0.97087634) -light_energy = 0.4 -light_indirect_energy = 0.767 -shadow_blur = 0.47 - -[node name="SpotLight3D2" type="SpotLight3D" parent="Lights" unique_id=918477294] -transform = Transform3D(0.904133, 0.30962402, -0.2944087, 0.3084627, 0.0037425472, 0.9512291, 0.2956252, -0.95085174, -0.0921237, -3.629381, 10.86347, -2.0124264) -light_energy = 16.0 -light_indirect_energy = 26.095 -distance_fade_enabled = true -spot_range = 32.90586 -spot_angle = 73.93 - -[node name="SpotLight3D" type="SpotLight3D" parent="Lights" unique_id=422808558] -transform = Transform3D(0.9763736, -9.445564e-09, 0.21608931, -0.20963383, 0.24260214, 0.9472053, -0.052423734, -0.9701259, 0.23687032, 4.0400305, 13.463301, 3.9410677) -visible = false -light_energy = 13.695 -shadow_enabled = true -spot_range = 32.90586 - -[node name="SUN_DirectionalLight3D2" type="DirectionalLight3D" parent="Lights" unique_id=1706919950] -transform = Transform3D(0.45811793, 0.34843865, -0.81775206, 0.5316611, 0.62986195, 0.56622505, 0.7123655, -0.69416475, 0.10329977, 0, 8.980043, 0) -light_energy = 0.106 -shadow_enabled = true -shadow_blur = 10.0 - -[node name="PublikNode" type="Node3D" parent="." unique_id=1020007445] -visible = false - -[node name="publikum2" type="Node3D" parent="PublikNode" unique_id=813507078] -transform = Transform3D(-0.91522944, 0, -0.4029331, 0, 1, 0, 0.4029331, 0, -0.91522944, 4.195425, 3.6553268, 13.885913) -visible = false -script = ExtResource("9_pubctrl") - -[node name="mees_plaksutab" parent="PublikNode/publikum2" unique_id=1052467743 instance=ExtResource("5_m7jmp")] - -[node name="mees_plaksutab2" parent="PublikNode/publikum2" unique_id=1015461330 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) - -[node name="mees_plaksutab3" parent="PublikNode/publikum2" unique_id=1089134459 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) - -[node name="mees_plaksutab4" parent="PublikNode/publikum2" unique_id=322028795 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) - -[node name="mees_plaksutab5" parent="PublikNode/publikum2" unique_id=482889095 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) - -[node name="mees_plaksutab6" parent="PublikNode/publikum2" unique_id=944445851 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) - -[node name="publikum4" type="Node3D" parent="PublikNode" unique_id=68365083] -transform = Transform3D(-0.629974, 0, -0.77661633, 0, 1, 0, 0.77661633, 0, -0.629974, 9.842437, 3.6553268, 10.401334) -visible = false -script = ExtResource("9_pubctrl") - -[node name="mees_plaksutab" parent="PublikNode/publikum4" unique_id=222379180 instance=ExtResource("5_m7jmp")] - -[node name="mees_plaksutab2" parent="PublikNode/publikum4" unique_id=1871765797 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) - -[node name="mees_plaksutab3" parent="PublikNode/publikum4" unique_id=10437017 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) - -[node name="mees_plaksutab4" parent="PublikNode/publikum4" unique_id=901414927 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) - -[node name="mees_plaksutab5" parent="PublikNode/publikum4" unique_id=2077358822 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) - -[node name="mees_plaksutab6" parent="PublikNode/publikum4" unique_id=1131238522 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) - -[node name="publikum5" type="Node3D" parent="PublikNode" unique_id=1254884314] -transform = Transform3D(-0.39064395, 0, 0.920542, 0, 1, 0, -0.920542, 0, -0.39064395, -14.538336, 3.6553268, 3.9378712) -script = ExtResource("9_pubctrl") - -[node name="mees_plaksutab" parent="PublikNode/publikum5" unique_id=1914156014 instance=ExtResource("5_m7jmp")] - -[node name="mees_plaksutab2" parent="PublikNode/publikum5" unique_id=602140062 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) - -[node name="mees_plaksutab3" parent="PublikNode/publikum5" unique_id=224404749 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) - -[node name="mees_plaksutab4" parent="PublikNode/publikum5" unique_id=958300982 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) - -[node name="mees_plaksutab5" parent="PublikNode/publikum5" unique_id=860025476 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) - -[node name="mees_plaksutab6" parent="PublikNode/publikum5" unique_id=1052988416 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) - -[node name="publikum6" type="Node3D" parent="PublikNode" unique_id=709827517] -transform = Transform3D(-0.7681126, 0, 0.64031476, 0, 1, 0, -0.64031476, 0, -0.7681126, -11.103292, 3.6553288, 9.335619) -script = ExtResource("9_pubctrl") - -[node name="mees_plaksutab" parent="PublikNode/publikum6" unique_id=1007939702 instance=ExtResource("5_m7jmp")] - -[node name="mees_plaksutab2" parent="PublikNode/publikum6" unique_id=860884329 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) - -[node name="mees_plaksutab3" parent="PublikNode/publikum6" unique_id=1928974579 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) - -[node name="mees_plaksutab4" parent="PublikNode/publikum6" unique_id=547889733 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) - -[node name="mees_plaksutab5" parent="PublikNode/publikum6" unique_id=1501877017 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) - -[node name="mees_plaksutab6" parent="PublikNode/publikum6" unique_id=1951764566 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) - -[node name="publikum7" type="Node3D" parent="PublikNode" unique_id=1483978907] -transform = Transform3D(0.55061686, 0, -0.8347584, 0, 1, 0, 0.8347584, 0, 0.55061686, 13.163043, 3.4379854, -5.268345) -script = ExtResource("9_pubctrl") - -[node name="mees_plaksutab" parent="PublikNode/publikum7" unique_id=788603784 instance=ExtResource("5_m7jmp")] - -[node name="mees_plaksutab2" parent="PublikNode/publikum7" unique_id=1537286096 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) - -[node name="mees_plaksutab3" parent="PublikNode/publikum7" unique_id=1957279061 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) - -[node name="mees_plaksutab4" parent="PublikNode/publikum7" unique_id=1379894897 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) - -[node name="mees_plaksutab5" parent="PublikNode/publikum7" unique_id=1920281402 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) - -[node name="mees_plaksutab6" parent="PublikNode/publikum7" unique_id=811906038 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) - -[node name="publikum8" type="Node3D" parent="PublikNode" unique_id=1073953916] -transform = Transform3D(0.85247177, 0, -0.5227735, 0, 1, 0, 0.5227735, 0, 0.85247177, 11.217102, 4.568224, -10.151715) -script = ExtResource("9_pubctrl") - -[node name="mees_plaksutab" parent="PublikNode/publikum8" unique_id=641607406 instance=ExtResource("5_m7jmp")] - -[node name="mees_plaksutab2" parent="PublikNode/publikum8" unique_id=740439664 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) - -[node name="mees_plaksutab3" parent="PublikNode/publikum8" unique_id=1896496635 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) - -[node name="mees_plaksutab4" parent="PublikNode/publikum8" unique_id=1787601370 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) - -[node name="mees_plaksutab5" parent="PublikNode/publikum8" unique_id=1153730967 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) - -[node name="mees_plaksutab6" parent="PublikNode/publikum8" unique_id=1023874478 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) - -[node name="publikum3" type="Node3D" parent="PublikNode" unique_id=71842083] -transform = Transform3D(0.8698311, 0, 0.49334967, 0, 1, 0, -0.49334967, 0, 0.8698311, -4.8910522, 4.627474, -14.224113) -script = ExtResource("9_pubctrl") - -[node name="mees_plaksutab" parent="PublikNode/publikum3" unique_id=1585155222 instance=ExtResource("5_m7jmp")] - -[node name="mees_plaksutab2" parent="PublikNode/publikum3" unique_id=1301960990 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) - -[node name="mees_plaksutab3" parent="PublikNode/publikum3" unique_id=1481094020 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) - -[node name="mees_plaksutab4" parent="PublikNode/publikum3" unique_id=1958614289 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) - -[node name="mees_plaksutab5" parent="PublikNode/publikum3" unique_id=318927325 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) - -[node name="mees_plaksutab6" parent="PublikNode/publikum3" unique_id=492002897 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) - -[node name="publikum9" type="Node3D" parent="PublikNode" unique_id=1844757155] -transform = Transform3D(0.9760592, 0, 0.21750516, 0, 1, 0, -0.21750516, 0, 0.9760592, -2.1318102, 3.6553268, -13.557504) -script = ExtResource("9_pubctrl") - -[node name="mees_plaksutab" parent="PublikNode/publikum9" unique_id=896469763 instance=ExtResource("5_m7jmp")] - -[node name="mees_plaksutab2" parent="PublikNode/publikum9" unique_id=1412552760 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) - -[node name="mees_plaksutab3" parent="PublikNode/publikum9" unique_id=59611810 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) - -[node name="mees_plaksutab4" parent="PublikNode/publikum9" unique_id=1161683450 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) - -[node name="mees_plaksutab5" parent="PublikNode/publikum9" unique_id=1706050589 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) - -[node name="mees_plaksutab6" parent="PublikNode/publikum9" unique_id=1929239115 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) - -[node name="publikum11" type="Node3D" parent="PublikNode" unique_id=727404993] -transform = Transform3D(-0.95107377, 0, -0.30896416, 0, 1, 0, 0.30896416, 0, -0.95107377, 2.4321594, 3.6553268, 14.119543) -visible = false -script = ExtResource("9_pubctrl") - -[node name="mees_plaksutab" parent="PublikNode/publikum11" unique_id=36716174 instance=ExtResource("5_m7jmp")] - -[node name="mees_plaksutab2" parent="PublikNode/publikum11" unique_id=2117684869 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) - -[node name="mees_plaksutab3" parent="PublikNode/publikum11" unique_id=684167492 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) - -[node name="mees_plaksutab4" parent="PublikNode/publikum11" unique_id=678520535 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) - -[node name="mees_plaksutab5" parent="PublikNode/publikum11" unique_id=1746700688 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) - -[node name="mees_plaksutab6" parent="PublikNode/publikum11" unique_id=462492236 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) - -[node name="publikum13" type="Node3D" parent="PublikNode" unique_id=1816403209] -transform = Transform3D(-0.2962035, 0, 0.95512486, 0, 1, 0, -0.95512486, 0, -0.2962035, -14.037817, 3.6553268, 2.0647745) -script = ExtResource("9_pubctrl") - -[node name="mees_plaksutab" parent="PublikNode/publikum13" unique_id=1252057421 instance=ExtResource("5_m7jmp")] - -[node name="mees_plaksutab2" parent="PublikNode/publikum13" unique_id=1096867731 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3.1134007, 0, 0.6326933) - -[node name="mees_plaksutab3" parent="PublikNode/publikum13" unique_id=1130631283 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) - -[node name="mees_plaksutab4" parent="PublikNode/publikum13" unique_id=2008334523 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) - -[node name="mees_plaksutab5" parent="PublikNode/publikum13" unique_id=1185367656 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) - -[node name="mees_plaksutab6" parent="PublikNode/publikum13" unique_id=649140591 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) - -[node name="publikum15" type="Node3D" parent="PublikNode" unique_id=926602558] -transform = Transform3D(0.43092924, 0, -0.90238583, 0, 1, 0, 0.90238583, 0, 0.43092924, 14.766085, 4.568224, -2.378694) -script = ExtResource("9_pubctrl") - -[node name="mees_plaksutab" parent="PublikNode/publikum15" unique_id=2043094085 instance=ExtResource("5_m7jmp")] - -[node name="mees_plaksutab2" parent="PublikNode/publikum15" unique_id=1927429095 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) - -[node name="mees_plaksutab3" parent="PublikNode/publikum15" unique_id=1660696957 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) - -[node name="mees_plaksutab4" parent="PublikNode/publikum15" unique_id=1684596154 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) - -[node name="mees_plaksutab5" parent="PublikNode/publikum15" unique_id=1401913487 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) - -[node name="mees_plaksutab6" parent="PublikNode/publikum15" unique_id=1893327164 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) - -[node name="ANIM_publik_mees" parent="PublikNode" unique_id=413106028 instance=ExtResource("7_xlvrw")] -transform = Transform3D(-0.9976194, 0, 0.0689602, 0, 1, 0, -0.0689602, 0, -0.9976194, -2.9310942, 4.1751385, 14.861324) -script = ExtResource("8_73fnb") - -[node name="publikum" type="Node3D" parent="PublikNode" unique_id=1311444475] -transform = Transform3D(0.70069104, 0, 0.7134648, 0, 1, 0, -0.7134648, 0, 0.70069104, -9.04294, 3.6553268, -10.339719) -script = ExtResource("9_pubctrl") - -[node name="mees_plaksutab" parent="PublikNode/publikum" unique_id=1787142704 instance=ExtResource("5_m7jmp")] - -[node name="mees_plaksutab2" parent="PublikNode/publikum" unique_id=330098781 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.8139224, 0, -0.33299255) - -[node name="mees_plaksutab3" parent="PublikNode/publikum" unique_id=1406686735 instance=ExtResource("5_m7jmp")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5662975, 0, 0.3316717) - -[node name="mees_plaksutab4" parent="PublikNode/publikum" unique_id=132494522 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -8.003071, 0, 1.5986013) - -[node name="mees_plaksutab5" parent="PublikNode/publikum" unique_id=2101925215 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.89589787, 0, 0.44426048, 0, 1, 0, -0.44426048, 0, 0.89589787, -7.1850557, 0.8316374, 0.36604595) - -[node name="mees_plaksutab6" parent="PublikNode/publikum" unique_id=403179328 instance=ExtResource("5_m7jmp")] -transform = Transform3D(0.97025216, 0, 0.24209678, 0, 1, 0, -0.24209678, 0, 0.97025216, -4.039103, 0.8316374, -0.8892679) - -[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=755853425] -environment = SubResource("Environment_73fnb") - -[node name="CrowdBillboards" type="MultiMeshInstance3D" parent="." unique_id=549548313] -visible = false -script = ExtResource("11_crowd") +[node name="LevelRoot" type="Node3D" parent="."] [editable path="Player"] [editable path="Player/bull"] -[editable path="Node3D/arena_placeholder_v01"] -[editable path="PublikNode/publikum2/mees_plaksutab"] -[editable path="PublikNode/publikum2/mees_plaksutab2"] -[editable path="PublikNode/publikum2/mees_plaksutab3"] -[editable path="PublikNode/publikum2/mees_plaksutab4"] -[editable path="PublikNode/publikum2/mees_plaksutab5"] -[editable path="PublikNode/publikum2/mees_plaksutab6"] -[editable path="PublikNode/publikum4/mees_plaksutab"] -[editable path="PublikNode/publikum4/mees_plaksutab2"] -[editable path="PublikNode/publikum4/mees_plaksutab3"] -[editable path="PublikNode/publikum4/mees_plaksutab4"] -[editable path="PublikNode/publikum4/mees_plaksutab5"] -[editable path="PublikNode/publikum4/mees_plaksutab6"] -[editable path="PublikNode/publikum5/mees_plaksutab"] -[editable path="PublikNode/publikum5/mees_plaksutab2"] -[editable path="PublikNode/publikum5/mees_plaksutab3"] -[editable path="PublikNode/publikum5/mees_plaksutab4"] -[editable path="PublikNode/publikum5/mees_plaksutab5"] -[editable path="PublikNode/publikum5/mees_plaksutab6"] -[editable path="PublikNode/publikum6/mees_plaksutab"] -[editable path="PublikNode/publikum6/mees_plaksutab2"] -[editable path="PublikNode/publikum6/mees_plaksutab3"] -[editable path="PublikNode/publikum6/mees_plaksutab4"] -[editable path="PublikNode/publikum6/mees_plaksutab5"] -[editable path="PublikNode/publikum6/mees_plaksutab6"] -[editable path="PublikNode/publikum7/mees_plaksutab"] -[editable path="PublikNode/publikum7/mees_plaksutab2"] -[editable path="PublikNode/publikum7/mees_plaksutab3"] -[editable path="PublikNode/publikum7/mees_plaksutab4"] -[editable path="PublikNode/publikum7/mees_plaksutab5"] -[editable path="PublikNode/publikum7/mees_plaksutab6"] -[editable path="PublikNode/publikum8/mees_plaksutab"] -[editable path="PublikNode/publikum8/mees_plaksutab2"] -[editable path="PublikNode/publikum8/mees_plaksutab3"] -[editable path="PublikNode/publikum8/mees_plaksutab4"] -[editable path="PublikNode/publikum8/mees_plaksutab5"] -[editable path="PublikNode/publikum8/mees_plaksutab6"] -[editable path="PublikNode/publikum3/mees_plaksutab"] -[editable path="PublikNode/publikum3/mees_plaksutab2"] -[editable path="PublikNode/publikum3/mees_plaksutab3"] -[editable path="PublikNode/publikum3/mees_plaksutab4"] -[editable path="PublikNode/publikum3/mees_plaksutab5"] -[editable path="PublikNode/publikum3/mees_plaksutab6"] -[editable path="PublikNode/publikum9/mees_plaksutab"] -[editable path="PublikNode/publikum9/mees_plaksutab2"] -[editable path="PublikNode/publikum9/mees_plaksutab3"] -[editable path="PublikNode/publikum9/mees_plaksutab4"] -[editable path="PublikNode/publikum9/mees_plaksutab5"] -[editable path="PublikNode/publikum9/mees_plaksutab6"] -[editable path="PublikNode/publikum11/mees_plaksutab"] -[editable path="PublikNode/publikum11/mees_plaksutab2"] -[editable path="PublikNode/publikum11/mees_plaksutab3"] -[editable path="PublikNode/publikum11/mees_plaksutab4"] -[editable path="PublikNode/publikum11/mees_plaksutab5"] -[editable path="PublikNode/publikum11/mees_plaksutab6"] -[editable path="PublikNode/publikum13/mees_plaksutab"] -[editable path="PublikNode/publikum13/mees_plaksutab2"] -[editable path="PublikNode/publikum13/mees_plaksutab3"] -[editable path="PublikNode/publikum13/mees_plaksutab4"] -[editable path="PublikNode/publikum13/mees_plaksutab5"] -[editable path="PublikNode/publikum13/mees_plaksutab6"] -[editable path="PublikNode/publikum15/mees_plaksutab"] -[editable path="PublikNode/publikum15/mees_plaksutab2"] -[editable path="PublikNode/publikum15/mees_plaksutab3"] -[editable path="PublikNode/publikum15/mees_plaksutab4"] -[editable path="PublikNode/publikum15/mees_plaksutab5"] -[editable path="PublikNode/publikum15/mees_plaksutab6"] -[editable path="PublikNode/ANIM_publik_mees"] -[editable path="PublikNode/publikum/mees_plaksutab"] -[editable path="PublikNode/publikum/mees_plaksutab2"] -[editable path="PublikNode/publikum/mees_plaksutab3"] -[editable path="PublikNode/publikum/mees_plaksutab4"] -[editable path="PublikNode/publikum/mees_plaksutab5"] -[editable path="PublikNode/publikum/mees_plaksutab6"] diff --git a/settings.gd b/settings.gd index 59eaecd..abe8aad 100644 --- a/settings.gd +++ b/settings.gd @@ -13,11 +13,39 @@ var screen_shake: bool = true ## Gore toggle — when off, no blood spawns (impact spurt + ongoing spear-wound bleed). var gore: bool = true +## Audio volumes stored as linear 0–1. Applied to AudioServer buses on load/change. +var volume_master: float = 1.0 +var volume_ambient: float = 1.0 +var volume_effects: float = 1.0 +var mute_all: bool = false + func _ready() -> void: + _ensure_buses() load_saved() +## Ensure Ambient and Effects buses exist as children of Master. +func _ensure_buses() -> void: + for bus_name: String in ["Ambient", "Effects"]: + if AudioServer.get_bus_index(bus_name) == -1: + AudioServer.add_bus() + var idx := AudioServer.bus_count - 1 + AudioServer.set_bus_name(idx, bus_name) + AudioServer.set_bus_send(idx, &"Master") + + +func _apply_audio() -> void: + AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Master"), _vol_db(volume_master)) + AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Ambient"), _vol_db(volume_ambient)) + AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Effects"), _vol_db(volume_effects)) + AudioServer.set_bus_mute(AudioServer.get_bus_index("Master"), mute_all) + + +func _vol_db(linear: float) -> float: + return linear_to_db(maxf(linear, 0.0001)) + + func set_screen_shake(value: bool) -> void: if screen_shake == value: return @@ -34,16 +62,56 @@ func set_gore(value: bool) -> void: save() +func set_volume_master(value: float) -> void: + volume_master = clampf(value, 0.0, 1.0) + AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Master"), _vol_db(volume_master)) + changed.emit("volume_master", volume_master) + save() + + +func set_volume_ambient(value: float) -> void: + volume_ambient = clampf(value, 0.0, 1.0) + AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Ambient"), _vol_db(volume_ambient)) + changed.emit("volume_ambient", volume_ambient) + save() + + +func set_volume_effects(value: float) -> void: + volume_effects = clampf(value, 0.0, 1.0) + AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Effects"), _vol_db(volume_effects)) + changed.emit("volume_effects", volume_effects) + save() + + +func set_mute_all(value: bool) -> void: + if mute_all == value: + return + mute_all = value + AudioServer.set_bus_mute(AudioServer.get_bus_index("Master"), mute_all) + changed.emit("mute_all", value) + save() + + func save() -> void: var cfg := ConfigFile.new() - cfg.set_value(SECTION, "screen_shake", screen_shake) - cfg.set_value(SECTION, "gore", gore) + cfg.set_value(SECTION, "screen_shake", screen_shake) + cfg.set_value(SECTION, "gore", gore) + cfg.set_value(SECTION, "volume_master", volume_master) + cfg.set_value(SECTION, "volume_ambient", volume_ambient) + cfg.set_value(SECTION, "volume_effects", volume_effects) + cfg.set_value(SECTION, "mute_all", mute_all) cfg.save(SAVE_PATH) func load_saved() -> void: var cfg := ConfigFile.new() if cfg.load(SAVE_PATH) != OK: + _apply_audio() return - screen_shake = cfg.get_value(SECTION, "screen_shake", true) as bool - gore = cfg.get_value(SECTION, "gore", true) as bool + screen_shake = cfg.get_value(SECTION, "screen_shake", true) as bool + gore = cfg.get_value(SECTION, "gore", true) as bool + volume_master = cfg.get_value(SECTION, "volume_master", 1.0) as float + volume_ambient = cfg.get_value(SECTION, "volume_ambient", 1.0) as float + volume_effects = cfg.get_value(SECTION, "volume_effects", 1.0) as float + mute_all = cfg.get_value(SECTION, "mute_all", false) as bool + _apply_audio() diff --git a/spear_projectile.gd b/spear_projectile.gd index 510ebcf..891ec57 100644 --- a/spear_projectile.gd +++ b/spear_projectile.gd @@ -14,7 +14,12 @@ const _DESPAWN_SEC: float = 8.0 const _STUCK_GROUP: StringName = &"stuck_spear" # Shaft geometry in spear.glb local space: tip at +Z, butt behind the origin. const _TIP_Z: float = 1.915 -const _EMBED_DEPTH: float = 0.45 # how far past the surface the tip buries +const _EMBED_DEPTH: float = 0.72 # how far past the surface the tip buries +# Bone the bull's spears ride on. The collision spheres sit outside the hide, so a +# spear stuck to the body root reads as floating; anchoring to COG also lets it bob +# and rotate with the animated body instead of only the character root. +const _COG_BONE: String = "COG" +const _ANCHOR_NAME: String = "SpearAnchor" var _mesh: Node3D = null var _stopped: bool = false @@ -53,10 +58,12 @@ static func shed_all(bull: Node) -> void: if scene == null: scene = bull var center: Vector3 = (bull as Node3D).global_position - for child in bull.get_children(): - if not child.is_in_group(_STUCK_GROUP): + # Stuck spears now live under a BoneAttachment3D deep in the skeleton, so walk the + # whole subtree by group rather than only the bull's direct children. + for node in bull.get_tree().get_nodes_in_group(_STUCK_GROUP): + if not (bull as Node).is_ancestor_of(node): continue - var mesh := child as Node3D + var mesh := node as Node3D var gx := mesh.global_transform var rb := RigidBody3D.new() rb.collision_layer = 0 @@ -195,7 +202,7 @@ func _embed(other: Object, contact: Vector3, flight: Vector3) -> void: if bull != null and bull.is_in_group(&"player"): bull.call(&"take_sword_hit", "thrown", contact) if is_instance_valid(_mesh) and is_instance_valid(bull): - _mesh.reparent(bull, false) + _mesh.reparent(_spear_anchor(bull), false) _mesh.global_transform = pose _mesh.add_to_group(_STUCK_GROUP) if Settings.gore: @@ -204,3 +211,20 @@ func _embed(other: Object, contact: Vector3, flight: Vector3) -> void: return freeze = true global_transform = pose + + +# Node embedded spears ride on the bull: a BoneAttachment3D pinned to the COG bone so +# they follow the animated body (bob, turn) rather than only the character root. One +# anchor is shared by every spear; falls back to the bull root if there's no skeleton. +static func _spear_anchor(bull: Node) -> Node3D: + var existing := bull.find_child(_ANCHOR_NAME, true, false) + if existing is Node3D: + return existing as Node3D + var skel := bull.find_child("Skeleton3D", true, false) as Skeleton3D + if skel == null: + return bull as Node3D + var ba := BoneAttachment3D.new() + ba.name = _ANCHOR_NAME + skel.add_child(ba) + ba.bone_name = _COG_BONE + return ba diff --git a/tests/bear_boss_test.gd b/tests/bear_boss_test.gd new file mode 100644 index 0000000..ecbf4b6 --- /dev/null +++ b/tests/bear_boss_test.gd @@ -0,0 +1,221 @@ +extends SceneTree +## Bear boss behaviour test. Self-contained: a floor, a real bull (Player.tscn), and the +## bear — no level/shell scene needed. Parks the bull at different range bands and checks +## the bear commits to band-appropriate routines through wind-up→active→recover, that the +## leap lands on the ground (not mid-air), hyper-armour holds mid-swing, and phase 2 + +## death fire. Run: godot --headless --script res://tests/bear_boss_test.gd + +var _pass := 0 +var _fail := 0 +var _bear: Node3D = null +var _bull: Node3D = null + + +func _init() -> void: + _run.call_deferred() + + +func _ok(c: bool, m: String) -> void: + if c: + _pass += 1 + print(" PASS: ", m) + else: + _fail += 1 + print(" FAIL: ", m) + + +func _base(s: String) -> String: + return s.split(":")[0].trim_suffix("*") + + +func _dt() -> float: + return 1.0 / Engine.get_physics_ticks_per_second() + + +# Keep the bull pinned at a flat distance from the bear and alive; report the routine +# bases the bear visits over `secs`. +func _observe(dist: float, secs: float) -> Dictionary: + var seen := {} + var t := 0.0 + while t < secs and is_instance_valid(_bear): + _pin_bull(dist) + await physics_frame + t += _dt() + seen[_base(_bear.call("ai_state_name"))] = true + return seen + + +func _pin_bull(dist: float) -> void: + if _bull == null or not is_instance_valid(_bull): + return + if dist > 0.0: + _bull.global_position = _bear.global_position + Vector3(dist, 0, 0) + _bull.set("_hp", 100000) # never let the bull die and trip a game-over during the test + + +func _make_floor() -> StaticBody3D: + var body := StaticBody3D.new() + var cs := CollisionShape3D.new() + var box := BoxShape3D.new() + box.size = Vector3(400, 1, 400) # oversized so the bear can't chase the bull off an edge + cs.shape = box + cs.position = Vector3(0, -0.5, 0) # top face at y=0 + body.add_child(cs) + return body + + +func _run() -> void: + root.add_child(_make_floor()) + + _bull = (load("res://Player.tscn") as PackedScene).instantiate() + root.add_child(_bull) + _bull.global_position = Vector3(4, 1, 0) + + _bear = (load("res://Bear.tscn") as PackedScene).instantiate() + root.add_child(_bear) + _bear.global_position = Vector3(0, 1, 0) + await physics_frame + await physics_frame + _ok(_bull.is_in_group(&"player"), "bull is in the player group") + + print(" anims: idle=%s run=%s walk2=%s stand=%s swipe=%s smash=%s leap=%s" % [ + _bear.get("_anim_idle"), _bear.get("_anim_run"), _bear.get("_anim_walk2"), + _bear.get("_anim_stand"), _bear.get("_anim_swipe"), _bear.get("_anim_smash"), + _bear.get("_anim_leap")]) + for k in ["_anim_idle", "_anim_run", "_anim_walk2", "_anim_stand", "_anim_swipe", "_anim_smash", "_anim_leap"]: + _ok(String(_bear.get(k)) != "", "resolved %s" % k) + + # Close band → melee (swipe / smash). + var close_seen := await _observe(2.5, 6.0) + print(" close-band states: ", close_seen.keys()) + _ok(close_seen.has("SWIPE") or close_seen.has("SMASH"), "close range → melee routine") + # Charge was removed — the bear must never enter it. + _ok(not close_seen.has("CHARGE"), "no charge routine (removed)") + + # It must not climb on top of the bull: pressed right up against a pinned bull while + # doing melee (never leaping), the grounded bear's height must stay near the floor. + var max_y := -INF + var ct := 0.0 + while ct < 4.0 and is_instance_valid(_bear): + _pin_bull(1.5) + await physics_frame + ct += _dt() + if not String(_bear.call("ai_state_name")).begins_with("LEAP"): + max_y = maxf(max_y, _bear.global_position.y) + _ok(max_y < 0.6, "bear does not climb on top of the bull (max grounded y=%.2f)" % max_y) + + # Claw slashes: an emitter is bound to each paw, and one fires during the 1-2 punch. + _ok(_bear.get("_claw_l") != null and _bear.get("_claw_r") != null, "claw emitters bound to both paws") + var clawed := false + var wt := 0.0 + while wt < 8.0 and is_instance_valid(_bear) and not clawed: + _pin_bull(2.5) + await physics_frame + wt += _dt() + var cl: CPUParticles3D = _bear.get("_claw_l") + var cr: CPUParticles3D = _bear.get("_claw_r") + if (cl != null and cl.emitting) or (cr != null and cr.emitting): + clawed = true + _ok(clawed, "claw slash emits during the 1-2 punch") + + # Mid band → walk-in (stalk) or leap. + var mid_seen := await _observe(7.0, 8.0) + print(" mid-band states: ", mid_seen.keys()) + _ok(mid_seen.has("STALK") or mid_seen.has("LEAP"), "mid range → stalk/leap") + + # Routines reach ACTIVE (hit) and RECOVER (punish) phases. + var saw_active := false + var saw_recover := false + var t := 0.0 + while t < 8.0 and is_instance_valid(_bear): + _pin_bull(2.5) + await physics_frame + t += _dt() + var s: String = _bear.call("ai_state_name") + if s.contains(":ACTIVE"): saw_active = true + if s.contains(":RECOVER"): saw_recover = true + _ok(saw_active, "routines reach their ACTIVE (hit) phase") + _ok(saw_recover, "routines reach their RECOVER (punish) phase") + + # Leap must actually get airborne and then LAND before it recovers — the slam lands on + # the ground (impact frame), not mid-air. Reset the bear to a known grounded spot, then + # force one leap deterministically and follow it. + _bear.call("_enter_neutral") + _bear.global_position = Vector3(0, 1, 0) + _bear.velocity = Vector3.ZERO + var settle := 0 + while settle < 30 and not _bear.is_on_floor(): + _pin_bull(7.0) + await physics_frame + settle += 1 + var start_y: float = _bear.global_position.y + _bear.call("_start_leap") + var peak_y := start_y + var recovered_on_floor := false + var recovered_airborne := false + var reached_recover := false + t = 0.0 + while t < 4.0 and is_instance_valid(_bear): + _pin_bull(7.0) + await physics_frame + t += _dt() + peak_y = maxf(peak_y, _bear.global_position.y) + var s: String = _bear.call("ai_state_name") + if s.begins_with("LEAP:RECOVER"): + reached_recover = true + if _bear.is_on_floor(): recovered_on_floor = true + else: recovered_airborne = true + break + _ok(peak_y > start_y + 0.3, "leap gets airborne (peak +%.2fm)" % (peak_y - start_y)) + _ok(reached_recover, "leap reaches its slam/recovery") + _ok(recovered_on_floor and not recovered_airborne, + "leap lands on the ground before recovering (frame-25 contact, no mid-air finish)") + + # Hyper-armour: let the bear commit to a swing, then hit it — it must NOT drop into + # STAGGER mid-swing (only its recovery is punishable), yet it must still take the wound. + var in_active := false + t = 0.0 + while t < 8.0 and is_instance_valid(_bear) and not in_active: + _pin_bull(2.5) + await physics_frame + t += _dt() + in_active = String(_bear.call("ai_state_name")).contains(":ACTIVE") + _ok(in_active, "bear commits to an ACTIVE swing") + + var hp_before := int(_bear.get("_hp")) + var staggered_mid_swing := false + var i := 0 + while i < 8 and is_instance_valid(_bear) and String(_bear.call("ai_state_name")).contains(":ACTIVE"): + _pin_bull(2.5) + _bear.call("apply_ability_hit", Vector3.RIGHT, 10.0, 0.0) + await physics_frame + if String(_bear.call("ai_state_name")).begins_with("STAGGER"): + staggered_mid_swing = true + i += 1 + _ok(not staggered_mid_swing, "hyper-armour: hits during a swing don't stagger it") + _ok(int(_bear.get("_hp")) < hp_before, "bear still takes damage during the swing") + + # Drive HP down (spaced out so it isn't stun-locked) and confirm phase 2 engages. + t = 0.0 + while t < 8.0 and is_instance_valid(_bear) and not bool(_bear.get("_enraged")): + _pin_bull(2.5) + _bear.call("apply_ability_hit", Vector3.RIGHT, 10.0, 0.0) + for _f in range(18): # ~0.3s gap between hits + _pin_bull(2.5) + await physics_frame + t += _dt() + _ok(bool(_bear.get("_enraged")), "phase 2 (enrage) triggered below the HP fraction") + + # Finish it off and confirm death + free. + for _k in range(int(_bear.get("_hp")) + 2): + if is_instance_valid(_bear): + _bear.call("apply_ability_hit", Vector3.RIGHT, 12.0, 0.0) + for _f in range(3): + await physics_frame + await physics_frame + _ok(is_instance_valid(_bear) and not _bear.call("is_active"), "bear dead after HP drained") + await create_timer(6.0).timeout + _ok(not is_instance_valid(_bear), "bear freed after death topple") + + print("Results: %d passed, %d failed" % [_pass, _fail]) + quit(1 if _fail > 0 else 0) diff --git a/tests/bear_boss_test.gd.uid b/tests/bear_boss_test.gd.uid new file mode 100644 index 0000000..27eb219 --- /dev/null +++ b/tests/bear_boss_test.gd.uid @@ -0,0 +1 @@ +uid://c7wjoqgkpsepj diff --git a/tests/level_switch_test.gd b/tests/level_switch_test.gd new file mode 100644 index 0000000..3d70d62 --- /dev/null +++ b/tests/level_switch_test.gd @@ -0,0 +1,72 @@ +extends SceneTree +## Level-isolation test: when the shell loads the Bear's Den, none of the arena level's +## geometry — crucially its wall/floor colliders and audience — may survive into it. +## Guards the modular-level contract: switching levels swaps the whole module, it never +## hides old nodes (hidden colliders still collide). Run: +## godot --headless --script tests/level_switch_test.gd + +var _pass := 0 +var _fail := 0 + + +func _init() -> void: + _run.call_deferred() + + +func _ok(c: bool, m: String) -> void: + if c: + _pass += 1 + print(" PASS: ", m) + else: + _fail += 1 + print(" FAIL: ", m) + + +func _count_named(node: Node, wanted: String, acc: int = 0) -> int: + if node.name == wanted: + acc += 1 + for child: Node in node.get_children(): + acc = _count_named(child, wanted, acc) + return acc + + +func _count_type(node: Node, klass: String, acc: int = 0) -> int: + if node.is_class(klass): + acc += 1 + for child: Node in node.get_children(): + acc = _count_type(child, klass, acc) + return acc + + +func _run() -> void: + var run: Node = root.get_node("Run") + run.ensure_run() + # Force the shell to build the Bear's Den regardless of the generated map. + run.debug_level_override = run.get_level(&"bear") + + var scene: Node = load("res://scene.tscn").instantiate() + root.add_child(scene) + current_scene = scene + # Let the shell instance the level and the spawner's deferred spawn run. + await create_timer(0.8).timeout + + # Shell survives and hosts exactly one level module. + var level_root: Node = scene.get_node_or_null("LevelRoot") + _ok(level_root != null, "shell has a LevelRoot") + _ok(level_root != null and level_root.get_child_count() == 1, "exactly one level module loaded") + _ok(not get_nodes_in_group(&"player").is_empty(), "player (shell) present") + + # The leak we are guarding against: no arena geometry may exist in the bear level. + _ok(_count_named(scene, "arena_placeholder_v01") == 0, "arena placeholder (+ its wall collider) gone") + _ok(_count_named(scene, "Arena2") == 0, "arena mesh 'Arena2' gone") + _ok(_count_named(scene, "PublikNode") == 0, "arena audience gone from bear level") + + # The bear level brings its own geometry, one floor collider, and its boss. + _ok(_count_named(scene, "bear_level_arena") == 1, "bear arena mesh present") + _ok(_count_type(level_root, "StaticBody3D") >= 1, "bear level has its own floor collider") + + await create_timer(0.4).timeout + _ok(not get_nodes_in_group(&"bear").is_empty(), "bear boss spawned in bear level") + + print("Results: %d passed, %d failed" % [_pass, _fail]) + quit(1 if _fail > 0 else 0) diff --git a/tests/level_switch_test.gd.uid b/tests/level_switch_test.gd.uid new file mode 100644 index 0000000..7e4c410 --- /dev/null +++ b/tests/level_switch_test.gd.uid @@ -0,0 +1 @@ +uid://b6ud4uune1clf