From fd89e89f2f00a73a60f6e5211dddeebafa534306 Mon Sep 17 00:00:00 2001 From: RicZ914 <3251573076@qq.com> Date: Mon, 15 Jun 2026 17:12:45 +0800 Subject: [PATCH] Connect map rooms and fix prop projectile collisions --- effects/projectiles/arcane_bolt.gd | 16 ++++ effects/projectiles/enemy_bolt.gd | 16 ++++ effects/projectiles/piercing_arrow.gd | 16 ++++ effects/projectiles/royal_bolt.gd | 16 ++++ systems/map/map_runtime.gd | 5 +- systems/run/audio_route.gd | 10 ++- systems/run/run_director.gd | 6 ++ tests/smoke_run_flow.gd | 2 +- tools/map_browser_demo.gd | 2 + world.gd | 104 +++++++++++++++++++------- 10 files changed, 157 insertions(+), 36 deletions(-) diff --git a/effects/projectiles/arcane_bolt.gd b/effects/projectiles/arcane_bolt.gd index 03f0a1a..16e20d1 100644 --- a/effects/projectiles/arcane_bolt.gd +++ b/effects/projectiles/arcane_bolt.gd @@ -35,7 +35,10 @@ func setup(owner_actor: Node, travel_direction: Vector2, hit_damage: float, hit_ timer.timeout.connect(queue_free) func _physics_process(delta: float) -> void: + var previous_position := global_position global_position += direction * speed * delta + if _expire_if_blocked_between(previous_position, global_position): + return pulse_time += delta bolt.scale = Vector2.ONE * (1.06 if int(pulse_time * 16.0) % 2 == 0 else 0.94) trail_timer -= delta @@ -80,6 +83,19 @@ func _expire_on_blocker() -> void: _spawn_hit_flash() _consume_bolt() +func _expire_if_blocked_between(from_position: Vector2, to_position: Vector2) -> bool: + if expired or from_position == to_position: + return false + var query := PhysicsRayQueryParameters2D.create(from_position, to_position) + query.collision_mask = 1 + query.exclude = [self] + var hit := get_world_2d().direct_space_state.intersect_ray(query) + var collider = hit.get("collider", null) + if collider is Node and (collider as Node).is_in_group("projectile_blocker"): + _expire_on_blocker() + return true + return false + func _try_hit_overlapping_targets() -> void: if expired: return diff --git a/effects/projectiles/enemy_bolt.gd b/effects/projectiles/enemy_bolt.gd index 7f3981f..d1e0b62 100644 --- a/effects/projectiles/enemy_bolt.gd +++ b/effects/projectiles/enemy_bolt.gd @@ -31,7 +31,10 @@ func setup(owner_actor: Node, travel_direction: Vector2, hit_damage: float, colo timer.timeout.connect(queue_free) func _physics_process(delta: float) -> void: + var previous_position := global_position global_position += direction * speed * delta + if _expire_if_blocked_between(previous_position, global_position): + return pulse_time += delta var pixel_pulse := 1.08 if int(pulse_time * 18.0) % 2 == 0 else 0.96 bolt.scale = Vector2.ONE * pixel_pulse @@ -92,6 +95,19 @@ func _expire_on_blocker() -> void: _spawn_hit_flash() queue_free() +func _expire_if_blocked_between(from_position: Vector2, to_position: Vector2) -> bool: + if expired or from_position == to_position: + return false + var query := PhysicsRayQueryParameters2D.create(from_position, to_position) + query.collision_mask = 1 + query.exclude = [self] + var hit := get_world_2d().direct_space_state.intersect_ray(query) + var collider = hit.get("collider", null) + if collider is Node and (collider as Node).is_in_group("projectile_blocker"): + _expire_on_blocker() + return true + return false + func _resolve_damage_target(target: Variant) -> Node: if target == null or not (target is Node): return null diff --git a/effects/projectiles/piercing_arrow.gd b/effects/projectiles/piercing_arrow.gd index 2f9c6f5..58c1ca8 100644 --- a/effects/projectiles/piercing_arrow.gd +++ b/effects/projectiles/piercing_arrow.gd @@ -31,7 +31,10 @@ func setup(owner_actor: Node, travel_direction: Vector2, hit_damage: float, hit_ timer.timeout.connect(queue_free) func _physics_process(delta: float) -> void: + var previous_position := global_position global_position += direction * speed * delta + if _expire_if_blocked_between(previous_position, global_position): + return pulse_time += delta trail.scale = Vector2(1.08 if int(pulse_time * 20.0) % 2 == 0 else 0.98, 1.0) trail.color = Color(1.0, 0.96, 0.72, 1.0) @@ -74,6 +77,19 @@ func _expire_on_blocker() -> void: _spawn_hit_flash() queue_free() +func _expire_if_blocked_between(from_position: Vector2, to_position: Vector2) -> bool: + if expired or from_position == to_position: + return false + var query := PhysicsRayQueryParameters2D.create(from_position, to_position) + query.collision_mask = 1 + query.exclude = [self] + var hit := get_world_2d().direct_space_state.intersect_ray(query) + var collider = hit.get("collider", null) + if collider is Node and (collider as Node).is_in_group("projectile_blocker"): + _expire_on_blocker() + return true + return false + func _resolve_damage_target(target: Variant) -> Node: if target == null or not (target is Node): return null diff --git a/effects/projectiles/royal_bolt.gd b/effects/projectiles/royal_bolt.gd index f6e29c9..fd18d3d 100644 --- a/effects/projectiles/royal_bolt.gd +++ b/effects/projectiles/royal_bolt.gd @@ -28,7 +28,10 @@ func setup(owner_actor: Node, travel_direction: Vector2, hit_damage: float, payl timer.timeout.connect(queue_free) func _physics_process(delta: float) -> void: + var previous_position := global_position global_position += direction * speed * delta + if _expire_if_blocked_between(previous_position, global_position): + return pulse_time += delta bolt.scale = Vector2.ONE * (1.08 if int(pulse_time * 18.0) % 2 == 0 else 0.96) trail_timer -= delta @@ -74,6 +77,19 @@ func _expire_on_blocker() -> void: _spawn_hit_flash() queue_free() +func _expire_if_blocked_between(from_position: Vector2, to_position: Vector2) -> bool: + if expired or from_position == to_position: + return false + var query := PhysicsRayQueryParameters2D.create(from_position, to_position) + query.collision_mask = 1 + query.exclude = [self] + var hit := get_world_2d().direct_space_state.intersect_ray(query) + var collider = hit.get("collider", null) + if collider is Node and (collider as Node).is_in_group("projectile_blocker"): + _expire_on_blocker() + return true + return false + func _resolve_damage_target(target: Variant) -> Node: if target == null or not (target is Node): return null diff --git a/systems/map/map_runtime.gd b/systems/map/map_runtime.gd index d3922cc..5fedfdd 100644 --- a/systems/map/map_runtime.gd +++ b/systems/map/map_runtime.gd @@ -48,7 +48,7 @@ func build() -> void: world_root.add_child(map_camera) -func activate_room(room_index: int, player_character: Node) -> void: +func activate_room(room_index: int, player_character: Node, move_player: bool = true) -> void: if map_walkable_rects.is_empty(): return var clamped_index := clampi(room_index, 0, map_walkable_rects.size() - 1) @@ -56,7 +56,7 @@ func activate_room(room_index: int, player_character: Node) -> void: spawn_marker.position = player_spawn_for_room(clamped_index) if encounter_marker != null: encounter_marker.position = encounter_spawn_for_room(clamped_index) - if player_character is Node2D and spawn_marker != null and is_instance_valid(player_character): + if move_player and player_character is Node2D and spawn_marker != null and is_instance_valid(player_character): (player_character as Node2D).position = spawn_marker.position update_camera(clamped_index, player_character, true) @@ -263,6 +263,7 @@ func _try_add_generated_room_prop(room_index: int, candidate: Dictionary, placed var source_width := maxf(1.0, float(source_size[0])) var source_height := maxf(1.0, float(source_size[1])) var texture_to_room_scale := Vector2(room_rect.size.x / source_width, room_rect.size.y / source_height) + texture_to_room_scale *= MapBrowserDemo.RANDOM_PROP_WORLD_SCALE var prop_size := Vector2(float(texture.get_width()) * texture_to_room_scale.x, float(texture.get_height()) * texture_to_room_scale.y) if not MapBrowserDemo.is_generated_prop_size_usable(prop_size, room_rect.size): return false diff --git a/systems/run/audio_route.gd b/systems/run/audio_route.gd index b71bef6..0bedcc1 100644 --- a/systems/run/audio_route.gd +++ b/systems/run/audio_route.gd @@ -4,13 +4,15 @@ extends RefCounted static func play_for_encounter(music: Node, next_encounter_index: int) -> void: if music == null: return - if next_encounter_index <= 4: + if next_encounter_index <= 3 or next_encounter_index == 7: music.call("play_profile", &"town_battle") - elif next_encounter_index == 5: + elif next_encounter_index >= 4 and next_encounter_index <= 6: + music.call("play_profile", &"church_intermission") + elif next_encounter_index == 8: music.call("play_profile", &"gate_guard") - elif next_encounter_index == 6: + elif next_encounter_index >= 9 and next_encounter_index <= 11: music.call("play_profile", &"palace_explore") - elif next_encounter_index == 7: + elif next_encounter_index == 12: music.call("play_profile", &"twin_princes") else: music.call("play_profile", &"emperor") diff --git a/systems/run/run_director.gd b/systems/run/run_director.gd index 61d7219..3cb91f1 100644 --- a/systems/run/run_director.gd +++ b/systems/run/run_director.gd @@ -110,6 +110,12 @@ func reset_run() -> void: town_service_consumed = false _emit_state() +func mark_town_services_consumed() -> void: + if town_service_consumed: + return + town_service_consumed = true + _emit_state() + func reward_encounter(encounter_index: int, actor: Node = null) -> int: cleared_encounters += 1 var base_reward: int = 12 + maxi(encounter_index, 0) * 6 diff --git a/tests/smoke_run_flow.gd b/tests/smoke_run_flow.gd index 11edc92..a7192fc 100644 --- a/tests/smoke_run_flow.gd +++ b/tests/smoke_run_flow.gd @@ -229,7 +229,7 @@ func _run() -> void: world.current_encounter = first_room_stub world.player_character.global_position = world._room_exit_target(0) world._on_encounter_defeated() - await create_timer(0.9).timeout + await create_timer(2.8).timeout await process_frame if world.stage_reward_panel == null or not world.stage_reward_panel.visible: push_error("Stage reward panel did not open after the first map encounter") diff --git a/tools/map_browser_demo.gd b/tools/map_browser_demo.gd index 5610664..a2f4944 100644 --- a/tools/map_browser_demo.gd +++ b/tools/map_browser_demo.gd @@ -101,6 +101,7 @@ const GENERATED_PROP_MANIFEST_PATH := "res://assets/maps/stitched_demo/generated const RANDOM_PROP_MIN_PER_ROOM := 2 const RANDOM_PROP_MAX_PER_ROOM := 4 const RANDOM_PROP_PLACEMENT_ATTEMPTS := 56 +const RANDOM_PROP_WORLD_SCALE := 0.70 const RANDOM_PROP_MIN_WIDTH_RATIO := 0.045 const RANDOM_PROP_MIN_HEIGHT_RATIO := 0.050 const RANDOM_PROP_MIN_AREA_RATIO := 0.0040 @@ -667,6 +668,7 @@ func _try_add_random_cover_prop(parent: Node, room_index: int, candidate: Dictio var source_width := maxf(1.0, float(source_size[0])) var source_height := maxf(1.0, float(source_size[1])) var texture_to_room_scale := Vector2(room_rect.size.x / source_width, room_rect.size.y / source_height) + texture_to_room_scale *= RANDOM_PROP_WORLD_SCALE var prop_size := Vector2(float(texture.get_width()) * texture_to_room_scale.x, float(texture.get_height()) * texture_to_room_scale.y) if not is_generated_prop_size_usable(prop_size, room_rect.size): return false diff --git a/world.gd b/world.gd index e177cdd..1c779ad 100644 --- a/world.gd +++ b/world.gd @@ -23,8 +23,13 @@ const ENCOUNTER_SCENES := [ preload("res://actors/encounters/town_mob_encounter.tscn"), preload("res://actors/encounters/town_mob_encounter.tscn"), preload("res://actors/encounters/town_mob_encounter.tscn"), + preload("res://actors/encounters/empty_encounter.tscn"), + preload("res://actors/encounters/empty_encounter.tscn"), + preload("res://actors/encounters/empty_encounter.tscn"), preload("res://actors/encounters/town_mob_encounter.tscn"), preload("res://actors/bosses/town/judicator_boss.tscn"), + preload("res://actors/encounters/town_mob_encounter.tscn"), + preload("res://actors/encounters/town_mob_encounter.tscn"), preload("res://actors/encounters/empty_encounter.tscn") ] const FINAL_BOSS_SCENES := [ @@ -32,7 +37,12 @@ const FINAL_BOSS_SCENES := [ RANGER_BOSS_SCENE, MAGE_BOSS_SCENE ] -const ENCOUNTER_ROOM_INDICES := [0, 1, 2, 3, 8, 9, 10, 11, 12] +const ENCOUNTER_ROOM_INDICES := [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] +const FUNCTION_ROOM_EVENTS := { + 4: "rest", + 5: "forge", + 6: "shop" +} const RELIC_REROLL_COST := 12 const HIT_FEEDBACK_COOLDOWN_MSEC := 55 const GATE_WALK_FINISH_DISTANCE := 10.0 @@ -76,6 +86,7 @@ var reward_pause_resume_pending: bool = false var last_attack_feedback_msec: int = 0 var reward_rng := RandomNumberGenerator.new() var map_runtime: Node = null +var active_map_room_index: int = -1 var gate_walk_active: bool = false var gate_walk_target: Vector2 = Vector2.ZERO var gate_walk_callback: Callable = Callable() @@ -215,6 +226,20 @@ func _walk_player_to_room_exit(callback: Callable) -> void: exit_target = player_node.global_position + Vector2(90.0, 0.0) _walk_player_to_target(exit_target, callback) +func _walk_player_to_next_room_entry(next_encounter_index: int, callback: Callable) -> void: + if next_encounter_index >= _encounter_count(): + if callback.is_valid(): + callback.call() + return + _walk_player_to_target(_player_spawn_for_room(next_encounter_index), func() -> void: + if not is_instance_valid(self): + return + _activate_map_room(next_encounter_index, false) + _update_map_camera(true) + if callback.is_valid(): + callback.call() + ) + func _walk_player_to_target(target_position: Vector2, callback: Callable) -> void: if player_character == null or not is_instance_valid(player_character) or not (player_character is Node2D): if callback.is_valid(): @@ -303,10 +328,12 @@ func _hide_legacy_arena() -> void: if child is CanvasItem: (child as CanvasItem).visible = false -func _activate_map_room(room_index: int) -> void: +func _activate_map_room(room_index: int, move_player: bool = true) -> void: if map_runtime == null: return - map_runtime.activate_room(_map_room_index_for_encounter(room_index), player_character) + var map_room_index := _map_room_index_for_encounter(room_index) + map_runtime.activate_room(map_room_index, player_character, move_player) + active_map_room_index = map_room_index func _player_spawn_for_room(room_index: int) -> Vector2: if map_runtime == null: @@ -498,14 +525,17 @@ func _start_next_encounter() -> void: _complete_run_victory() return if encounter_index > 0: - _transition_through_room_door(encounter_index, Callable(self, "_begin_current_encounter")) + if active_map_room_index == _map_room_index_for_encounter(encounter_index): + _begin_current_encounter() + else: + _transition_through_room_door(encounter_index, Callable(self, "_begin_current_encounter")) return _begin_current_encounter() func _begin_current_encounter() -> void: if LineageDirector != null: LineageDirector.record_checkpoint(encounter_index) - _activate_map_room(encounter_index) + _activate_map_room(encounter_index, active_map_room_index < 0) _play_audio_profile_for_encounter(encounter_index) current_encounter = _encounter_scene_for_index(encounter_index).instantiate() current_encounter.position = encounter_marker.position @@ -525,28 +555,13 @@ func _transition_through_room_door(room_index: int, callback: Callable) -> void: if callback.is_valid(): callback.call() return - if door_transition_layer == null or door_transition_backdrop == null: - if callback.is_valid(): - callback.call() - return - door_transition_layer.visible = true - door_transition_backdrop.color = Color(0.01, 0.012, 0.018, 0.0) - var tween := create_tween() - tween.tween_property(door_transition_backdrop, "color:a", 0.86, 0.12) - tween.tween_callback(func() -> void: + _walk_player_to_target(_player_spawn_for_room(room_index), func() -> void: if not is_instance_valid(self): return - _activate_map_room(room_index) - if player_character is Node2D and is_instance_valid(player_character): - (player_character as Node2D).global_position = _room_entrance_target(room_index) + _activate_map_room(room_index, false) _update_map_camera(true) - ) - tween.tween_property(door_transition_backdrop, "color:a", 0.0, 0.18) - tween.tween_callback(func() -> void: - if not is_instance_valid(self): - return - door_transition_layer.visible = false - _walk_player_to_target(_player_spawn_for_room(room_index), callback) + if callback.is_valid(): + callback.call() ) func _toggle_inventory_panel() -> void: @@ -573,6 +588,7 @@ func _on_encounter_defeated() -> void: if skip_rewards: if not active_encounter_prep.is_empty(): RunDirector.set_pending_encounter_prep(active_encounter_prep) + var function_event_kind := _function_event_kind_for_encounter(encounter_index) current_encounter = null active_encounter_prep.clear() _refresh_battle_status( @@ -583,10 +599,13 @@ func _on_encounter_defeated() -> void: var empty_timer := get_tree().create_timer(0.35) empty_timer.timeout.connect(func() -> void: if is_instance_valid(self) and player_character != null and is_instance_valid(player_character) and float(player_character.hp) > 0.0: - _walk_player_to_room_exit(func() -> void: - if is_instance_valid(self): - _start_next_encounter() - ) + if not function_event_kind.is_empty(): + _offer_run_event_kind(function_event_kind) + else: + _walk_player_to_room_exit(func() -> void: + if is_instance_valid(self): + _start_next_encounter() + ) ) return var reward := RunDirector.reward_encounter(encounter_index, player_character) @@ -615,7 +634,15 @@ func _on_encounter_defeated() -> void: if defeated_final_encounter: _complete_run_victory() else: - _offer_stage_clear_reward() + var next_encounter_index := encounter_index + 1 + _walk_player_to_next_room_entry(next_encounter_index, func() -> void: + if not is_instance_valid(self) or player_character == null or not is_instance_valid(player_character) or float(player_character.hp) <= 0.0: + return + if not _function_event_kind_for_encounter(next_encounter_index).is_empty(): + _start_next_encounter() + else: + _offer_stage_clear_reward() + ) ) ) @@ -704,6 +731,25 @@ func _offer_accessory(reason: String, source: String = "route") -> void: ]) ) +func _function_event_kind_for_encounter(index: int) -> String: + var room_index := _map_room_index_for_encounter(index) + return String(FUNCTION_ROOM_EVENTS.get(room_index, "")) + +func _offer_run_event_kind(kind: String) -> void: + if kind == "relic" or run_event_panel == null or not run_event_panel.has_method("open"): + _offer_accessory(_ui_text("Victory Relic", "Victory Relic", "Victory Relic"), "victory") + return + if kind == "rest" or kind == "forge" or kind == "shop": + RunDirector.mark_town_services_consumed() + _play_intermission_audio() + active_run_event_kind = kind + run_event_panel.open(kind, int(RunDirector.gold)) + _refresh_battle_status( + _ui_text("Run Event", "Run Event", "Run Event"), + _ui_text("Choose a reward before moving on.", "Choose a reward before moving on.", "Choose a reward before moving on."), + _localized_detail_text("%s: %d" % [_ui_text("Gold", "Gold", "Gold"), int(RunDirector.gold)]) + ) + func _offer_next_run_event() -> void: var kind := RunDirector.next_event_kind() if kind == "relic" or run_event_panel == null or not run_event_panel.has_method("open"):