diff --git a/src/default_bus_layout.tres b/src/default_bus_layout.tres index cae1604..80f25de 100644 --- a/src/default_bus_layout.tres +++ b/src/default_bus_layout.tres @@ -5,6 +5,11 @@ resource_name = "Reverb" room_size = 0.51 wet = 0.28 +[sub_resource type="AudioEffectLowPassFilter" id="AudioEffectLowPassFilter_j3pel"] +resource_name = "LowPassFilter" +cutoff_hz = 958.0 +resonance = 0.75 + [resource] bus/1/name = &"Sfx" bus/1/solo = false @@ -14,3 +19,11 @@ bus/1/volume_db = 0.0 bus/1/send = &"Master" bus/1/effect/0/effect = SubResource("AudioEffectReverb_j3pel") bus/1/effect/0/enabled = true +bus/2/name = &"SfxFiltered" +bus/2/solo = false +bus/2/mute = false +bus/2/bypass_fx = false +bus/2/volume_db = 0.0 +bus/2/send = &"Sfx" +bus/2/effect/0/effect = SubResource("AudioEffectLowPassFilter_j3pel") +bus/2/effect/0/enabled = true diff --git a/src/scenes/ingame_hud.tscn b/src/scenes/ingame_hud.tscn index addefdb..05c6aae 100644 --- a/src/scenes/ingame_hud.tscn +++ b/src/scenes/ingame_hud.tscn @@ -110,6 +110,23 @@ theme = SubResource("Theme_standard_font") text = "00:00" horizontal_alignment = 1 +[node name="VBoxContainerDarknessDebug" type="VBoxContainer" parent="UpperLeft/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="LabelDarknessDebugTitle" type="Label" parent="UpperLeft/HBoxContainer/VBoxContainerDarknessDebug"] +layout_mode = 2 +theme = SubResource("Theme_standard_font") +text = "Darkness" +horizontal_alignment = 1 + +[node name="LabelDarknessDebug" type="Label" parent="UpperLeft/HBoxContainer/VBoxContainerDarknessDebug"] +layout_mode = 2 +theme = SubResource("Theme_standard_font") +text = "" +horizontal_alignment = 1 +autowrap_mode = 2 + [node name="UpperRight" type="MarginContainer" parent="." unique_id=1261821969] anchors_preset = 1 anchor_left = 1.0 diff --git a/src/scripts/attack_bomb.gd b/src/scripts/attack_bomb.gd index 2bb28a5..e49eb99 100644 --- a/src/scripts/attack_bomb.gd +++ b/src/scripts/attack_bomb.gd @@ -32,10 +32,19 @@ var blink_start_time: float = 1.0 # Start blinking 1 second before explosion var can_be_collected: bool = false var collection_delay: float = 0.2 # Can be collected after 0.2 seconds -# Fallout (landed in quicksand): sink and explode with no visual/damage, but sound + screenshake +# Fallout (landed in quicksand): stop momentum, glide to tile center, sink in place (like player) var fell_in_fallout: bool = false var fallout_sink_progress: float = 1.0 const FALLOUT_SINK_DURATION: float = 0.5 +const FALLOUT_CENTER_THRESHOLD: float = 2.0 +const FALLOUT_GLIDE_SPEED: float = 220.0 # px/s toward tile center when on fallout +const FALLOUT_TILE_HALF_SIZE: float = 8.0 + +# Bus for fuse/explosion when in fallout (SfxFiltered if it exists, else Sfx so sound still plays) +static func _fallout_sfx_bus() -> String: + if AudioServer.get_bus_index("SfxFiltered") >= 0: + return "SfxFiltered" + return "Sfx" @onready var sprite = $Sprite2D @onready var explosion_sprite = $ExplosionSprite @@ -137,8 +146,11 @@ func _start_fuse(): fuse_timer = 0.0 # Play fuse sound - if has_node("SfxFuse"): - $SfxFuse.play() + var sfx_fuse = get_node_or_null("SfxFuse") + if sfx_fuse: + if fell_in_fallout: + sfx_fuse.bus = _fallout_sfx_bus() + sfx_fuse.play() # Start fuse particles if fuse_particles: @@ -239,8 +251,36 @@ func _physics_process(delta): shadow.scale = Vector2.ONE shadow.modulate = Color(0, 0, 0, 0.5) - # Apply friction if on ground - if not is_airborne: + # On ground: check for fallout tile (stop momentum, glide to center, sink in place like player) + var gw = get_tree().get_first_node_in_group("game_world") + if gw and gw.has_method("_is_position_on_fallout_tile") and gw._is_position_on_fallout_tile(global_position): + # Stop all momentum and lock to fallout behavior + throw_velocity = Vector2.ZERO + velocity = Vector2.ZERO + if gw.has_method("_get_tile_center_at"): + var tile_center = gw._get_tile_center_at(global_position) + var dist_to_center = global_position.distance_to(tile_center) + if dist_to_center < FALLOUT_CENTER_THRESHOLD: + # Close enough: snap to center and start sinking + global_position = tile_center + fell_in_fallout = true + fallout_sink_progress = 1.0 + can_be_collected = false + if collection_area: + collection_area.set_deferred("monitoring", false) + # Route fuse sound to fallout bus if already playing (e.g. rolled onto fallout) + var sfx_fuse = get_node_or_null("SfxFuse") + if sfx_fuse and sfx_fuse.playing: + sfx_fuse.bus = _fallout_sfx_bus() + else: + # Glide toward tile center (like player on quicksand) + var dir = (tile_center - global_position).normalized() + var edge_t = clamp(dist_to_center / FALLOUT_TILE_HALF_SIZE, 0.0, 1.0) + var edge_drag_factor = lerp(1.0, 0.45, edge_t) + var strength_mult = 1.0 + 0.8 * (1.0 - clamp(dist_to_center / FALLOUT_TILE_HALF_SIZE, 0.0, 1.0)) + velocity = dir * FALLOUT_GLIDE_SPEED * strength_mult * edge_drag_factor + else: + # Normal ground: apply friction throw_velocity = throw_velocity.lerp(Vector2.ZERO, delta * 5.0) if throw_velocity.length() < 5.0: throw_velocity = Vector2.ZERO @@ -272,10 +312,14 @@ func _land(): is_airborne = false position_z = 0.0 velocity_z = 0.0 + velocity = Vector2.ZERO + throw_velocity = Vector2.ZERO - # If landed on fallout tile: sink and will explode with no visual/damage (sound + screenshake only) + # If landed on fallout tile: lock to tile center and sink in place (no visual/damage, sound + screenshake only) var gw = get_tree().get_first_node_in_group("game_world") if gw and gw.has_method("_is_position_on_fallout_tile") and gw._is_position_on_fallout_tile(global_position): + if gw.has_method("_get_tile_center_at"): + global_position = gw._get_tile_center_at(global_position) fell_in_fallout = true fallout_sink_progress = 1.0 can_be_collected = false @@ -300,10 +344,17 @@ func _explode(): if fuse_light: fuse_light.enabled = false - # Fell in fallout: no explosion visual, no damage, but sound + screenshake + # Fell in fallout: no explosion visual, no damage, but sound (filtered) + screenshake if fell_in_fallout: - if has_node("SfxExplosion"): - $SfxExplosion.play() + var sfx_explosion = get_node_or_null("SfxExplosion") + if sfx_explosion: + sfx_explosion.bus = _fallout_sfx_bus() + # Reparent so sound keeps playing after bomb is freed (otherwise queue_free cuts it off) + var game_world = get_tree().get_first_node_in_group("game_world") + if game_world: + sfx_explosion.reparent(game_world, true) + sfx_explosion.finished.connect(sfx_explosion.queue_free) + sfx_explosion.play() _cause_screenshake() if bomb_area: bomb_area.set_deferred("monitoring", false) diff --git a/src/scripts/character_stats.gd b/src/scripts/character_stats.gd index d437aab..966aae5 100644 --- a/src/scripts/character_stats.gd +++ b/src/scripts/character_stats.gd @@ -201,9 +201,10 @@ var spell_amp: float: get: return (baseStats.int + get_pass("int")) * 0.5 +# Fixed value; movement speed is not scaled by DEX (player uses its own move_speed) var move_speed: float: get: - return 2.0 + ((baseStats.dex + get_pass("dex")) * 0.01) + return 1.0 var attack_speed: float: get: diff --git a/src/scripts/detected_effect.gd b/src/scripts/detected_effect.gd index 40ac9dc..f505b76 100644 --- a/src/scripts/detected_effect.gd +++ b/src/scripts/detected_effect.gd @@ -7,13 +7,13 @@ extends Node2D # "enemy" -> frames 2123-2135, red light (e.g. enemy_hand) const FRAME_RATE: float = 8.0 # frames per second -const LIFETIME: float = 30.0 +const LIFETIME: float = 30.0 # Default (chest, enemy); trap/cracked use EFFECT_CONFIG lifetime const FADE_DURATION: float = 2.0 -# Effect type -> { frames: Array, light_color: Color, optional light_energy: float } +# Effect type -> { frames: Array, light_color: Color, optional light_energy: float, optional lifetime: float } const EFFECT_CONFIG: Dictionary = { "chest": {"frames": [169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179], "light_color": Color(0.35, 0.5, 0.95, 1)}, - "trap": {"frames": [274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284], "light_color": Color(0.7, 0.35, 0.95, 1)}, + "trap": {"frames": [274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284], "light_color": Color(0.7, 0.35, 0.95, 1), "lifetime": 5.0}, "enemy": {"frames": [2123, 2124, 2125, 2126, 2127, 2128, 2129, 2130, 2131, 2132, 2133, 2134, 2135], "light_color": Color(1.0, 0.1, 0.1, 1), "light_energy": 1.4} } @@ -22,6 +22,7 @@ var _elapsed: float = 0.0 var _fading: bool = false var _fade_elapsed: float = 0.0 var _initial_light_energy: float = 0.9 +var _lifetime: float = LIFETIME @onready var fx_sprite: Sprite2D = $FxSprite @onready var detect_light: PointLight2D = $DetectLight @@ -34,12 +35,14 @@ func setup(world_pos: Vector2, effect_type: String = "chest") -> void: if EFFECT_CONFIG.has(effect_type): var cfg = EFFECT_CONFIG[effect_type] _frames = cfg.frames + _lifetime = cfg.get("lifetime", LIFETIME) if detect_light: detect_light.color = cfg.light_color if cfg.get("light_energy", 0.0) > 0.0: detect_light.energy = cfg.light_energy else: _frames = EFFECT_CONFIG.chest.frames + _lifetime = LIFETIME if fx_sprite and _frames.size() > 0: fx_sprite.frame = _frames[0] if detect_light: @@ -63,6 +66,6 @@ func _process(delta: float) -> void: if fx_sprite and _frames.size() > 0: var frame_idx = int(_elapsed * FRAME_RATE) % _frames.size() fx_sprite.frame = _frames[frame_idx] - if _elapsed >= LIFETIME: + if _elapsed >= _lifetime: _fading = true _fade_elapsed = 0.0 diff --git a/src/scripts/enemy_humanoid.gd b/src/scripts/enemy_humanoid.gd index fd2cfbf..3855137 100644 --- a/src/scripts/enemy_humanoid.gd +++ b/src/scripts/enemy_humanoid.gd @@ -763,8 +763,7 @@ func _load_random_headgear(): "Basic Melee": [ "DarkKnightHelm.png", "DragonKnightHelm.png", "GruntHelm.png", "KnightHelm.png", "NoviceHelm.png", "PaladinHelmCyan.png", "ScoutHelmGreen.png", - "SoldierBronzeHelmBlue.png", "SoldierBronzeHelmRed.png", "SoldierGoldHelmBlue.png", - "SoldierIronHelmBlue.png", "SoldierSteelHelmBlue.png" + "SoldierBronzeHelmBlue.png", "SoldierIronHelmBlue.png", "SoldierSteelHelmBlue.png" ], "Basic Range": [ "ArcherHatCyan.png", "HunterHatRed.png", "RogueHatGreen.png" diff --git a/src/scripts/game_world.gd b/src/scripts/game_world.gd index 156ce3b..8768c46 100644 --- a/src/scripts/game_world.gd +++ b/src/scripts/game_world.gd @@ -43,6 +43,7 @@ const FOG_RAY_STEP: float = 0.5 const FOG_RAY_ANGLE_STEP: int = 10 const FOG_UPDATE_INTERVAL: float = 0.25 # Run less often to avoid spikes (was 0.1) const FOG_UPDATE_INTERVAL_CORRIDOR: float = 0.06 # Update often in corridors so vision feels correct +const FOG_PLAYER_VICINITY_RADIUS: int = 4 # Tiles around player always visible (never dull) const FOG_DEBUG_DRAW: bool = false const FOG_SIMPLE_MODE: bool = true # Whole room / corridor+rooms visible (no raycast) var fog_update_timer: float = 0.0 @@ -85,14 +86,18 @@ var _torch_darken_last_room_id: String = "" var _torch_darken_target_scale: float = 1.0 var _torch_darken_current_scale: float = 1.0 const _TORCH_DARKEN_LERP_SPEED: float = 4.0 -const _TORCH_DARKEN_MIN_SCALE: float = 0.05 # Floor brightness so it's never insanely dark; same for all players +const _TORCH_DARKEN_MIN_SCALE: float = 0.18 # Floor brightness so 0-torch rooms are dark but not broken; same for all players +const _TORCH_DARKEN_DEFAULT_RAW: float = 0.6 # When corridor has no connected rooms, use this (avoid stuck at min brightness) var _synced_darkness_scale: float = 1.0 # Server syncs this to clients so host and joiner see same darkness var _last_synced_darkness_sent: float = -1.0 # Server: last value we sent var _darkness_sync_timer: float = 0.0 # Server: throttle sync RPCs +var _last_darkness_debug_scale: float = -1.0 # For debug: only print when brightness changes var seen_by_player: Dictionary = {} # player_name -> PackedInt32Array (0 unseen, 1 seen) var combined_seen: PackedInt32Array = PackedInt32Array() var explored_map: PackedInt32Array = PackedInt32Array() # 0 unseen, 1 explored var fog_debug_lines: Array = [] +# When true, fog update only marks current room (or corridor tiles), not all corridor-connected rooms (used on level load) +var _restrict_fog_to_current_room_only: bool = false # Dungeon generation var dungeon_data: Dictionary = {} @@ -131,7 +136,7 @@ var _fallout_cache_map_size: Vector2i = Vector2i.ZERO # Cracked floor: stand too long -> tile breaks and becomes fallout var cracked_stand_timers: Dictionary = {} # "player_key|tx|ty" -> float (seconds on that tile) -const CRACKED_STAND_DURATION: float = 0.9 # Seconds standing on cracked tile before it breaks +const CRACKED_STAND_DURATION: float = 0.2 # Seconds standing on cracked tile before it breaks const CRACKED_TILE_ATLAS: Vector2i = Vector2i(15, 16) # Cracked floor: normally invisible; once per game per tile a player can roll perception when close to reveal const CRACKED_DETECTION_RADIUS: float = 99.0 # Same as trap detection (pixels) @@ -3277,6 +3282,68 @@ func _init_fog_of_war(): _synced_darkness_scale = 1.0 _last_synced_darkness_sent = -1.0 _darkness_sync_timer = 0.0 + _last_darkness_debug_scale = -1.0 + # Clear minimap again for new level so no level-1 room stays visible (in case clear in _clear_level ran before node was ready) + var minimap_draw = get_node_or_null("Minimap/MarginContainer/MinimapView/MinimapDraw") + if minimap_draw and minimap_draw.has_method("clear_for_new_level"): + minimap_draw.clear_for_new_level() + # Apply visibility next frame and again after a short delay (client may get position sync after first frame) + call_deferred("_apply_level_visibility") + get_tree().create_timer(0.2).timeout.connect(_apply_level_visibility) + +func _apply_level_visibility() -> void: + if not is_inside_tree(): + return + # Run one fog update and reapply torch darkening so new level (e.g. level 2) is visible immediately + if fog_node and is_instance_valid(fog_node) and not dungeon_data.is_empty(): + var map_size_apply = dungeon_data.map_size + var total_apply = map_size_apply.x * map_size_apply.y + if combined_seen.size() != total_apply: + combined_seen.resize(total_apply) + if explored_map.size() != total_apply: + explored_map.resize(total_apply) + # Level 2+ with entrance: only reveal start_room + player vicinity. Do NOT run full fog update or corridor BFS, + # otherwise the 24-step BFS from the entrance floods into adjacent rooms and pre-explores them. + var level_load_with_entrance: bool = current_level > 1 and dungeon_data.has("entrance") and not dungeon_data.entrance.is_empty() and dungeon_data.has("start_room") and not dungeon_data.start_room.is_empty() + if level_load_with_entrance: + for i in range(total_apply): + combined_seen[i] = 0 + explored_map[i] = 0 + var start_room: Dictionary = dungeon_data.start_room + _mark_room_visible(start_room) + _mark_room_explored(start_room) + var local_list = player_manager.get_local_players() + for player in local_list: + if not player or not is_instance_valid(player): + continue + var pt = Vector2i(int(player.global_position.x / FOG_TILE_SIZE), int(player.global_position.y / FOG_TILE_SIZE)) + for dx in range(-FOG_PLAYER_VICINITY_RADIUS, FOG_PLAYER_VICINITY_RADIUS + 1): + for dy in range(-FOG_PLAYER_VICINITY_RADIUS, FOG_PLAYER_VICINITY_RADIUS + 1): + var tx = pt.x + dx + var ty = pt.y + dy + if tx >= 0 and ty >= 0 and tx < map_size_apply.x and ty < map_size_apply.y: + var idx = tx + ty * map_size_apply.x + if idx >= 0 and idx < combined_seen.size(): + combined_seen[idx] = 1 + for i in range(total_apply): + if combined_seen[i] != 0: + explored_map[i] = 1 + else: + # Level 1 or no entrance: run normal fog update once + fog_update_timer = FOG_UPDATE_INTERVAL + fog_visual_tick = 1 + _restrict_fog_to_current_room_only = true + _update_fog_of_war(0.0) + _restrict_fog_to_current_room_only = false + if current_level > 1 and dungeon_data.has("start_room") and not dungeon_data.start_room.is_empty(): + var start_room: Dictionary = dungeon_data.start_room + _mark_room_visible(start_room) + _mark_room_explored(start_room) + # Fog texture: full update both phases so the whole screen isn't half black + if fog_node.has_method("set_maps"): + fog_node.set_maps(explored_map, combined_seen, 0) + fog_node.set_maps(explored_map, combined_seen, 1) + _reapply_torch_darkening() func _update_fog_of_war(delta: float) -> void: if not fog_node or dungeon_data.is_empty() or not dungeon_data.has("map_size"): @@ -3341,10 +3408,27 @@ func _update_fog_of_war(delta: float) -> void: for idx in range(cached_corridor_mask.size()): if cached_corridor_mask[idx] == 1: combined_seen[idx] = 1 - for room in cached_corridor_rooms: - _mark_room_in_seen_map(combined_seen, room) + # On level load we only mark corridor tiles, not all connected rooms (avoids minimap pre-showing many rooms) + if not _restrict_fog_to_current_room_only: + for room in cached_corridor_rooms: + _mark_room_in_seen_map(combined_seen, room) last_corridor_fog_update = Time.get_ticks_msec() / 1000.0 + # Guarantee: player's immediate vicinity is always visible (never dull) so "where I am" is never dimmed + var map_size_vis = dungeon_data.map_size + for player in local_player_list: + if not player or not is_instance_valid(player): + continue + var pt = Vector2i(int(player.global_position.x / FOG_TILE_SIZE), int(player.global_position.y / FOG_TILE_SIZE)) + for dx in range(-FOG_PLAYER_VICINITY_RADIUS, FOG_PLAYER_VICINITY_RADIUS + 1): + for dy in range(-FOG_PLAYER_VICINITY_RADIUS, FOG_PLAYER_VICINITY_RADIUS + 1): + var tx = pt.x + dx + var ty = pt.y + dy + if tx >= 0 and ty >= 0 and tx < map_size_vis.x and ty < map_size_vis.y: + var idx = tx + ty * map_size_vis.x + if idx >= 0 and idx < combined_seen.size(): + combined_seen[idx] = 1 + # Merge visible into explored (single pass) for i in range(total): if combined_seen[i] != 0: @@ -3621,6 +3705,18 @@ func _median_torch_scale_from_rooms(rooms: Array) -> float: func _torch_scale_to_display(raw: float) -> float: return clampf(_TORCH_DARKEN_MIN_SCALE + (1.0 - _TORCH_DARKEN_MIN_SCALE) * raw, _TORCH_DARKEN_MIN_SCALE, 1.0) +# True if world_pos is inside the entrance area (level > 1). Used so brightness uses start_room when standing in entrance. +func _is_position_in_entrance_area(world_pos: Vector2) -> bool: + if current_level <= 1 or dungeon_data.is_empty() or not dungeon_data.has("entrance") or dungeon_data.entrance.is_empty(): + return false + var ed = dungeon_data.entrance + if not ed.has("world_pos") or not ed.has("world_size"): + return false + var c = ed.world_pos + var sz = ed.world_size + var half = Vector2(sz.x * 0.5, sz.y * 0.5) + return world_pos.x >= c.x - half.x and world_pos.x <= c.x + half.x and world_pos.y >= c.y - half.y and world_pos.y <= c.y + half.y + # Compute target darkness scale for a given world position (for sync: server can use any player's position) func _get_darkness_scale_at_position(world_pos: Vector2) -> float: var p_tile = Vector2i(int(world_pos.x / FOG_TILE_SIZE), int(world_pos.y / FOG_TILE_SIZE)) @@ -3631,9 +3727,38 @@ func _get_darkness_scale_at_position(world_pos: Vector2) -> float: var tc = clampi(_count_torches_in_room(current_room), 0, 4) raw = tc / 4.0 else: - raw = _median_torch_scale_from_rooms(cached_corridor_rooms) + # Level 2+: in entrance use start_room brightness so first room isn't dimmed + if _is_position_in_entrance_area(world_pos) and dungeon_data.has("start_room") and not dungeon_data.start_room.is_empty(): + var tc = clampi(_count_torches_in_room(dungeon_data.start_room), 0, 4) + raw = tc / 4.0 + else: + raw = _median_torch_scale_from_rooms(cached_corridor_rooms) + if raw <= 0.0: + raw = _TORCH_DARKEN_DEFAULT_RAW return _torch_scale_to_display(raw) +func _set_darkness_debug(msg: String) -> void: + var hud = get_tree().get_first_node_in_group("ingame_hud") + if hud and hud.has_method("update_darkness_debug"): + hud.update_darkness_debug(msg) + +func _sync_ambient_to_canvas_modulate() -> void: + # Tile shader uses "ambient" for darkening; it was set once in _apply_dungeon_color_scheme. + # Keep it in sync with CanvasModulate so brightness=1.0 actually looks bright (tiles + lights). + var cm = get_node_or_null("CanvasModulate") + if not cm or not is_instance_valid(cm): + return + var ambient_color = cm.color + var env_node = get_node_or_null("Environment") + if not env_node: + return + for child in env_node.get_children(): + if not child is TileMapLayer or not is_instance_valid(child): + continue + var mat = (child as TileMapLayer).material + if mat is ShaderMaterial: + (mat as ShaderMaterial).set_shader_parameter("ambient", ambient_color) + func _update_canvas_modulate_by_torches() -> void: if dungeon_data.is_empty() or not dungeon_data.has("torches"): return @@ -3647,6 +3772,10 @@ func _update_canvas_modulate_by_torches() -> void: _torch_darken_current_scale = lerpf(_torch_darken_current_scale, _synced_darkness_scale, clampf(dt * _TORCH_DARKEN_LERP_SPEED, 0.0, 1.0)) var brightness := _torch_darken_current_scale cm.color = Color(brightness, brightness, brightness) + _sync_ambient_to_canvas_modulate() + if abs(brightness - _last_darkness_debug_scale) > 0.005 or _last_darkness_debug_scale < 0: + _last_darkness_debug_scale = brightness + _set_darkness_debug("CLIENT (synced): brightness=%.3f" % brightness) return # Server or single player: compute target scale var local_list = player_manager.get_local_players() if player_manager else [] @@ -3667,6 +3796,11 @@ func _update_canvas_modulate_by_torches() -> void: _darkness_sync_timer = 0.0 _last_synced_darkness_sent = target_scale _sync_darkness_scale.rpc(target_scale) + # Debug: server applies same scale + var s_server := maxf(target_scale, _TORCH_DARKEN_MIN_SCALE) + if abs(s_server - _last_darkness_debug_scale) > 0.005 or _last_darkness_debug_scale < 0: + _last_darkness_debug_scale = s_server + _set_darkness_debug("SERVER: brightness=%.3f (max over %d players)" % [s_server, all_players.size()]) else: if local_list.is_empty() or not local_list[0]: return @@ -3691,7 +3825,10 @@ func _update_canvas_modulate_by_torches() -> void: var tc = clampi(_count_torches_in_room(current_room), 0, 4) _torch_darken_target_scale = _torch_scale_to_display(tc / 4.0) else: - _torch_darken_target_scale = _torch_scale_to_display(_median_torch_scale_from_rooms(cached_corridor_rooms)) + var raw = _median_torch_scale_from_rooms(cached_corridor_rooms) + if raw <= 0.0: + raw = _TORCH_DARKEN_DEFAULT_RAW + _torch_darken_target_scale = _torch_scale_to_display(raw) target_scale = _torch_darken_target_scale var delta := get_process_delta_time() if not (multiplayer.has_multiplayer_peer() and multiplayer.is_server()): @@ -3700,6 +3837,26 @@ func _update_canvas_modulate_by_torches() -> void: _torch_darken_current_scale = target_scale var s := maxf(_torch_darken_current_scale, _TORCH_DARKEN_MIN_SCALE) cm.color = Color(s, s, s) + _sync_ambient_to_canvas_modulate() + # Debug: print when brightness changes (single-player or local view) + if not (multiplayer.has_multiplayer_peer() and multiplayer.is_server()): + if abs(s - _last_darkness_debug_scale) > 0.005 or _last_darkness_debug_scale < 0: + var reason := "" + var p = local_list[0] + var p_tile_d = Vector2i(int(p.global_position.x / FOG_TILE_SIZE), int(p.global_position.y / FOG_TILE_SIZE)) + var current_room_d = _find_room_at_tile(p_tile_d) + var in_room_d := not current_room_d.is_empty() + if in_room_d: + var tc = clampi(_count_torches_in_room(current_room_d), 0, 4) + reason = "ROOM tile(%d,%d) torches=%d raw=%.2f" % [p_tile_d.x, p_tile_d.y, tc, tc / 4.0] + else: + var raw_c = _median_torch_scale_from_rooms(cached_corridor_rooms) + var used_default = raw_c <= 0.0 + if used_default: + raw_c = _TORCH_DARKEN_DEFAULT_RAW + reason = "CORRIDOR tile(%d,%d) median_raw=%.2f (connected_rooms=%d%s)" % [p_tile_d.x, p_tile_d.y, raw_c, cached_corridor_rooms.size(), " USED_DEFAULT" if used_default else ""] + _last_darkness_debug_scale = s + _set_darkness_debug("brightness=%.3f (target=%.3f)\n%s" % [s, target_scale, reason]) @rpc("authority", "reliable") func _sync_darkness_scale(darkness_scale: float) -> void: @@ -3723,7 +3880,14 @@ func _reapply_torch_darkening() -> void: var tc = clampi(_count_torches_in_room(current_room), 0, 4) raw = tc / 4.0 else: - raw = _median_torch_scale_from_rooms(cached_corridor_rooms) + # Level 2+: in entrance use start_room so first room brightness is correct + if _is_position_in_entrance_area(p.global_position) and dungeon_data.has("start_room") and not dungeon_data.start_room.is_empty(): + var tc = clampi(_count_torches_in_room(dungeon_data.start_room), 0, 4) + raw = tc / 4.0 + else: + raw = _median_torch_scale_from_rooms(cached_corridor_rooms) + if raw <= 0.0: + raw = _TORCH_DARKEN_DEFAULT_RAW var t := _torch_scale_to_display(raw) _torch_darken_target_scale = t _torch_darken_current_scale = t @@ -3734,6 +3898,20 @@ func _reapply_torch_darkening() -> void: _torch_darken_in_room_last = in_room _torch_darken_last_room_id = room_id cm.color = Color(t, t, t) + _sync_ambient_to_canvas_modulate() + if abs(t - _last_darkness_debug_scale) > 0.005 or _last_darkness_debug_scale < 0: + _last_darkness_debug_scale = t + var reason := "" + if in_room: + var tc = clampi(_count_torches_in_room(current_room), 0, 4) + reason = "REAPPLY ROOM tile(%d,%d) torches=%d raw=%.2f" % [p_tile.x, p_tile.y, tc, tc / 4.0] + else: + var raw_r = _median_torch_scale_from_rooms(cached_corridor_rooms) + var used_default = raw_r <= 0.0 + if used_default: + raw_r = _TORCH_DARKEN_DEFAULT_RAW + reason = "REAPPLY CORRIDOR tile(%d,%d) raw=%.2f connected_rooms=%d%s" % [p_tile.x, p_tile.y, raw_r, cached_corridor_rooms.size(), " USED_DEFAULT" if used_default else ""] + _set_darkness_debug("%s -> brightness=%.3f" % [reason, t]) func _get_view_angle_weight(view_dir: Vector2, ray_dir: Vector2) -> float: if view_dir.length() < 0.1 or ray_dir.length() < 0.1: @@ -3778,7 +3956,7 @@ func _build_corridor_mask(start_tile: Vector2i) -> PackedInt32Array: queue.append(start_tile) mask[start_tile.x + start_tile.y * map_size.x] = 1 - var max_steps = 10 # Reduced from 24 to prevent corridor branches from reaching far rooms + var max_steps = 10 # Visible corridor stretch; avoid seeing rooms far away while queue.size() > 0: var tile = queue.pop_front() var dist = abs(tile.x - start_tile.x) + abs(tile.y - start_tile.y) @@ -3811,8 +3989,8 @@ func _get_rooms_connected_to_corridor(corridor_mask: PackedInt32Array, player_ti if dungeon_data.is_empty() or not dungeon_data.has("rooms") or player_tile.x < 0: return rooms var map_size = dungeon_data.map_size - # Only check rooms within a small distance of the player (8 tiles) - var max_room_distance = 8 + # Only check rooms within a small distance of the player (4 tiles) so we don't reveal far rooms + var max_room_distance = 4 var room_distances = [] for room in dungeon_data.rooms: # Calculate room center distance from player - skip if too far @@ -3860,9 +4038,9 @@ func _get_rooms_connected_to_corridor(corridor_mask: PackedInt32Array, player_ti if touches_corridor: room_distances.append({"room": room, "dist": closest_corridor_dist}) - # Sort by distance and return only the 2 closest rooms + # Sort by distance and return only the 1 closest room (avoid revealing far rooms) room_distances.sort_custom(func(a, b): return a.dist < b.dist) - for i in range(min(2, room_distances.size())): + for i in range(min(1, room_distances.size())): rooms.append(room_distances[i].room) return rooms @@ -4402,7 +4580,8 @@ func _render_dungeon(): LogManager.log("GameWorld: Placed " + str(above_tiles_placed) + " tiles on above layer", LogManager.CATEGORY_DUNGEON) LogManager.log("GameWorld: Dungeon rendered on TileMapLayer", LogManager.CATEGORY_DUNGEON) _init_fog_of_war() - + # _init_fog_of_war() schedules _apply_level_visibility() deferred so level 2 isn't all black + # Create stairs Area2D if stairs data exists _create_stairs_area() @@ -7602,6 +7781,8 @@ func _clear_level(): fog_node.queue_free() fog_node = null fog_tile_to_room_index.resize(0) + fog_update_timer = 0.0 + fog_visual_tick = 0 _cached_closed_door_tiles.clear() _fallout_tile_cache.resize(0) _fallout_cache_map_size = Vector2i.ZERO diff --git a/src/scripts/ingame_hud.gd b/src/scripts/ingame_hud.gd index e384f78..1c6d857 100644 --- a/src/scripts/ingame_hud.gd +++ b/src/scripts/ingame_hud.gd @@ -26,6 +26,7 @@ var label_disconnected: Label = null var label_matchbox_status: Label = null var label_ice_status: Label = null var label_data_channels_status: Label = null +var label_darkness_debug: Label = null var game_world: Node = null var network_manager: Node = null @@ -37,6 +38,7 @@ var timer_running: bool = true # Flag to stop/start timer const HUD_BASE_SIZE: Vector2 = Vector2(1280, 720) func _ready(): + add_to_group("ingame_hud") print("IngameHUD: _ready() called") # Find nodes safely (using get_node_or_null to avoid crashes) @@ -72,6 +74,7 @@ func _ready(): label_matchbox_status = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerConnectionStatus/LabelMatchboxStatus") label_ice_status = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerConnectionStatus/LabelICEStatus") label_data_channels_status = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerConnectionStatus/LabelDataChannelsStatus") + label_darkness_debug = get_node_or_null("UpperLeft/HBoxContainer/VBoxContainerDarknessDebug/LabelDarknessDebug") # Debug: Log if connection status labels weren't found if not label_matchbox_status: @@ -135,6 +138,10 @@ func _ready(): # Add HP value label (curr/max, like inventory) and MP bar _setup_hp_mp_ui() +func update_darkness_debug(text: String) -> void: + if label_darkness_debug: + label_darkness_debug.text = text + func _on_player_connected(_peer_id: int, _player_info: Dictionary): _update_host_info() diff --git a/src/scripts/interactable_object.gd b/src/scripts/interactable_object.gd index 65e9276..0b790fb 100644 --- a/src/scripts/interactable_object.gd +++ b/src/scripts/interactable_object.gd @@ -100,7 +100,10 @@ func _apply_hidden_state() -> void: sprite_above.modulate.a = 0.0 if shadow: shadow.visible = false - # Enable detection area and connect if not already + # No collision while invisible so player doesn't bump into hidden chest + collision_layer = 0 + collision_mask = 0 + # Enable detection area and connect if not already (perception still runs when player enters) var det = get_node_or_null("DetectionArea") if det: if not det.body_entered.is_connected(_on_detection_area_body_entered): @@ -115,6 +118,9 @@ func _apply_hidden_state() -> void: sprite_above.modulate.a = 1.0 if shadow: shadow.visible = true + # Restore collision when visible (detected or not hidden) + collision_layer = 2 + collision_mask = 1 | 2 | 4 func _on_detection_area_body_entered(body: Node) -> void: if not body.is_in_group("player"): diff --git a/src/scripts/minimap.gd b/src/scripts/minimap.gd index 47689a8..d812e81 100644 --- a/src/scripts/minimap.gd +++ b/src/scripts/minimap.gd @@ -78,6 +78,9 @@ func clear_for_new_level() -> void: _overlay_control.queue_redraw() func set_maps(explored_map: PackedInt32Array, map_size: Vector2i, grid: Array, player_tile: Vector2i = Vector2i(-1, -1), exit_tile: Vector2i = Vector2i(-1, -1), exit_discovered: bool = false, other_player_tiles: Array = [], rooms: Array = [], visible_tiles: Array = []) -> void: + # New level (map size changed): clear so we don't show level 1 rooms as explored on level 2 + if map_size != _map_size and _map_size != Vector2i.ZERO: + clear_for_new_level() _explored_map = explored_map _map_size = map_size _grid = grid diff --git a/src/scripts/player.gd b/src/scripts/player.gd index 20955d6..0a52625 100644 --- a/src/scripts/player.gd +++ b/src/scripts/player.gd @@ -6,10 +6,11 @@ extends CharacterBody2D var character_stats: CharacterStats var appearance_rng: RandomNumberGenerator # Deterministic RNG for appearance/stats -@export var move_speed: float = 80.0 +@export var move_speed: float = 65.0 # Base move speed (not affected by DEX) @export var grab_range: float = 20.0 @export var base_throw_force: float = 80.0 # Base throw force (reduced from 150), scales with STR @export var cone_light_angle: float = 180.0 # Cone spread angle in degrees (adjustable, default wider) +const CONE_LIGHT_LERP_SPEED: float = 12.0 # How quickly cone rotation follows target (higher = snappier) # Network identity var peer_id: int = 1 @@ -1743,16 +1744,13 @@ func _update_facing_from_mouse(mouse_direction: Vector2): # Mark that mouse control is active (prevents movement keys from overriding attack direction) mouse_control_active = true - # Store full 360-degree direction for attacks + # Store full 360-degree direction for attacks (cone light uses this for smooth rotation) if mouse_direction.length() > 0.1: facing_direction_vector = mouse_direction.normalized() var new_direction = _get_direction_from_vector(mouse_direction) as Direction - - # Update direction and cone light rotation if changed if new_direction != current_direction: current_direction = new_direction - _update_cone_light_rotation() func _set_animation(anim_name: String): if current_animation != anim_name: @@ -1782,10 +1780,18 @@ func _direction_to_angle(direction: int) -> float: _: return PI / 2.0 # Default to DOWN -# Update cone light rotation based on player's facing direction -func _update_cone_light_rotation(): - if cone_light: - cone_light.rotation = _direction_to_angle(current_direction) + (PI / 2) +# Update cone light rotation based on facing (lerps toward target for smooth 360° movement) +func _update_cone_light_rotation(delta: float = 1.0): + if not cone_light: + return + var target_angle: float + if facing_direction_vector.length() > 0.1: + target_angle = facing_direction_vector.angle() + (PI / 2.0) + else: + target_angle = _direction_to_angle(current_direction) + (PI / 2.0) + # Lerp toward target (delta=1.0 in _ready snaps; in _physics_process uses smooth follow) + var t = 1.0 - exp(-CONE_LIGHT_LERP_SPEED * delta) + cone_light.rotation = lerp_angle(cone_light.rotation, target_angle, t) # Create a cone-shaped light texture programmatically # Creates a directional cone texture that extends forward and fades to the sides @@ -2511,25 +2517,17 @@ func _handle_input(): var new_direction = _get_direction_from_vector(shield_block_direction) as Direction if new_direction != current_direction: current_direction = new_direction - _update_cone_light_rotation() elif not is_pushing and (not mouse_control_active or input_device != -1) and direction_lock_timer <= 0.0: var new_direction = _get_direction_from_vector(input_vector) as Direction - - # Update direction and cone light rotation if changed if new_direction != current_direction: current_direction = new_direction - _update_cone_light_rotation() elif direction_lock_timer > 0.0 and locked_facing_direction.length() > 0.0: - # Use locked direction for animations during attack var new_direction = _get_direction_from_vector(locked_facing_direction) as Direction if new_direction != current_direction: current_direction = new_direction - _update_cone_light_rotation() elif is_pushing or (held_object and not is_lifting): - # Keep direction from when grab started (don't turn to face the object) if push_direction_locked != current_direction: current_direction = push_direction_locked as Direction - _update_cone_light_rotation() # Set animation based on state if grabbed_by_enemy_hand: @@ -2571,20 +2569,20 @@ func _handle_input(): elif is_pushing or (held_object and not is_lifting): if is_pushing: _set_animation("IDLE_PUSH") - # Keep direction from when grab started if push_direction_locked != current_direction: current_direction = push_direction_locked as Direction - _update_cone_light_rotation() elif is_shielding: - # Keep locked block direction when shielding and idle var new_direction = _get_direction_from_vector(shield_block_direction) as Direction if new_direction != current_direction: current_direction = new_direction - _update_cone_light_rotation() else: if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF" and current_animation != "AXE" and current_animation != "PUNCH" and current_animation != "CONJURE" and current_animation != "LIFT" and current_animation != "FINISH_SPELL": _set_animation("IDLE") + # Cone light lerps toward facing direction every frame (360°) + if is_local_player and cone_light and cone_light.visible: + _update_cone_light_rotation(get_physics_process_delta_time()) + # Handle drag sound for interactable objects var is_dragging_now = false if held_object and is_pushing and not is_lifting: