Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d57c3dc2a2 | |||
| 09ae6bfbf4 | |||
| 40c1b8e680 |
+5
-1
@@ -10,6 +10,7 @@ extends Node
|
||||
##
|
||||
## Toggle from the console:
|
||||
## show_collisions · show_hitboxes · show_bones · show_raycasts (true / false)
|
||||
## show_hitboxes also surfaces the bull's and bear's body capsules, not just Area3Ds
|
||||
## show_states — floating AI-state tags + velocity vectors over each matador
|
||||
## show_animation_name — floating current-clip tag over every AnimationPlayer host
|
||||
## show_stats — 2D corner readout: fps / frame ms / draw calls / matador tally
|
||||
@@ -421,7 +422,10 @@ func _collect(node: Node, col: bool, hit: bool, bone: bool, anim: bool, wanted:
|
||||
# Ragdoll bone colliders (PhysicalBone3D) are excluded — use show_bones for
|
||||
# the skeleton; otherwise every corpse floods the view with capsules.
|
||||
var is_body := parent is PhysicsBody3D and not (parent is PhysicalBone3D)
|
||||
if (is_area and hit) or (is_body and col):
|
||||
# The bull's and bear's body capsules are combat volumes too, so they also
|
||||
# surface under show_hitboxes (not just show_collisions).
|
||||
var is_combatant := is_body and (parent.is_in_group(&"player") or parent.is_in_group(&"bear"))
|
||||
if (is_area and hit) or (is_body and col) or (is_combatant and hit):
|
||||
wanted[cs.get_instance_id()] = [cs, is_area]
|
||||
for child in node.get_children():
|
||||
_collect(child, col, hit, bone, anim, wanted)
|
||||
|
||||
+17
-3
@@ -433,12 +433,22 @@ func save() -> void:
|
||||
if p["value"] == p["default"]:
|
||||
continue
|
||||
var re := RegEx.new()
|
||||
var literal := ""
|
||||
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))
|
||||
literal = _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"))
|
||||
literal = "true" if bool(p["value"]) else "false"
|
||||
var m := re.search(src)
|
||||
if m == null:
|
||||
push_warning("DP.save: no _reg_ line found for '%s' — skipped" % key)
|
||||
continue
|
||||
# Splice the value in by index rather than RegEx.sub(): sub()'s replacement
|
||||
# string parses "$n" as a backreference, so "$1"+"12.5" becomes "$112.5"
|
||||
# (group 112 → PCRE2 error → mangled output). Indexed splice inserts the
|
||||
# literal verbatim, exactly the value the user entered.
|
||||
src = src.substr(0, m.get_start(2)) + literal + src.substr(m.get_end(2))
|
||||
p["default"] = p["value"]
|
||||
|
||||
var wfile := FileAccess.open(path, FileAccess.WRITE)
|
||||
@@ -449,8 +459,12 @@ func save() -> void:
|
||||
wfile.close()
|
||||
|
||||
|
||||
# Godot's String % operator has no "g" conversion — "%.6g" % v slips past the
|
||||
# compiler (v is not const) but emits the literal "%.6g" at runtime, which would be
|
||||
# written straight into the source. "%s" gives the shortest round-tripping form,
|
||||
# matching the value the user entered; the guard keeps it a float literal.
|
||||
func _fmt_float(v: float) -> String:
|
||||
var s := "%.6g" % v
|
||||
var s := "%s" % v
|
||||
if not ("." in s or "e" in s):
|
||||
s += ".0"
|
||||
return s
|
||||
|
||||
@@ -217,7 +217,7 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and not event.echo:
|
||||
if _game_over and event.physical_keycode in [KEY_ENTER, KEY_KP_ENTER]:
|
||||
if _result_win:
|
||||
_continue_to_map()
|
||||
_continue_to_lobby()
|
||||
else:
|
||||
_restart()
|
||||
elif event.physical_keycode == KEY_TAB and not _game_over:
|
||||
@@ -238,12 +238,15 @@ func _restart() -> void:
|
||||
get_tree().reload_current_scene()
|
||||
|
||||
|
||||
# A cleared arena advances the run: commit the win and open the map to pick the next node.
|
||||
func _continue_to_map() -> void:
|
||||
# A cleared level advances the run and drops the player into the lobby hub: commit the
|
||||
# win, flag the lobby as the active level, then reload the shell so it builds the lobby.
|
||||
# (Choosing the next map node happens from the lobby via trigger areas, added later.)
|
||||
func _continue_to_lobby() -> void:
|
||||
get_tree().paused = false
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
Run.complete_current_level()
|
||||
get_tree().change_scene_to_file("res://levels/MapScreen.tscn")
|
||||
Run.go_to_lobby()
|
||||
get_tree().change_scene_to_file(Run.active_level().scene_path)
|
||||
|
||||
|
||||
func _toggle_controls() -> void:
|
||||
@@ -400,7 +403,7 @@ func _build_game_over() -> void:
|
||||
# Win → Continue to the run map; loss → Go Again from the top. Shown per-result in
|
||||
# _show_game_over; both share the Enter shortcut.
|
||||
_over_continue = _make_button("Continue (Enter)")
|
||||
_over_continue.pressed.connect(_continue_to_map)
|
||||
_over_continue.pressed.connect(_continue_to_lobby)
|
||||
row.add_child(_over_continue)
|
||||
|
||||
_over_again = _make_button("Go Again (Enter)")
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
[gd_scene format=3 uid="uid://32nyipcwrab5"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://cwmo5vh738x6w" path="res://Assets/Lobby_level.glb" id="1_lobby"]
|
||||
|
||||
[sub_resource type="Environment" id="Environment_lobby"]
|
||||
|
||||
[node name="lobby_level" type="Node3D"]
|
||||
|
||||
[node name="Lobby_level" parent="." instance=ExtResource("1_lobby")]
|
||||
|
||||
[node name="Lights" type="Node3D" parent="."]
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Lights"]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.7071067, 0.70710677, 0, -0.70710677, 0.7071067, 0, 36.945946, 70)
|
||||
light_color = Color(1, 0.9137255, 0.827451, 1)
|
||||
light_energy = 1.6
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="DirectionalLight3D2" type="DirectionalLight3D" parent="Lights"]
|
||||
transform = Transform3D(1, 0, 0, 0, -0.8660254, 0.49999994, 0, -0.49999994, -0.8660254, 0, 112.56815, 93.37274)
|
||||
light_color = Color(0.4784314, 0.6156863, 0.8, 1)
|
||||
light_energy = 0.45
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||
environment = SubResource("Environment_lobby")
|
||||
+22
-1
@@ -15,6 +15,7 @@ signal run_started
|
||||
const _SHELL_SCENE := "res://scene.tscn"
|
||||
const _ARENA_LEVEL := preload("res://levels/arena_level.tscn")
|
||||
const _BEAR_LEVEL := preload("res://levels/bear_level.tscn")
|
||||
const _LOBBY_LEVEL := preload("res://levels/lobby_level.tscn")
|
||||
|
||||
# Map shape. Small + placeholder for now; the final row is a single boss node.
|
||||
const _ROWS := 5
|
||||
@@ -33,6 +34,11 @@ 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
|
||||
## The lobby is an interstitial hub between fights, not a map node: the player is sent
|
||||
## here after clearing a level (go_to_lobby()) to move around freely before choosing the
|
||||
## next node. While set, active_level() serves the lobby; cleared once a node is picked
|
||||
## (select_node) or a fresh run begins.
|
||||
var enter_lobby: bool = false
|
||||
|
||||
# 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
|
||||
@@ -43,6 +49,7 @@ const _BEAR_SCENE := preload("res://Bear.tscn")
|
||||
var _arena: LevelDef
|
||||
var _arena_boss: LevelDef
|
||||
var _bear: LevelDef
|
||||
var _lobby: LevelDef
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -52,6 +59,8 @@ func _ready() -> void:
|
||||
)
|
||||
_bear = _make_level(&"bear", "Bear's Den", _BEAR_LEVEL, Color(0.55, 0.38, 0.22), false)
|
||||
_bear.enemy_scene = _BEAR_SCENE
|
||||
# The lobby is a fightless hub — no enemy_scene, so no wave spawns and no win/lose.
|
||||
_lobby = _make_level(&"lobby", "Lobby", _LOBBY_LEVEL, Color(0.62, 0.66, 0.72), false)
|
||||
|
||||
|
||||
func _make_level(
|
||||
@@ -72,6 +81,8 @@ func _make_level(
|
||||
func active_level() -> LevelDef:
|
||||
if debug_level_override != null:
|
||||
return debug_level_override
|
||||
if enter_lobby:
|
||||
return _lobby
|
||||
var n := node_at(pending_row, pending_col)
|
||||
if n == null:
|
||||
n = current_node()
|
||||
@@ -84,12 +95,13 @@ func get_level(id: StringName) -> LevelDef:
|
||||
&"arena": return _arena
|
||||
&"arena_boss": return _arena_boss
|
||||
&"bear": return _bear
|
||||
&"lobby": return _lobby
|
||||
return null
|
||||
|
||||
|
||||
## All known level ids — shown by `level` in the console.
|
||||
func level_ids() -> Array[StringName]:
|
||||
return [&"arena", &"arena_boss", &"bear"]
|
||||
return [&"arena", &"arena_boss", &"bear", &"lobby"]
|
||||
|
||||
|
||||
## Begin a fresh run: build a new map and place the player at the start gate. Called from
|
||||
@@ -101,6 +113,7 @@ func start_new_run() -> void:
|
||||
pending_row = -1
|
||||
pending_col = -1
|
||||
debug_level_override = null
|
||||
enter_lobby = false
|
||||
run_started.emit()
|
||||
|
||||
|
||||
@@ -156,10 +169,18 @@ func run_complete() -> bool:
|
||||
func select_node(node: MapNode) -> void:
|
||||
if node == null:
|
||||
return
|
||||
enter_lobby = false
|
||||
pending_row = node.row
|
||||
pending_col = node.col
|
||||
|
||||
|
||||
## Send the player to the lobby hub instead of a fight — active_level() serves the lobby
|
||||
## until a node is picked. Called after clearing a level so the player lands in the lobby
|
||||
## rather than jumping straight to the next fight.
|
||||
func go_to_lobby() -> void:
|
||||
enter_lobby = true
|
||||
|
||||
|
||||
## Commit the level just won: move the player onto the pending node (or leave them at the
|
||||
## start gate after the intro fight, which has no pending node).
|
||||
func complete_current_level() -> void:
|
||||
|
||||
Reference in New Issue
Block a user